diff --git a/.gitignore b/.gitignore index 47b95a67e..801132d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ website/docs/Rakefile website/www/.sass-cache website/www/build website/www/Rakefile +exec/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c458fe37..181b8b28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ IMPROVEMENTS: more easily. Vagrant will login for you if you specify auth. [GH-4042] - providers/docker: `stop_timeout` can be used to modify the `docker stop` timeout. [GH-4504] + - provisioners/chef: Automatically install Chef when using a Chef provisioner. - synced\_folders/nfs: Won't use `sudo` to write to /etc/exports if there are write privileges. [GH-2643] - synced\_folders/smb: Credentials from one SMB will be copied to the rest. [GH-4675] 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 ae63a4522..37124ad3f 100644 --- a/plugins/provisioners/chef/config/base.rb +++ b/plugins/provisioners/chef/config/base.rb @@ -6,134 +6,98 @@ module VagrantPlugins class Base < Vagrant.plugin("2", :config) extend Vagrant::Util::Counter - attr_accessor :arguments - attr_accessor :attempts + # The path to Chef's bin/ directory. + # @return [String] attr_accessor :binary_path + + # Arbitrary environment variables to set before running the Chef + # provisioner command. + # @return [String] attr_accessor :binary_env - attr_accessor :custom_config_path - attr_accessor :encrypted_data_bag_secret_key_path - attr_accessor :environment - attr_accessor :formatter - attr_accessor :http_proxy - attr_accessor :http_proxy_user - attr_accessor :http_proxy_pass - attr_accessor :https_proxy - attr_accessor :https_proxy_user - attr_accessor :https_proxy_pass - attr_accessor :json + + # Install Chef on the system if it does not exist. Default is true. + # This is a trinary attribute (it can have three values): + # + # - true (bool) install Chef + # - false (bool) do not install Chef + # - "force" (string) install Chef, even if it is already installed at + # the proper version + # + # @return [true, false, String] + attr_accessor :install + + # The Chef log level. See the Chef docs for acceptable values. + # @return [String, Symbol] attr_accessor :log_level - attr_accessor :no_proxy - attr_accessor :node_name - attr_accessor :provisioning_path - attr_accessor :run_list - attr_accessor :file_cache_path - attr_accessor :file_backup_path - attr_accessor :verbose_logging + + # 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 + # the value specified in this attribute will be installed over top of + # the existing version (a warning will be displayed). + # + # You can also specify "latest" (default), which will install the latest + # version of Chef on the system. In this case, Chef will use whatever + # version is on the system. To force the newest version of Chef to be + # installed on every provision, set the {#install} option to "force". + # + # @return [String] + attr_accessor :version def initialize super - @arguments = UNSET_VALUE - @attempts = UNSET_VALUE - @binary_path = UNSET_VALUE - @binary_env = UNSET_VALUE - @custom_config_path = UNSET_VALUE - @encrypted_data_bag_secret_key_path = UNSET_VALUE - @environment = UNSET_VALUE - @formatter = UNSET_VALUE - @http_proxy = UNSET_VALUE - @http_proxy_user = UNSET_VALUE - @http_proxy_pass = UNSET_VALUE - @https_proxy = UNSET_VALUE - @https_proxy_user = UNSET_VALUE - @https_proxy_pass = UNSET_VALUE - @log_level = UNSET_VALUE - @no_proxy = UNSET_VALUE - @node_name = UNSET_VALUE - @provisioning_path = UNSET_VALUE - @file_cache_path = UNSET_VALUE - @file_backup_path = UNSET_VALUE - @verbose_logging = UNSET_VALUE - - @json = {} - @run_list = [] - end - - def encrypted_data_bag_secret=(value) - puts "DEPRECATION: Chef encrypted_data_bag_secret has no effect anymore." - puts "Remove this from your Vagrantfile since it'll be removed in the next" - puts "Vagrant version." + @binary_path = UNSET_VALUE + @binary_env = UNSET_VALUE + @install = UNSET_VALUE + @log_level = UNSET_VALUE + @prerelease = UNSET_VALUE + @version = UNSET_VALUE end def finalize! - @arguments = nil if @arguments == UNSET_VALUE - @attempts = 1 if @attempts == UNSET_VALUE - @binary_path = nil if @binary_path == UNSET_VALUE - @binary_env = nil if @binary_env == UNSET_VALUE - @custom_config_path = nil if @custom_config_path == UNSET_VALUE - @environment = nil if @environment == UNSET_VALUE - @formatter = nil if @formatter == UNSET_VALUE - @http_proxy = nil if @http_proxy == UNSET_VALUE - @http_proxy_user = nil if @http_proxy_user == UNSET_VALUE - @http_proxy_pass = nil if @http_proxy_pass == UNSET_VALUE - @https_proxy = nil if @https_proxy == UNSET_VALUE - @https_proxy_user = nil if @https_proxy_user == UNSET_VALUE - @https_proxy_pass = nil if @https_proxy_pass == UNSET_VALUE - @log_level = :info if @log_level == UNSET_VALUE - @no_proxy = nil if @no_proxy == UNSET_VALUE - @node_name = nil if @node_name == UNSET_VALUE - @provisioning_path = nil if @provisioning_path == UNSET_VALUE - @file_backup_path = "/var/chef/backup" if @file_backup_path == UNSET_VALUE - @file_cache_path = "/var/chef/cache" if @file_cache_path == UNSET_VALUE - @verbose_logging = false if @verbose_logging == 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 - if @encrypted_data_bag_secret_key_path == UNSET_VALUE - @encrypted_data_bag_secret_key_path = nil + # 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) + @version = @version.to_sym end # Make sure the log level is a symbol @log_level = @log_level.to_sym - - # Set the default provisioning path to be a unique path in /tmp - if !@provisioning_path - counter = self.class.get_and_update_counter(:chef_config) - @provisioning_path = "/tmp/vagrant-chef-#{counter}" - end end - def merge(other) - super.tap do |result| - result.instance_variable_set(:@json, @json.merge(other.json)) - result.instance_variable_set(:@run_list, (@run_list + other.run_list)) - end - end - - # Just like the normal configuration "validate" method except that - # it returns an array of errors that should be merged into some - # other error accumulator. + # Like validate, but returns a list of errors to append. + # + # @return [Array] def validate_base(machine) errors = _detected_errors - if @custom_config_path - expanded = File.expand_path(@custom_config_path, machine.env.root_path) - if !File.file?(expanded) - errors << I18n.t("vagrant.config.chef.custom_config_path_missing") - end + if missing?(log_level) + errors << I18n.t("vagrant.provisioners.chef.log_level_empty") end errors end - # Adds a recipe to the run list - def add_recipe(name) - name = "recipe[#{name}]" unless name =~ /^recipe\[(.+?)\]$/ - run_list << name - end - - # Adds a role to the run list - def add_role(name) - name = "role[#{name}]" unless name =~ /^role\[(.+?)\]$/ - run_list << name + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? end end end diff --git a/plugins/provisioners/chef/config/base_runner.rb b/plugins/provisioners/chef/config/base_runner.rb new file mode 100644 index 000000000..334c9b739 --- /dev/null +++ b/plugins/provisioners/chef/config/base_runner.rb @@ -0,0 +1,137 @@ +require "vagrant/util/counter" + +require_relative "base" + +module VagrantPlugins + module Chef + module Config + # This is the config base for Chef provisioners that need a full Chef + # Runner object, like chef-solo or chef-client. For provisioners like + # chef-apply, these options are not valid + class BaseRunner < Base + attr_accessor :arguments + attr_accessor :attempts + attr_accessor :custom_config_path + attr_accessor :encrypted_data_bag_secret_key_path + attr_accessor :environment + attr_accessor :formatter + attr_accessor :http_proxy + attr_accessor :http_proxy_user + attr_accessor :http_proxy_pass + attr_accessor :https_proxy + attr_accessor :https_proxy_user + attr_accessor :https_proxy_pass + attr_accessor :json + attr_accessor :no_proxy + attr_accessor :node_name + attr_accessor :provisioning_path + attr_accessor :run_list + attr_accessor :file_cache_path + attr_accessor :file_backup_path + attr_accessor :verbose_logging + + def initialize + super + + @arguments = UNSET_VALUE + @attempts = UNSET_VALUE + @custom_config_path = UNSET_VALUE + + # /etc/chef/client.rb config options + @encrypted_data_bag_secret_key_path = UNSET_VALUE + @environment = UNSET_VALUE + @formatter = UNSET_VALUE + @http_proxy = UNSET_VALUE + @http_proxy_user = UNSET_VALUE + @http_proxy_pass = UNSET_VALUE + @https_proxy = UNSET_VALUE + @https_proxy_user = UNSET_VALUE + @https_proxy_pass = UNSET_VALUE + @no_proxy = UNSET_VALUE + @node_name = UNSET_VALUE + @provisioning_path = UNSET_VALUE + @file_cache_path = UNSET_VALUE + @file_backup_path = UNSET_VALUE + @verbose_logging = UNSET_VALUE + + # Runner options + @json = {} + @run_list = [] + end + + def encrypted_data_bag_secret=(value) + puts "DEPRECATION: Chef encrypted_data_bag_secret has no effect anymore." + puts "Remove this from your Vagrantfile since it'll be removed in the next" + puts "Vagrant version." + end + + def finalize! + super + + @arguments = nil if @arguments == UNSET_VALUE + @attempts = 1 if @attempts == UNSET_VALUE + @custom_config_path = nil if @custom_config_path == UNSET_VALUE + @environment = nil if @environment == UNSET_VALUE + @formatter = nil if @formatter == UNSET_VALUE + @http_proxy = nil if @http_proxy == UNSET_VALUE + @http_proxy_user = nil if @http_proxy_user == UNSET_VALUE + @http_proxy_pass = nil if @http_proxy_pass == UNSET_VALUE + @https_proxy = nil if @https_proxy == UNSET_VALUE + @https_proxy_user = nil if @https_proxy_user == UNSET_VALUE + @https_proxy_pass = nil if @https_proxy_pass == UNSET_VALUE + @no_proxy = nil if @no_proxy == UNSET_VALUE + @node_name = nil if @node_name == UNSET_VALUE + @provisioning_path = nil if @provisioning_path == UNSET_VALUE + @file_backup_path = "/var/chef/backup" if @file_backup_path == UNSET_VALUE + @file_cache_path = "/var/chef/cache" if @file_cache_path == UNSET_VALUE + @verbose_logging = false if @verbose_logging == UNSET_VALUE + + if @encrypted_data_bag_secret_key_path == UNSET_VALUE + @encrypted_data_bag_secret_key_path = nil + end + + # Set the default provisioning path to be a unique path in /tmp + if !@provisioning_path + counter = self.class.get_and_update_counter(:chef_config) + @provisioning_path = "/tmp/vagrant-chef-#{counter}" + end + end + + def merge(other) + super.tap do |result| + result.instance_variable_set(:@json, @json.merge(other.json)) + result.instance_variable_set(:@run_list, (@run_list + other.run_list)) + end + end + + # Just like the normal configuration "validate" method except that + # it returns an array of errors that should be merged into some + # other error accumulator. + def validate_base(machine) + errors = super + + if @custom_config_path + expanded = File.expand_path(@custom_config_path, machine.env.root_path) + if !File.file?(expanded) + errors << I18n.t("vagrant.config.chef.custom_config_path_missing") + end + end + + errors + end + + # Adds a recipe to the run list + def add_recipe(name) + name = "recipe[#{name}]" unless name =~ /^recipe\[(.+?)\]$/ + run_list << name + end + + # Adds a role to the run list + def add_role(name) + name = "role[#{name}]" unless name =~ /^role\[(.+?)\]$/ + run_list << name + end + end + end + end +end diff --git a/plugins/provisioners/chef/config/chef_apply.rb b/plugins/provisioners/chef/config/chef_apply.rb index abcd8ea6e..9f313b790 100644 --- a/plugins/provisioners/chef/config/chef_apply.rb +++ b/plugins/provisioners/chef/config/chef_apply.rb @@ -1,9 +1,9 @@ +require_relative "base" + module VagrantPlugins module Chef module Config - class ChefApply < Vagrant.plugin("2", :config) - extend Vagrant::Util::Counter - + class ChefApply < Base # The raw recipe text (as a string) to execute via chef-apply. # @return [String] attr_accessor :recipe @@ -13,25 +13,17 @@ module VagrantPlugins # @return [String] attr_accessor :upload_path - # The Chef log level. - # @return [String] - attr_accessor :log_level - def initialize - @recipe = UNSET_VALUE + super - @log_level = UNSET_VALUE + @recipe = UNSET_VALUE @upload_path = UNSET_VALUE end def finalize! - @recipe = nil if @recipe == UNSET_VALUE + super - if @log_level == UNSET_VALUE - @log_level = :info - else - @log_level = @log_level.to_sym - end + @recipe = nil if @recipe == UNSET_VALUE if @upload_path == UNSET_VALUE counter = self.class.get_and_update_counter(:chef_apply) @@ -40,28 +32,18 @@ module VagrantPlugins end def validate(machine) - errors = _detected_errors + errors = validate_base(machine) - if missing(recipe) + if missing?(recipe) errors << I18n.t("vagrant.provisioners.chef.recipe_empty") end - if missing(log_level) - errors << I18n.t("vagrant.provisioners.chef.log_level_empty") - end - - if missing(upload_path) + if missing?(upload_path) errors << I18n.t("vagrant.provisioners.chef.upload_path_empty") end { "chef apply provisioner" => errors } end - - # Determine if the given string is "missing" (blank) - # @return [true, false] - def missing(obj) - obj.to_s.strip.empty? - end end end end diff --git a/plugins/provisioners/chef/config/chef_client.rb b/plugins/provisioners/chef/config/chef_client.rb index 3f9ce3fe8..7d4f4d27a 100644 --- a/plugins/provisioners/chef/config/chef_client.rb +++ b/plugins/provisioners/chef/config/chef_client.rb @@ -1,16 +1,33 @@ require "vagrant/util/which" -require_relative "base" +require_relative "base_runner" module VagrantPlugins module Chef module Config - class ChefClient < Base + class ChefClient < BaseRunner + # The URL endpoint to the Chef Server. + # @return [String] attr_accessor :chef_server_url + + # The path on disk to the Chef client key, + # @return [String] attr_accessor :client_key_path + + # Delete the client key when the VM is destroyed. Default is false. + # @return [true, false] attr_accessor :delete_client + + # Delete the node when the VM is destroyed. Default is false. + # @return [true, false] attr_accessor :delete_node + + # The path to the validation key on disk. + # @return [String] attr_accessor :validation_key_path + + # The name of the validation client. + # @return [String] attr_accessor :validation_client_name def initialize @@ -36,8 +53,7 @@ module VagrantPlugins end def validate(machine) - errors = _detected_errors - errors.concat(validate_base(machine)) + errors = validate_base(machine) if chef_server_url.to_s.strip.empty? errors << I18n.t("vagrant.config.chef.server_url_empty") diff --git a/plugins/provisioners/chef/config/chef_solo.rb b/plugins/provisioners/chef/config/chef_solo.rb index 524596962..ee4a2ac2e 100644 --- a/plugins/provisioners/chef/config/chef_solo.rb +++ b/plugins/provisioners/chef/config/chef_solo.rb @@ -1,25 +1,60 @@ -require_relative "base" +require_relative "base_runner" module VagrantPlugins module Chef module Config - class ChefSolo < Base + class ChefSolo < BaseRunner + # The path on disk where Chef cookbooks are stored. + # Default is "cookbooks". + # @return [String] attr_accessor :cookbooks_path + + # The path where data bags are stored on disk. + # @return [String] attr_accessor :data_bags_path + + # The path where environments are stored on disk. + # @return [String] attr_accessor :environments_path + + # A URL download a remote recipe from. Note: you should use chef-apply + # instead. + # + # @deprecated + # + # @return [String] attr_accessor :recipe_url + + # The path where roles are stored on disk. + # @return [String] attr_accessor :roles_path + + # The type of synced folders to use. + # @return [String] attr_accessor :synced_folder_type def initialize super - @cookbooks_path = UNSET_VALUE - @data_bags_path = UNSET_VALUE - @environments_path = UNSET_VALUE - @recipe_url = UNSET_VALUE - @roles_path = UNSET_VALUE - @synced_folder_type = UNSET_VALUE + @cookbooks_path = UNSET_VALUE + @data_bags_path = UNSET_VALUE + @environments_path = UNSET_VALUE + @recipe_url = UNSET_VALUE + @roles_path = UNSET_VALUE + @synced_folder_type = UNSET_VALUE + end + + # @deprecated This is deprecated in Chef and will be removed in Chef 12. + def recipe_url=(value) + puts "DEPRECATION: The 'recipe_url' setting for the Chef Solo" + puts "provisioner is deprecated. This value will be removed in" + puts "Chef 12. It is recommended you use the Chef Apply provisioner" + puts "instead. The 'recipe_url' setting will be removed in the next" + puts "version of Vagrant." + + if value + @recipe_url = value + end end def nfs=(value) @@ -63,8 +98,7 @@ module VagrantPlugins end def validate(machine) - errors = _detected_errors - errors.concat(validate_base(machine)) + errors = validate_base(machine) if [cookbooks_path].flatten.compact.empty? errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") 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/commands/init/Vagrantfile.erb b/templates/commands/init/Vagrantfile.erb index 139797dfa..710e61664 100644 --- a/templates/commands/init/Vagrantfile.erb +++ b/templates/commands/init/Vagrantfile.erb @@ -89,14 +89,42 @@ Vagrant.configure(2) do |config| # puppet.manifest_file = "default.pp" # end - # Enable provisioning with chef solo, specifying a cookbooks path, roles + # Enable provisioning with Chef Solo, specifying a cookbooks path, roles # path, and data_bags path (all relative to this Vagrantfile), and adding # some recipes and/or roles. # # config.vm.provision "chef_solo" do |chef| - # chef.cookbooks_path = "../my-recipes/cookbooks" - # chef.roles_path = "../my-recipes/roles" - # chef.data_bags_path = "../my-recipes/data_bags" + # chef.cookbooks_path = "~/chef/cookbooks" + # chef.roles_path = "~/chef/roles" + # chef.data_bags_path = "~/chef/data_bags" + # + # chef.add_recipe "mysql" + # chef.add_role "web" + # + # chef.json = { mysql_password: "foo" } + # end + # + # Chef Solo will automatically install the latest version of Chef for you. + # This can be configured in the provisioner block: + # + # config.vm.provision "chef_solo" do |chef| + # chef.version = "11.16.4" + # end + # + # Alternative you can disable the installation of Chef entirely: + # + # config.vm.provision "chef_solo" do |chef| + # chef.install = false + # end + + # Enable provisioning with Chef Zero. The Chef Zero provisioner accepts the + # exact same parameter as the Chef Solo provisioner: + # + # config.vm.provision "chef_zero" do |chef| + # chef.cookbooks_path = "~/chef/cookbooks" + # chef.roles_path = "~/chef/roles" + # chef.data_bags_path = "~/chef/data_bags" + # # chef.add_recipe "mysql" # chef.add_role "web" # @@ -104,10 +132,10 @@ Vagrant.configure(2) do |config| # chef.json = { mysql_password: "foo" } # end - # Enable provisioning with chef server, specifying the chef server URL, + # Enable provisioning with Chef Server, specifying the chef server URL, # and the path to the validation key (relative to this Vagrantfile). # - # The Opscode Platform uses HTTPS. Substitute your organization for + # The Hosted Chef platform uses HTTPS. Substitute your organization for # ORGNAME in the URL and validation key. # # If you have your own Chef Server, use the appropriate URL, which may be @@ -115,15 +143,44 @@ Vagrant.configure(2) do |config| # validation key to validation.pem. # # config.vm.provision "chef_client" do |chef| - # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" + # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" # chef.validation_key_path = "ORGNAME-validator.pem" # end # - # If you're using the Opscode platform, your validator client is + # If you're using the Hosted Chef platform, your validator client is # ORGNAME-validator, replacing ORGNAME with your organization name. # # If you have your own Chef Server, the default validation client name is # chef-validator, unless you changed the configuration. # # chef.validation_client_name = "ORGNAME-validator" + # + # Chef Client will automatically install the latest version of Chef for you. + # This can be configured in the provisioner block: + # + # config.vm.provision "chef_client" do |chef| + # chef.version = "11.16.4" + # end + # + # Alternative you can disable the installation of Chef entirely: + # + # config.vm.provision "chef_client" do |chef| + # chef.install = false + # end + + # Enable provisioning with Chef Apply, specifying an inline recipe to execute + # on the target system. + # + # config.vm.provision "chef_apply" do |chef| + # chef.recipe = <<-RECIPE + # package "curl" + # RECIPE + # end + # + # Chef Apply will automatically install the latest version of Chef for you. + # This can be configured in the provisioner block: + # + # config.vm.provision "chef_apply" do |chef| + # chef.version = "11.16.4" + # end end 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_runner_test.rb b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb new file mode 100644 index 000000000..591ec743e --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb @@ -0,0 +1,255 @@ +require_relative "../../../../base" + +require Vagrant.source_root.join("plugins/provisioners/chef/config/base_runner") + +describe VagrantPlugins::Chef::Config::BaseRunner do + include_context "unit" + + subject { described_class.new } + + let(:machine) { double("machine") } + + describe "#arguments" do + it "defaults to nil" do + subject.finalize! + expect(subject.arguments).to be(nil) + end + end + + describe "#attempts" do + it "defaults to 1" do + subject.finalize! + expect(subject.attempts).to eq(1) + end + end + + describe "#custom_config_path" do + it "defaults to nil" do + subject.finalize! + expect(subject.custom_config_path).to be(nil) + end + end + + describe "#environment" do + it "defaults to nil" do + subject.finalize! + expect(subject.environment).to be(nil) + end + end + + describe "#encrypted_data_bag_secret_key_path" do + it "defaults to nil" do + subject.finalize! + expect(subject.encrypted_data_bag_secret_key_path).to be(nil) + end + end + + describe "#formatter" do + it "defaults to nil" do + subject.finalize! + expect(subject.formatter).to be(nil) + end + end + + describe "#http_proxy" do + it "defaults to nil" do + subject.finalize! + expect(subject.http_proxy).to be(nil) + end + end + + describe "#http_proxy_user" do + it "defaults to nil" do + subject.finalize! + expect(subject.http_proxy_user).to be(nil) + end + end + + describe "#http_proxy_pass" do + it "defaults to nil" do + subject.finalize! + expect(subject.http_proxy_pass).to be(nil) + end + end + + describe "#https_proxy" do + it "defaults to nil" do + subject.finalize! + expect(subject.https_proxy).to be(nil) + end + end + + describe "#https_proxy_user" do + it "defaults to nil" do + subject.finalize! + expect(subject.https_proxy_user).to be(nil) + end + end + + describe "#https_proxy_pass" do + it "defaults to nil" do + subject.finalize! + expect(subject.https_proxy_pass).to be(nil) + end + end + + describe "#log_level" do + it "defaults to :info" do + subject.finalize! + expect(subject.log_level).to be(:info) + end + + it "is converted to a symbol" do + subject.log_level = "foo" + subject.finalize! + expect(subject.log_level).to eq(:foo) + end + end + + describe "#no_proxy" do + it "defaults to nil" do + subject.finalize! + expect(subject.no_proxy).to be(nil) + end + end + + describe "#node_name" do + it "defaults to nil" do + subject.finalize! + expect(subject.node_name).to be(nil) + end + end + + describe "#provisioning_path" do + it "defaults to a tmp_path" do + subject.finalize! + expect(subject.provisioning_path).to match(%r{/tmp/vagrant-chef-\d+}) + end + end + + describe "#file_backup_path" do + it "defaults to /var/chef/backup" do + subject.finalize! + expect(subject.file_backup_path).to eq("/var/chef/backup") + end + end + + describe "#file_cache_path" do + it "defaults to /var/chef/cache" do + subject.finalize! + expect(subject.file_cache_path).to eq("/var/chef/cache") + end + end + + describe "#verbose_logging" do + it "defaults to false" do + subject.finalize! + expect(subject.verbose_logging).to be(false) + end + end + + describe "#run_list" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.run_list).to be_a(Array) + expect(subject.run_list).to be_empty + end + end + + describe "#json" do + it "defaults to an empty hash" do + subject.finalize! + expect(subject.json).to be_a(Hash) + expect(subject.json).to be_empty + end + end + + describe "#add_recipe" do + context "when the prefix is given" do + it "adds the value to the run_list" do + subject.add_recipe("recipe[foo::bar]") + expect(subject.run_list).to eq %w(recipe[foo::bar]) + end + end + + context "when the prefix is not given" do + it "adds the prefixed value to the run_list" do + subject.add_recipe("foo::bar") + expect(subject.run_list).to eq %w(recipe[foo::bar]) + end + end + end + + describe "#add_role" do + context "when the prefix is given" do + it "adds the value to the run_list" do + subject.add_role("role[foo]") + expect(subject.run_list).to eq %w(role[foo]) + end + end + + context "when the prefix is not given" do + it "adds the prefixed value to the run_list" do + subject.add_role("foo") + expect(subject.run_list).to eq %w(role[foo]) + end + end + end + + describe "#validate_base" do + context "when #custom_config_path does not exist" do + let(:path) { "/path/to/file" } + + before do + allow(File).to receive(:file?) + .with(path) + .and_return(false) + + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + end + + it "returns an error" do + subject.custom_config_path = path + subject.finalize! + + expect(subject.validate_base(machine)) + .to eq ['Path specified for "custom_config_path" does not exist.'] + end + end + end + + describe "#merge" do + it "merges the json hash" do + a = described_class.new.tap do |i| + i.json = { "foo" => "bar" } + end + b = described_class.new.tap do |i| + i.json = { "zip" => "zap" } + end + + result = a.merge(b) + expect(result.json).to eq( + "foo" => "bar", + "zip" => "zap", + ) + end + + it "appends the run_list array" do + a = described_class.new.tap do |i| + i.run_list = ["recipe[foo::bar]"] + end + b = described_class.new.tap do |i| + i.run_list = ["recipe[zip::zap]"] + end + + result = a.merge(b) + expect(result.run_list).to eq %w( + recipe[foo::bar] + recipe[zip::zap] + ) + end + end +end diff --git a/test/unit/plugins/provisioners/chef/config/base_test.rb b/test/unit/plugins/provisioners/chef/config/base_test.rb index 062c48b08..2355f28cd 100644 --- a/test/unit/plugins/provisioners/chef/config/base_test.rb +++ b/test/unit/plugins/provisioners/chef/config/base_test.rb @@ -9,20 +9,6 @@ describe VagrantPlugins::Chef::Config::Base do let(:machine) { double("machine") } - describe "#arguments" do - it "defaults to nil" do - subject.finalize! - expect(subject.arguments).to be(nil) - end - end - - describe "#attempts" do - it "defaults to 1" do - subject.finalize! - expect(subject.attempts).to eq(1) - end - end - describe "#binary_path" do it "defaults to nil" do subject.finalize! @@ -37,66 +23,16 @@ describe VagrantPlugins::Chef::Config::Base do end end - describe "#custom_config_path" do - it "defaults to nil" do + describe "#install" do + it "defaults to true" do subject.finalize! - expect(subject.custom_config_path).to be(nil) + expect(subject.install).to be(true) end - end - describe "#environment" do - it "defaults to nil" do + it "is converted to a symbol" do + subject.install = "force" subject.finalize! - expect(subject.environment).to be(nil) - end - end - - describe "#formatter" do - it "defaults to nil" do - subject.finalize! - expect(subject.formatter).to be(nil) - end - end - - describe "#http_proxy" do - it "defaults to nil" do - subject.finalize! - expect(subject.http_proxy).to be(nil) - end - end - - describe "#http_proxy_user" do - it "defaults to nil" do - subject.finalize! - expect(subject.http_proxy_user).to be(nil) - end - end - - describe "#http_proxy_pass" do - it "defaults to nil" do - subject.finalize! - expect(subject.http_proxy_pass).to be(nil) - end - end - - describe "#https_proxy" do - it "defaults to nil" do - subject.finalize! - expect(subject.https_proxy).to be(nil) - end - end - - describe "#https_proxy_user" do - it "defaults to nil" do - subject.finalize! - expect(subject.https_proxy_user).to be(nil) - end - end - - describe "#https_proxy_pass" do - it "defaults to nil" do - subject.finalize! - expect(subject.https_proxy_pass).to be(nil) + expect(subject.install).to eq(:force) end end @@ -113,150 +49,23 @@ describe VagrantPlugins::Chef::Config::Base do end end - describe "#no_proxy" do - it "defaults to nil" do + describe "#prerelease" do + it "defaults to true" do subject.finalize! - expect(subject.no_proxy).to be(nil) + expect(subject.prerelease).to be(false) end end - describe "#node_name" do - it "defaults to nil" do + describe "#version" do + it "defaults to :latest" do subject.finalize! - expect(subject.node_name).to be(nil) + expect(subject.version).to eq(:latest) end - end - describe "#provisioning_path" do - it "defaults to a tmp_path" do + it "converts the string 'latest' to a symbol" do + subject.version = "latest" subject.finalize! - expect(subject.provisioning_path).to match(%r{/tmp/vagrant-chef-\d+}) - end - end - - describe "#file_backup_path" do - it "defaults to /var/chef/backup" do - subject.finalize! - expect(subject.file_backup_path).to eq("/var/chef/backup") - end - end - - describe "#file_cache_path" do - it "defaults to /var/chef/cache" do - subject.finalize! - expect(subject.file_cache_path).to eq("/var/chef/cache") - end - end - - describe "#verbose_logging" do - it "defaults to false" do - subject.finalize! - expect(subject.verbose_logging).to be(false) - end - end - - describe "#run_list" do - it "defaults to an empty array" do - subject.finalize! - expect(subject.run_list).to be_a(Array) - expect(subject.run_list).to be_empty - end - end - - describe "#json" do - it "defaults to an empty hash" do - subject.finalize! - expect(subject.json).to be_a(Hash) - expect(subject.json).to be_empty - end - end - - describe "#add_recipe" do - context "when the prefix is given" do - it "adds the value to the run_list" do - subject.add_recipe("recipe[foo::bar]") - expect(subject.run_list).to eq %w(recipe[foo::bar]) - end - end - - context "when the prefix is not given" do - it "adds the prefixed value to the run_list" do - subject.add_recipe("foo::bar") - expect(subject.run_list).to eq %w(recipe[foo::bar]) - end - end - end - - describe "#add_role" do - context "when the prefix is given" do - it "adds the value to the run_list" do - subject.add_role("role[foo]") - expect(subject.run_list).to eq %w(role[foo]) - end - end - - context "when the prefix is not given" do - it "adds the prefixed value to the run_list" do - subject.add_role("foo") - expect(subject.run_list).to eq %w(role[foo]) - end - end - end - - describe "#validate_base" do - context "when #custom_config_path does not exist" do - let(:path) { "/path/to/file" } - - before do - allow(File).to receive(:file?) - .with(path) - .and_return(false) - - allow(machine).to receive(:env) - .and_return(double("env", - root_path: "", - )) - end - - it "returns an error" do - subject.custom_config_path = path - subject.finalize! - - expect(subject.validate_base(machine)) - .to eq ['Path specified for "custom_config_path" does not exist.'] - end - end - end - - describe "#merge" do - it "merges the json hash" do - a = described_class.new.tap do |i| - i.json = { "foo" => "bar" } - end - b = described_class.new.tap do |i| - i.json = { "zip" => "zap" } - end - - result = a.merge(b) - expect(result.json).to eq( - "foo" => "bar", - "zip" => "zap", - ) - end - - it "appends the run_list array" do - a = described_class.new.tap do |i| - i.run_list = ["recipe[foo::bar]"] - end - b = described_class.new.tap do |i| - i.run_list = ["recipe[zip::zap]"] - end - - result = a.merge(b) - expect(result.run_list).to eq %w( - recipe[foo::bar] - recipe[zip::zap] - ) + expect(subject.version).to eq(:latest) end end end diff --git a/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb index 4ea316443..242eda7ad 100644 --- a/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb +++ b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb @@ -20,19 +20,6 @@ describe VagrantPlugins::Chef::Config::ChefApply do end end - describe "#log_level" do - it "defaults to :info" do - subject.finalize! - expect(subject.log_level).to be(:info) - end - - it "is converted to a symbol" do - subject.log_level = "foo" - subject.finalize! - expect(subject.log_level).to eq(:foo) - end - end - describe "#upload_path" do it "defaults to /tmp/vagrant-chef-apply.rb" 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