diff --git a/plugins/provisioners/chef/config/base_runner.rb b/plugins/provisioners/chef/config/base_runner.rb index 12ac0aabd..df0329589 100644 --- a/plugins/provisioners/chef/config/base_runner.rb +++ b/plugins/provisioners/chef/config/base_runner.rb @@ -29,6 +29,7 @@ module VagrantPlugins attr_accessor :file_cache_path attr_accessor :file_backup_path attr_accessor :verbose_logging + attr_accessor :enable_reporting def initialize super @@ -53,6 +54,7 @@ module VagrantPlugins @file_cache_path = UNSET_VALUE @file_backup_path = UNSET_VALUE @verbose_logging = UNSET_VALUE + @enable_reporting = UNSET_VALUE # Runner options @json = {} @@ -85,6 +87,7 @@ module VagrantPlugins @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 + @enable_reporting = true if @enable_reporting == UNSET_VALUE if @encrypted_data_bag_secret_key_path == UNSET_VALUE @encrypted_data_bag_secret_key_path = nil diff --git a/plugins/provisioners/chef/config/chef_zero.rb b/plugins/provisioners/chef/config/chef_zero.rb index 993a3fab9..d28de3dc9 100644 --- a/plugins/provisioners/chef/config/chef_zero.rb +++ b/plugins/provisioners/chef/config/chef_zero.rb @@ -3,26 +3,104 @@ require_relative "chef_solo" module VagrantPlugins module Chef module Config - class ChefZero < ChefSolo - attr_accessor :nodes_path + class ChefZero < 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 + + # 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 - @nodes_path = UNSET_VALUE + @cookbooks_path = UNSET_VALUE + @data_bags_path = UNSET_VALUE + @environments_path = UNSET_VALUE + @roles_path = UNSET_VALUE + @synced_folder_type = UNSET_VALUE end def finalize! super - @nodes_path = [] if @nodes_path == UNSET_VALUE + @synced_folder_type = nil if @synced_folder_type == UNSET_VALUE + + if @cookbooks_path == UNSET_VALUE + @cookbooks_path = [] + @cookbooks_path << [:host, "cookbooks"] if !@recipe_url + @cookbooks_path << [:vm, "cookbooks"] + end + + @data_bags_path = [] if @data_bags_path == UNSET_VALUE + @roles_path = [] if @roles_path == UNSET_VALUE + @environments_path = [] if @environments_path == UNSET_VALUE + @environments_path = [@environments_path].flatten # Make sure the path is an array. - @nodes_path = prepare_folders_config(@nodes_path) + @cookbooks_path = prepare_folders_config(@cookbooks_path) + @data_bags_path = prepare_folders_config(@data_bags_path) + @roles_path = prepare_folders_config(@roles_path) + @environments_path = prepare_folders_config(@environments_path) + end def validate(machine) - { "chef zero provisioner" => super["chef solo provisioner"] } + errors = validate_base(machine) + + if [cookbooks_path].flatten.compact.empty? + errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") + end + + if environment && environments_path.empty? + errors << I18n.t("vagrant.config.chef.environment_path_required") + end + + environments_path.each do |type, raw_path| + next if type != :host + + path = Pathname.new(raw_path).expand_path(machine.env.root_path) + if !path.directory? + errors << I18n.t("vagrant.config.chef.environment_path_missing", + path: raw_path.to_s + ) + end + end + + { "chef zero provisioner" => errors } + end + + protected + + # This takes any of the configurations that take a path or + # array of paths and turns it into the proper format. + # + # @return [Array] + def prepare_folders_config(config) + # Make sure the path is an array + config = [config] if !config.is_a?(Array) || config.first.is_a?(Symbol) + + return [] if config.flatten.compact.empty? + + # Make sure all the paths are in the proper format + config.map do |path| + path = [:host, path] if !path.is_a?(Array) + path + end end end end diff --git a/plugins/provisioners/chef/provisioner/base.rb b/plugins/provisioners/chef/provisioner/base.rb index f623bb498..8d8ee7d02 100644 --- a/plugins/provisioners/chef/provisioner/base.rb +++ b/plugins/provisioners/chef/provisioner/base.rb @@ -93,6 +93,7 @@ module VagrantPlugins log_level: @config.log_level.to_sym, node_name: @config.node_name, verbose_logging: @config.verbose_logging, + enable_reporting: @config.enable_reporting, http_proxy: @config.http_proxy, http_proxy_user: @config.http_proxy_user, http_proxy_pass: @config.http_proxy_pass, diff --git a/plugins/provisioners/chef/provisioner/chef_zero.rb b/plugins/provisioners/chef/provisioner/chef_zero.rb index f634bc9a7..1a63be73f 100644 --- a/plugins/provisioners/chef/provisioner/chef_zero.rb +++ b/plugins/provisioners/chef/provisioner/chef_zero.rb @@ -1,36 +1,207 @@ +require "digest/md5" +require "securerandom" +require "set" + require "log4r" -require_relative "chef_solo" +require "vagrant/util/counter" + +require_relative "base" module VagrantPlugins module Chef module Provisioner # This class implements provisioning via chef-zero. - class ChefZero < ChefSolo - attr_reader :node_folders + class ChefZero < Base + extend Vagrant::Util::Counter + include Vagrant::Util::Counter + include Vagrant::Action::Builtin::MixinSyncedFolders + + attr_reader :environments_folders + attr_reader :cookbook_folders + attr_reader :role_folders + attr_reader :data_bags_folders def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::chef_zero") + @shared_folders = [] end def configure(root_config) - super + @cookbook_folders = expanded_folders(@config.cookbooks_path, "cookbooks") + @role_folders = expanded_folders(@config.roles_path, "roles") + @data_bags_folders = expanded_folders(@config.data_bags_path, "data_bags") + @environments_folders = expanded_folders(@config.environments_path, "environments") - @node_folders = expanded_folders(@config.nodes_path, "nodes") - - share_folders(root_config, "csn", @node_folders) + existing = synced_folders(@machine, cached: true) + share_folders(root_config, "csc", @cookbook_folders, existing) + share_folders(root_config, "csr", @role_folders, existing) + share_folders(root_config, "csdb", @data_bags_folders, existing) + share_folders(root_config, "cse", @environments_folders, existing) end - def provision - super(:zero) + def provision(mode = :client) + install_chef + # Verify that the proper shared folders exist. + check = [] + @shared_folders.each do |type, local_path, remote_path| + # We only care about checking folders that have a local path, meaning + # they were shared from the local machine, rather than assumed to + # exist on the VM. + check << remote_path if local_path + end + + chown_provisioning_folder + verify_shared_folders(check) + verify_binary(chef_binary_path("chef-client")) + upload_encrypted_data_bag_secret + setup_json + setup_zero_config + run_chef(mode) + delete_encrypted_data_bag_secret end - def solo_config - super.merge( + # Converts paths to a list of properly expanded paths with types. + def expanded_folders(paths, appended_folder=nil) + # Convert the path to an array if it is a string or just a single + # path element which contains the folder location (:host or :vm) + paths = [paths] if paths.is_a?(String) || paths.first.is_a?(Symbol) + + results = [] + paths.each do |type, path| + # Create the local/remote path based on whether this is a host + # or VM path. + local_path = nil + remote_path = nil + if type == :host + # Get the expanded path that the host path points to + local_path = File.expand_path(path, @machine.env.root_path) + + if File.exist?(local_path) + # Path exists on the host, setup the remote path. We use + # the MD5 of the local path so that it is predictable. + key = Digest::MD5.hexdigest(local_path) + remote_path = "#{@config.provisioning_path}/#{key}" + else + @machine.ui.warn(I18n.t("vagrant.provisioners.chef.cookbook_folder_not_found_warning", + path: local_path.to_s)) + next + end + else + # Path already exists on the virtual machine. Expand it + # relative to where we're provisioning. + remote_path = File.expand_path(path, @config.provisioning_path) + + # Remove drive letter if running on a windows host. This is a bit + # of a hack but is the most portable way I can think of at the moment + # to achieve this. Otherwise, Vagrant attempts to share at some crazy + # path like /home/vagrant/c:/foo/bar + remote_path = remote_path.gsub(/^[a-zA-Z]:/, "") + end + + # If we have specified a folder name to append then append it + remote_path += "/#{appended_folder}" if appended_folder + + # Append the result + results << [type, local_path, remote_path] + end + + results + end + + # Shares the given folders with the given prefix. The folders should + # be of the structure resulting from the `expanded_folders` function. + def share_folders(root_config, prefix, folders, existing=nil) + existing_set = Set.new + (existing || []).each do |_, fs| + fs.each do |id, data| + existing_set.add(data[:guestpath]) + end + end + + folders.each do |type, local_path, remote_path| + next if type != :host + + # If this folder already exists, then we don't share it, it means + # it was already put down on disk. + if existing_set.include?(remote_path) + @logger.debug("Not sharing #{local_path}, exists as #{remote_path}") + next + end + + opts = {} + opts[:id] = "v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}" + opts[:type] = @config.synced_folder_type if @config.synced_folder_type + + root_config.vm.synced_folder(local_path, remote_path, opts) + end + + @shared_folders += folders + end + + def setup_zero_config + setup_config("provisioners/chef_zero/zero", "client.rb", { local_mode: true, - node_path: guest_paths(@node_folders).first - ) + enable_reporting: false, + cookbooks_path: guest_paths(@cookbook_folders), + roles_path: guest_paths(@role_folders), + data_bags_path: guest_paths(@data_bags_folders).first, + environments_path: guest_paths(@environments_folders).first, + }) + end + + def run_chef(mode) + if @config.run_list && @config.run_list.empty? + @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) + end + + if @machine.guest.capability?(:wait_for_reboot) + @machine.guest.capability(:wait_for_reboot) + end + + command = build_command(:client) + + @config.attempts.times do |attempt| + if attempt == 0 + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}") + else + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}_again") + end + + opts = { error_check: false, elevated: true } + exit_status = @machine.communicate.sudo(command, opts) do |type, data| + # Output the data with the proper color based on the stream. + color = type == :stdout ? :green : :red + + data = data.chomp + next if data.empty? + + @machine.ui.info(data, color: color) + end + + # There is no need to run Chef again if it converges + return if exit_status == 0 + end + + # If we reached this point then Chef never converged! Error. + raise ChefError, :no_convergence + end + + def verify_shared_folders(folders) + folders.each do |folder| + @logger.debug("Checking for shared folder: #{folder}") + if !@machine.communicate.test("test -d #{folder}", sudo: true) + raise ChefError, :missing_shared_folders + end + end + end + + protected + + # Extracts only the remote paths from a list of folders + def guest_paths(folders) + folders.map { |parts| parts[2] } end end end diff --git a/templates/provisioners/chef_zero/zero.erb b/templates/provisioners/chef_zero/zero.erb new file mode 100644 index 000000000..29de30d23 --- /dev/null +++ b/templates/provisioners/chef_zero/zero.erb @@ -0,0 +1,44 @@ +<% if node_name %> +node_name "<%= node_name %>" +<% end %> +file_cache_path "<%= file_cache_path %>" +file_backup_path "<%= file_backup_path %>" +cookbook_path <%= cookbooks_path.inspect %> +<% if roles_path %> +role_path <%= roles_path.size == 1 ? roles_path.first.inspect : roles_path.inspect %> +<% end %> +log_level <%= log_level.inspect %> +verbose_logging <%= verbose_logging.inspect %> +<% if !enable_reporting %> +enable_reporting <%= enable_reporting.inspect %> +<% end %> + +encrypted_data_bag_secret <%= encrypted_data_bag_secret.inspect %> + +<% if data_bags_path -%> +data_bag_path <%= data_bags_path.inspect %> +<% end %> + +<% if environments_path %> +environment_path <%= environments_path.inspect %> +<% end -%> + +<% if environment %> +environment "<%= environment %>" +<% end -%> + +<% if local_mode -%> +chef_zero.enabled true +local_mode true +<% end -%> +<% if node_path -%> +node_path <%= node_path.inspect %> +<% end -%> + +<% if formatter %> +add_formatter "<%= formatter %>" +<% end %> + +<% if custom_configuration -%> +Chef::Config.from_file "<%= custom_configuration %>" +<% end -%> diff --git a/test/unit/plugins/provisioners/chef/config/base_runner_test.rb b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb index 330c31616..3ef7e2fab 100644 --- a/test/unit/plugins/provisioners/chef/config/base_runner_test.rb +++ b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb @@ -148,6 +148,13 @@ describe VagrantPlugins::Chef::Config::BaseRunner do end end + describe "#enable_reporting" do + it "defaults to true" do + subject.finalize! + expect(subject.enable_reporting).to be(true) + end + end + describe "#run_list" do it "defaults to an empty array" do subject.finalize! diff --git a/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb index 6149a3bde..2f9cd82aa 100644 --- a/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb +++ b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb @@ -9,11 +9,132 @@ describe VagrantPlugins::Chef::Config::ChefZero do let(:machine) { double("machine") } - describe "#nodes_path" do - it "defaults to an array" do + describe "#cookbooks_path" do + it "defaults to something" do subject.finalize! - expect(subject.nodes_path).to be_a(Array) - expect(subject.nodes_path).to be_empty + expect(subject.cookbooks_path).to eq([ + [:host, "cookbooks"], + [:vm, "cookbooks"], + ]) + end + end + + describe "#data_bags_path" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.data_bags_path).to be_a(Array) + expect(subject.data_bags_path).to be_empty + end + end + + describe "#environments_path" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.environments_path).to be_a(Array) + expect(subject.environments_path).to be_empty + end + + it "merges deeply nested paths" do + subject.environments_path = ["/foo", "/bar", ["/zip"]] + subject.finalize! + expect(subject.environments_path) + .to eq([:host, :host, :host].zip %w(/foo /bar /zip)) + end + end + + describe "#roles_path" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.roles_path).to be_a(Array) + expect(subject.roles_path).to be_empty + end + end + + describe "#synced_folder_type" do + it "defaults to nil" do + subject.finalize! + expect(subject.synced_folder_type).to be(nil) + end + end + + describe "#validate" do + before do + allow(machine).to receive(:env) + .and_return(double("env", + root_path: "", + )) + + subject.cookbooks_path = ["/cookbooks", "/more/cookbooks"] + end + + let(:result) { subject.validate(machine) } + let(:errors) { result["chef zero provisioner"] } + + context "when the cookbooks_path is nil" do + it "returns an error" do + subject.cookbooks_path = nil + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] + end + end + + context "when the cookbooks_path is an empty array" do + it "returns an error" do + subject.cookbooks_path = [] + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] + end + end + + context "when the cookbooks_path is an array with nil" do + it "returns an error" do + subject.cookbooks_path = [nil, nil] + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] + end + end + + context "when environments is given" do + before do + subject.environment = "production" + end + + context "when the environments_path is nil" do + it "returns an error" do + subject.environments_path = nil + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] + end + end + + context "when the environments_path is an empty array" do + it "returns an error" do + subject.environments_path = [] + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] + end + end + + context "when the environments_path is an array with nil" do + it "returns an error" do + subject.environments_path = [nil, nil] + subject.finalize! + expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] + end + end + + context "when the environments_path does not exist" do + it "returns an error" do + env_path = "/path/to/environments/that/will/never/exist" + subject.environments_path = env_path + subject.finalize! + expect(errors).to eq [ + I18n.t("vagrant.config.chef.environment_path_missing", + path: env_path + ) + ] + end + end end end end diff --git a/website/docs/source/v2/provisioning/chef_common.html.md b/website/docs/source/v2/provisioning/chef_common.html.md index d63a0d680..b5347d522 100644 --- a/website/docs/source/v2/provisioning/chef_common.html.md +++ b/website/docs/source/v2/provisioning/chef_common.html.md @@ -105,3 +105,6 @@ which include [Chef Solo](/v2/provisioning/chef_solo.html), [Chef Zero](/v2/prov * `verbose_logging` (boolean) - Whether or not to enable the Chef `verbose_logging` option. By default this is false. + +* `enable_reporting` (boolean) - Whether or not to enable the Chef + `enable_reporting` option. By default this is true. diff --git a/website/docs/source/v2/provisioning/chef_zero.html.md b/website/docs/source/v2/provisioning/chef_zero.html.md index ec50dac0a..4f35f362a 100644 --- a/website/docs/source/v2/provisioning/chef_zero.html.md +++ b/website/docs/source/v2/provisioning/chef_zero.html.md @@ -31,8 +31,29 @@ This section lists the complete set of available options for the Chef Zero provisioner. More detailed examples of how to use the provisioner are available below this section. -* `nodes_path` (string) - A path where the Chef nodes are stored. Be default, - no node path is set. +* `cookbooks_path` (string or array) - A list of paths to where cookbooks + are stored. By default this is "cookbooks", expecting a cookbooks folder + relative to the Vagrantfile location. + +* `data_bags_path` (string) - A path where data bags are stored. By default, no + data bag path is set. + +* `environments_path` (string) - A path where environment definitions are + located. By default, no environments folder is set. + +* `environment` (string) - The environment you want the Chef run to be + a part of. This requires Chef 11.6.0 or later, and that `environments_path` + is set. + +* `roles_path` (string or array) - A list of paths where roles are defined. + By default this is empty. Multiple role directories are only supported by + Chef 11.8.0 and later. + +* `synced_folder_type` (string) - The type of synced folders to use when + sharing the data required for the provisioner to work properly. By default + this will use the default synced folder type. For example, you can set this + to "nfs" to use NFS synced folders. + In addition to all the options listed above, the Chef Zero provisioner supports the [common options for all Chef provisioners](/v2/provisioning/chef_common.html). @@ -50,8 +71,8 @@ Vagrant.configure("2") do |config| config.vm.provision "chef_zero" do |chef| # Specify the local paths where Chef data is stored chef.cookbooks_path = "cookbooks" + chef.data_bags_path = "data_bags" chef.roles_path = "roles" - chef.nodes_path = "nodes" # Add a recipe chef.add_recipe "apache"