Merge pull request #5339 from YpNo/master

Chef Provisioner : Support real chef-zero/local mode
This commit is contained in:
Seth Vargo 2015-02-16 11:32:56 -05:00
commit 9bdff90d7c
9 changed files with 475 additions and 26 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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 -%>

View File

@ -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!

View File

@ -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

View File

@ -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.

View File

@ -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"