diff --git a/plugins/provisioners/chef/cap/debian/chef_install.rb b/plugins/provisioners/chef/cap/debian/chef_install.rb new file mode 100644 index 000000000..5bb5f6acb --- /dev/null +++ b/plugins/provisioners/chef/cap/debian/chef_install.rb @@ -0,0 +1,19 @@ +require_relative "../../omnibus" + +module VagrantPlugins + module Chef + module Cap + module Debian + module ChefInstall + def self.chef_install(machine, version, prerelease) + machine.communicate.sudo("apt-get update -y -qq") + machine.communicate.sudo("apt-get install -y -qq curl") + + command = Omnibus.build_command(version, prerelease) + machine.communicate.sudo(command) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/linux/chef_installed.rb b/plugins/provisioners/chef/cap/linux/chef_installed.rb new file mode 100644 index 000000000..aa891c541 --- /dev/null +++ b/plugins/provisioners/chef/cap/linux/chef_installed.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module Chef + module Cap + module Linux + module ChefInstalled + # Check if Chef is installed at the given version. + # @return [true, false] + def self.chef_installed(machine, version) + knife = "/opt/chef/bin/knife" + command = "test -x #{knife}" + + if version != :latest + command << "&& #{knife} --version | grep 'Chef: #{version}'" + end + + machine.communicate.test(command, sudo: true) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/redhat/chef_install.rb b/plugins/provisioners/chef/cap/redhat/chef_install.rb new file mode 100644 index 000000000..49db9bf9c --- /dev/null +++ b/plugins/provisioners/chef/cap/redhat/chef_install.rb @@ -0,0 +1,18 @@ +require_relative "../../omnibus" + +module VagrantPlugins + module Chef + module Cap + module Redhat + module ChefInstall + def self.chef_install(machine, version, prerelease) + machine.communicate.sudo("yum install -y -q curl") + + command = Omnibus.build_command(version, prerelease) + machine.communicate.sudo(command) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/config/base.rb b/plugins/provisioners/chef/config/base.rb index 1ee8bca0a..37124ad3f 100644 --- a/plugins/provisioners/chef/config/base.rb +++ b/plugins/provisioners/chef/config/base.rb @@ -30,6 +30,10 @@ module VagrantPlugins # @return [String, Symbol] attr_accessor :log_level + # Install a prerelease version of Chef. + # @return [true, false] + attr_accessor :prerelease + # The version of Chef to install. If Chef is already installed on the # system, the installed version is compared with the requested version. # If they match, no action is taken. If they do not match, version of @@ -47,19 +51,26 @@ module VagrantPlugins def initialize super - @binary_path = UNSET_VALUE - @binary_env = UNSET_VALUE - @install = UNSET_VALUE - @log_level = UNSET_VALUE - @version = UNSET_VALUE + @binary_path = UNSET_VALUE + @binary_env = UNSET_VALUE + @install = UNSET_VALUE + @log_level = UNSET_VALUE + @prerelease = UNSET_VALUE + @version = UNSET_VALUE end def finalize! - @binary_path = nil if @binary_path == UNSET_VALUE - @binary_env = nil if @binary_env == UNSET_VALUE - @install = true if @install == UNSET_VALUE - @log_level = :info if @log_level == UNSET_VALUE - @version = :latest if @version == UNSET_VALUE + @binary_path = nil if @binary_path == UNSET_VALUE + @binary_env = nil if @binary_env == UNSET_VALUE + @install = true if @install == UNSET_VALUE + @log_level = :info if @log_level == UNSET_VALUE + @prerelease = false if @prerelease == UNSET_VALUE + @version = :latest if @version == UNSET_VALUE + + # Make sure the install is a symbol if it's not a boolean + if @install.respond_to?(:to_sym) + @install = @install.to_sym + end # Make sure the version is a symbol if it's not a boolean if @version.respond_to?(:to_sym) diff --git a/plugins/provisioners/chef/config/chef_apply.rb b/plugins/provisioners/chef/config/chef_apply.rb index 8b77394f4..9f313b790 100644 --- a/plugins/provisioners/chef/config/chef_apply.rb +++ b/plugins/provisioners/chef/config/chef_apply.rb @@ -1,3 +1,5 @@ +require_relative "base" + module VagrantPlugins module Chef module Config diff --git a/plugins/provisioners/chef/installer.rb b/plugins/provisioners/chef/installer.rb new file mode 100644 index 000000000..e8b08f10d --- /dev/null +++ b/plugins/provisioners/chef/installer.rb @@ -0,0 +1,45 @@ +module VagrantPlugins + module Chef + class Installer + def initialize(machine, options = {}) + @machine = machine + @version = options.fetch(:version, :latest) + @prerelease = options.fetch(:prerelease, :latest) + @force = options.fetch(:force, false) + end + + # This handles verifying the Chef installation, installing it if it was + # requested, and so on. This method will raise exceptions if things are + # wrong. + def ensure_installed + # If the guest cannot check if Chef is installed, just exit printing a + # warning... + if !@machine.guest.capability?(:chef_installed) + @machine.ui.warn(I18n.t("vagrant.chef_cant_detect")) + return + end + + if !should_install_chef? + @machine.ui.info(I18n.t("vagrant.chef_already_installed", + version: @version.to_s)) + return + end + + @machine.ui.detail(I18n.t("vagrant.chef_installing", + version: @version.to_s)) + @machine.guest.capability(:chef_install, @version, @prerelease) + + if !@machine.guest.capability(:chef_installed, @version) + raise Provisioner::Base::ChefError, :install_failed + end + end + + # Determine if Chef should be installed. Chef is installed if the "force" + # option is given or if the guest does not have Chef installed at the + # proper version. + def should_install_chef? + @force || !@machine.guest.capability(:chef_installed, @version) + end + end + end +end diff --git a/plugins/provisioners/chef/omnibus.rb b/plugins/provisioners/chef/omnibus.rb new file mode 100644 index 000000000..db62b4fdc --- /dev/null +++ b/plugins/provisioners/chef/omnibus.rb @@ -0,0 +1,28 @@ +module VagrantPlugins + module Chef + module Omnibus + OMNITRUCK = "https://www.getchef.com/chef/install.sh".freeze + + # Read more about the Omnibus installer here: + # https://docs.getchef.com/install_omnibus.html + def build_command(version, prerelease = false) + command = "curl -sL #{OMNITRUCK} | sudo bash" + + if prerelease || version != :latest + command << " -s --" + end + + if prerelease + command << " -p" + end + + if version != :latest + command << " -v \"#{version}\"" + end + + command + end + module_function :build_command + end + end +end diff --git a/plugins/provisioners/chef/plugin.rb b/plugins/provisioners/chef/plugin.rb index 343a82018..b2fb5cef5 100644 --- a/plugins/provisioners/chef/plugin.rb +++ b/plugins/provisioners/chef/plugin.rb @@ -52,6 +52,21 @@ module VagrantPlugins require_relative "provisioner/chef_zero" Provisioner::ChefZero end + + guest_capability(:linux, :chef_installed) do + require_relative "cap/linux/chef_installed" + Cap::Linux::ChefInstalled + end + + guest_capability(:debian, :chef_install) do + require_relative "cap/debian/chef_install" + Cap::Debian::ChefInstall + end + + guest_capability(:redhat, :chef_install) do + require_relative "cap/redhat/chef_install" + Cap::Redhat::ChefInstall + end end end end diff --git a/plugins/provisioners/chef/provisioner/base.rb b/plugins/provisioners/chef/provisioner/base.rb index a891b1251..81f8c6e48 100644 --- a/plugins/provisioners/chef/provisioner/base.rb +++ b/plugins/provisioners/chef/provisioner/base.rb @@ -2,6 +2,8 @@ require 'tempfile' require "vagrant/util/template_renderer" +require_relative "../installer" + module VagrantPlugins module Chef module Provisioner @@ -13,6 +15,24 @@ module VagrantPlugins error_namespace("vagrant.provisioners.chef") end + def initialize(machine, config) + super + + @logger = Log4r::Logger.new("vagrant::provisioners::chef") + end + + def install_chef + return if !config.install + + @logger.info("Checking for Chef installation...") + installer = Installer.new(@machine, + force: config.install == :force, + version: config.version, + prerelease: config.prerelease, + ) + installer.ensure_installed + end + def verify_binary(binary) # Checks for the existence of chef binary and error if it # doesn't exist. @@ -20,7 +40,8 @@ module VagrantPlugins "which #{binary}", error_class: ChefError, error_key: :chef_not_detected, - binary: binary) + binary: binary, + ) end # This returns the command to run Chef for the given client diff --git a/plugins/provisioners/chef/provisioner/chef_apply.rb b/plugins/provisioners/chef/provisioner/chef_apply.rb index 04029f1a7..ed857c3b3 100644 --- a/plugins/provisioners/chef/provisioner/chef_apply.rb +++ b/plugins/provisioners/chef/provisioner/chef_apply.rb @@ -1,13 +1,18 @@ require "tempfile" +require_relative "base" + module VagrantPlugins module Chef module Provisioner - class ChefApply < Vagrant.plugin("2", :provisioner) + class ChefApply < Base def provision + install_chef + verify_binary(chef_binary_path("chef-apply")) + command = "chef-apply" - command << " --log-level #{config.log_level}" - command << " #{config.upload_path}" + command << " \"#{target_recipe_path}\"" + command << " --log_level #{config.log_level}" user = @machine.ssh_info[:username] @@ -18,7 +23,7 @@ module VagrantPlugins # Upload the recipe upload_recipe - @machine.ui.info(I18n.t("vagrant.provisioners.chef.running_chef_apply", + @machine.ui.info(I18n.t("vagrant.provisioners.chef.running_apply", script: config.path) ) @@ -34,6 +39,12 @@ module VagrantPlugins end end + # The destination (on the guest) where the recipe will live + # @return [String] + def target_recipe_path + File.join(config.upload_path, "recipe.rb") + end + # Write the raw recipe contents to a tempfile and upload that to the # machine. def upload_recipe @@ -43,8 +54,7 @@ module VagrantPlugins file.rewind # Upload the tempfile to the guest - destination = File.join(config.upload_path, "recipe.rb") - @machine.communicate.upload(file.path, destination) + @machine.communicate.upload(file.path, target_recipe_path) ensure # Delete our template file.close diff --git a/plugins/provisioners/chef/provisioner/chef_client.rb b/plugins/provisioners/chef/provisioner/chef_client.rb index 9b0c2341f..5824f56be 100644 --- a/plugins/provisioners/chef/provisioner/chef_client.rb +++ b/plugins/provisioners/chef/provisioner/chef_client.rb @@ -18,6 +18,7 @@ module VagrantPlugins end def provision + install_chef verify_binary(chef_binary_path("chef-client")) chown_provisioning_folder create_client_key_folder diff --git a/plugins/provisioners/chef/provisioner/chef_solo.rb b/plugins/provisioners/chef/provisioner/chef_solo.rb index a8c1e84d3..d05794f18 100644 --- a/plugins/provisioners/chef/provisioner/chef_solo.rb +++ b/plugins/provisioners/chef/provisioner/chef_solo.rb @@ -35,6 +35,7 @@ module VagrantPlugins end def provision + install_chef # Verify that the proper shared folders exist. check = [] @shared_folders.each do |type, local_path, remote_path| diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 617cc3b43..81365587d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -87,6 +87,14 @@ en: CFEngine running in "single run" mode. Will execute one file. cfengine_single_run_execute: |- Executing run file for CFEngine... + chef_cant_detect: |- + Vagrant does not support detecting whether Chef is installed + for the guest OS running in the machine. Vagrant will assume it is + installed and attempt to continue. + chef_already_installed: |- + Detected Chef (%{version}) is already installed + chef_installing: |- + Installing Chef (%{version})... chef_client_cleanup_failed: |- Cleaning up the '%{deletable}' for Chef failed. The stdout and stderr are shown below. Vagrant will continue destroying the machine, @@ -1754,6 +1762,16 @@ en: "The cookbook path '%{path}' doesn't exist. Ignoring..." json: "Generating chef JSON and uploading..." client_key_folder: "Creating folder to hold client key..." + install_failed: |- + Vagrant could not detect Chef on the guest! Even after Vagrant + attempted to install Chef, it could still not find Chef on the system. + Please make sure you are connected to the Internet and can access + Chef's package distribution servers. If you already have Chef + installed on this guest, you can disable the automatic Chef detection + by setting the 'install' option in the Chef configuration section of + your Vagrantfile: + + chef.install = false log_level_empty: |- The Chef provisioner requires a log level. If you did not set a log level, this is probably a bug and should be reported. @@ -1765,7 +1783,7 @@ en: guest. running_client: "Running chef-client..." running_client_again: "Running chef-client again (failed to converge)..." - running_client_apply: "Running chef-apply..." + running_apply: "Running chef-apply..." running_solo: "Running chef-solo..." running_solo_again: "Running chef-solo again (failed to converge)..." missing_shared_folders: |- diff --git a/test/unit/plugins/provisioners/chef/config/base_test.rb b/test/unit/plugins/provisioners/chef/config/base_test.rb index 6ca27ba08..2355f28cd 100644 --- a/test/unit/plugins/provisioners/chef/config/base_test.rb +++ b/test/unit/plugins/provisioners/chef/config/base_test.rb @@ -28,6 +28,12 @@ describe VagrantPlugins::Chef::Config::Base do subject.finalize! expect(subject.install).to be(true) end + + it "is converted to a symbol" do + subject.install = "force" + subject.finalize! + expect(subject.install).to eq(:force) + end end describe "#log_level" do @@ -43,6 +49,13 @@ describe VagrantPlugins::Chef::Config::Base do end end + describe "#prerelease" do + it "defaults to true" do + subject.finalize! + expect(subject.prerelease).to be(false) + end + end + describe "#version" do it "defaults to :latest" do subject.finalize! diff --git a/test/unit/plugins/provisioners/chef/omnibus_test.rb b/test/unit/plugins/provisioners/chef/omnibus_test.rb new file mode 100644 index 000000000..9c42df1cc --- /dev/null +++ b/test/unit/plugins/provisioners/chef/omnibus_test.rb @@ -0,0 +1,45 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/provisioners/chef/omnibus") + +describe VagrantPlugins::Chef::Omnibus, :focus do + let(:prefix) { "curl -sL #{described_class.const_get(:OMNITRUCK)}" } + + let(:version) { :latest } + let(:prerelease) { false } + + let(:build_command) { described_class.build_command(version, prerelease) } + + context "when prerelease is given" do + let(:prerelease) { true } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -p") + end + end + + context "when version is :latest" do + let(:version) { :latest } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash") + end + end + + context "when version is a string" do + let(:version) { "1.2.3" } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -v \"1.2.3\"") + end + end + + context "when prerelease and version are given" do + let(:version) { "1.2.3" } + let(:prerelease) { true } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -p -v \"1.2.3\"") + end + end +end