From 1cbac3167f5bde110fd996ba667dd999c229419e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Apr 2012 21:53:19 -0700 Subject: [PATCH] Move provisioners into plugins --- lib/vagrant.rb | 15 -- lib/vagrant/plugin/v1.rb | 13 + lib/vagrant/provisioners/chef.rb | 168 ------------- lib/vagrant/provisioners/chef_client.rb | 132 ---------- lib/vagrant/provisioners/chef_solo.rb | 234 ----------------- lib/vagrant/provisioners/puppet.rb | 175 ------------- lib/vagrant/provisioners/puppet_server.rb | 78 ------ plugins/chef/plugin.rb | 21 ++ plugins/chef/provisioner/base.rb | 166 ++++++++++++ plugins/chef/provisioner/chef_client.rb | 134 ++++++++++ plugins/chef/provisioner/chef_solo.rb | 236 ++++++++++++++++++ plugins/kernel/config/vm_provisioner.rb | 12 +- plugins/puppet/plugin.rb | 21 ++ plugins/puppet/provisioner/puppet.rb | 176 +++++++++++++ plugins/puppet/provisioner/puppet_server.rb | 80 ++++++ plugins/shell/plugin.rb | 17 ++ .../shell.rb => plugins/shell/provisioner.rb | 9 +- test/unit/vagrant/plugin/v1_test.rb | 28 +++ test/unit/vagrant_test.rb | 4 - 19 files changed, 908 insertions(+), 811 deletions(-) delete mode 100644 lib/vagrant/provisioners/chef.rb delete mode 100644 lib/vagrant/provisioners/chef_client.rb delete mode 100644 lib/vagrant/provisioners/chef_solo.rb delete mode 100644 lib/vagrant/provisioners/puppet.rb delete mode 100644 lib/vagrant/provisioners/puppet_server.rb create mode 100644 plugins/chef/plugin.rb create mode 100644 plugins/chef/provisioner/base.rb create mode 100644 plugins/chef/provisioner/chef_client.rb create mode 100644 plugins/chef/provisioner/chef_solo.rb create mode 100644 plugins/puppet/plugin.rb create mode 100644 plugins/puppet/provisioner/puppet.rb create mode 100644 plugins/puppet/provisioner/puppet_server.rb create mode 100644 plugins/shell/plugin.rb rename lib/vagrant/provisioners/shell.rb => plugins/shell/provisioner.rb (95%) diff --git a/lib/vagrant.rb b/lib/vagrant.rb index f36cf5b5a..e96bad3bd 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -124,14 +124,6 @@ module Vagrant def self.hosts @hosts ||= Registry.new end - - # Global registry of provisioners. - # - # This registry is used to look up the provisioners provided for - # `config.vm.provision`. - def self.provisioners - @provisioners ||= Registry.new - end end # # Default I18n to load the en locale @@ -175,10 +167,3 @@ Vagrant.hosts.register(:freebsd) { Vagrant::Hosts::FreeBSD } Vagrant.hosts.register(:gentoo) { Vagrant::Hosts::Gentoo } Vagrant.hosts.register(:linux) { Vagrant::Hosts::Linux } Vagrant.hosts.register(:windows) { Vagrant::Hosts::Windows } - -# Register the built-in provisioners -Vagrant.provisioners.register(:chef_solo) { Vagrant::Provisioners::ChefSolo } -Vagrant.provisioners.register(:chef_client) { Vagrant::Provisioners::ChefClient } -Vagrant.provisioners.register(:puppet) { Vagrant::Provisioners::Puppet } -Vagrant.provisioners.register(:puppet_server) { Vagrant::Provisioners::PuppetServer } -Vagrant.provisioners.register(:shell) { Vagrant::Provisioners::Shell } diff --git a/lib/vagrant/plugin/v1.rb b/lib/vagrant/plugin/v1.rb index cf0adf340..179425208 100644 --- a/lib/vagrant/plugin/v1.rb +++ b/lib/vagrant/plugin/v1.rb @@ -72,6 +72,19 @@ module Vagrant data[:guests] end + # Registers additional provisioners to be available. + # + # @param [String] name Name of the provisioner. + def self.provisioner(name=UNSET_VALUE, &block) + data[:provisioners] ||= Registry.new + + # Register a new provisioner class only if a name was given + data[:provisioners].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:provisioners] + end + # Registers the plugin. This makes the plugin actually work with # Vagrant. Prior to registering, the plugin is merely a skeleton. def self.register!(plugin=nil) diff --git a/lib/vagrant/provisioners/chef.rb b/lib/vagrant/provisioners/chef.rb deleted file mode 100644 index cf80ffc42..000000000 --- a/lib/vagrant/provisioners/chef.rb +++ /dev/null @@ -1,168 +0,0 @@ -require 'tempfile' - -module Vagrant - module Provisioners - # This class is a base class where the common functionality shared between - # chef-solo and chef-client provisioning are stored. This is **not an actual - # provisioner**. Instead, {ChefSolo} or {ChefServer} should be used. - class Chef < Base - include Util::Counter - - def initialize(env, config) - super - - config.provisioning_path ||= "/tmp/vagrant-chef-#{get_and_update_counter(:provisioning_path)}" - end - - def prepare - raise ChefError, :invalid_provisioner - end - - def verify_binary(binary) - # Checks for the existence of chef binary and error if it - # doesn't exist. - env[:vm].channel.sudo("which #{binary}", - :error_class => ChefError, - :error_key => :chef_not_detected, - :binary => binary) - end - - # Returns the path to the Chef binary, taking into account the - # `binary_path` configuration option. - def chef_binary_path(binary) - return binary if !config.binary_path - return File.join(config.binary_path, binary) - end - - def chown_provisioning_folder - env[:vm].channel.sudo("mkdir -p #{config.provisioning_path}") - env[:vm].channel.sudo("chown #{env[:vm].config.ssh.username} #{config.provisioning_path}") - end - - def setup_config(template, filename, template_vars) - config_file = TemplateRenderer.render(template, { - :log_level => config.log_level.to_sym, - :http_proxy => config.http_proxy, - :http_proxy_user => config.http_proxy_user, - :http_proxy_pass => config.http_proxy_pass, - :https_proxy => config.https_proxy, - :https_proxy_user => config.https_proxy_user, - :https_proxy_pass => config.https_proxy_pass, - :no_proxy => config.no_proxy - }.merge(template_vars)) - - # Create a temporary file to store the data so we - # can upload it - temp = Tempfile.new("vagrant") - temp.write(config_file) - temp.close - - remote_file = File.join(config.provisioning_path, filename) - env[:vm].channel.sudo("rm #{remote_file}", :error_check => false) - env[:vm].channel.upload(temp.path, remote_file) - end - - def setup_json - env[:ui].info I18n.t("vagrant.provisioners.chef.json") - - # Set up our configuration that is passed to the attributes by default - data = { :config => env[:global_config].to_hash } - - # Add our default share directory if it exists - default_share = env[:vm].config.vm.shared_folders["v-root"] - data[:directory] = default_share[:guestpath] if default_share - - # And wrap it under the "vagrant" namespace - data = { :vagrant => data } - - # Merge with the "extra data" which isn't put under the - # vagrant namespace by default - data.merge!(config.merged_json) - - json = data.to_json - - # Create a temporary file to store the data so we - # can upload it - temp = Tempfile.new("vagrant") - temp.write(json) - temp.close - - env[:vm].channel.upload(temp.path, File.join(config.provisioning_path, "dna.json")) - end - end - - class Chef < Base - class ChefError < Errors::VagrantError - error_namespace("vagrant.provisioners.chef") - end - end - - class Chef < Base - # This is the configuration which is available through `config.chef` - class Config < Vagrant::Config::Base - # Shared config - attr_accessor :node_name - attr_accessor :provisioning_path - attr_accessor :log_level - attr_accessor :json - 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 :no_proxy - attr_accessor :binary_path - attr_accessor :binary_env - attr_accessor :attempts - attr_writer :run_list - - # Provide defaults in such a way that they won't override the instance - # variable. This is so merging continues to work properly. - def attempts; @attempts || 1; end - def json; @json ||= {}; end - def log_level; @log_level || :info; end - - # This returns the json that is merged with the defaults and the - # user set data. - def merged_json - original = { :instance_role => "vagrant" } - original[:run_list] = @run_list if @run_list - original.merge(json || {}) - end - - # Returns the run list, but also sets it up to be empty if it - # hasn't been defined already. - def run_list - @run_list ||= [] - 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 - - def validate(env, errors) - super - - errors.add(I18n.t("vagrant.config.chef.vagrant_as_json_key")) if json.has_key?(:vagrant) - end - - def instance_variables_hash - # Overridden so that the 'json' key could be removed, since its just - # merged into the config anyways - result = super - result.delete("json") - result - end - end - end - end -end diff --git a/lib/vagrant/provisioners/chef_client.rb b/lib/vagrant/provisioners/chef_client.rb deleted file mode 100644 index 85d49a208..000000000 --- a/lib/vagrant/provisioners/chef_client.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'pathname' - -require 'vagrant/provisioners/chef' - -module Vagrant - module Provisioners - # This class implements provisioning via chef-client, allowing provisioning - # with a chef server. - class ChefClient < Chef - class Config < Chef::Config - attr_accessor :chef_server_url - attr_accessor :validation_key_path - attr_accessor :validation_client_name - attr_accessor :client_key_path - attr_accessor :file_cache_path - attr_accessor :file_backup_path - attr_accessor :environment - attr_accessor :encrypted_data_bag_secret_key_path - attr_accessor :encrypted_data_bag_secret - - # Provide defaults in such a way that they won't override the instance - # variable. This is so merging continues to work properly. - def validation_client_name; @validation_client_name || "chef-validator"; end - def client_key_path; @client_key_path || "/etc/chef/client.pem"; end - def file_cache_path; @file_cache_path || "/srv/chef/file_store"; end - def file_backup_path; @file_backup_path || "/srv/chef/cache"; end - def encrypted_data_bag_secret; @encrypted_data_bag_secret || "/tmp/encrypted_data_bag_secret"; end - - def validate(env, errors) - super - - errors.add(I18n.t("vagrant.config.chef.server_url_empty")) if !chef_server_url || chef_server_url.strip == "" - errors.add(I18n.t("vagrant.config.chef.validation_key_path")) if !validation_key_path - errors.add(I18n.t("vagrant.config.chef.run_list_empty")) if @run_list && @run_list.empty? - end - end - - def self.config_class - Config - end - - def prepare - raise ChefError, :server_validation_key_required if config.validation_key_path.nil? - raise ChefError, :server_validation_key_doesnt_exist if !File.file?(validation_key_path) - raise ChefError, :server_url_required if config.chef_server_url.nil? - end - - def provision! - verify_binary(chef_binary_path("chef-client")) - chown_provisioning_folder - create_client_key_folder - upload_validation_key - upload_encrypted_data_bag_secret if config.encrypted_data_bag_secret_key_path - setup_json - setup_server_config - run_chef_client - end - - def create_client_key_folder - env[:ui].info I18n.t("vagrant.provisioners.chef.client_key_folder") - path = Pathname.new(config.client_key_path) - - env[:vm].channel.sudo("mkdir -p #{path.dirname}") - end - - def upload_validation_key - env[:ui].info I18n.t("vagrant.provisioners.chef.upload_validation_key") - env[:vm].channel.upload(validation_key_path, guest_validation_key_path) - end - - def upload_encrypted_data_bag_secret - env[:ui].info I18n.t("vagrant.provisioners.chef.upload_encrypted_data_bag_secret_key") - env[:vm].channel.upload(encrypted_data_bag_secret_key_path, - config.encrypted_data_bag_secret) - end - - def setup_server_config - setup_config("provisioners/chef_client/client", "client.rb", { - :node_name => config.node_name, - :chef_server_url => config.chef_server_url, - :validation_client_name => config.validation_client_name, - :validation_key => guest_validation_key_path, - :client_key => config.client_key_path, - :file_cache_path => config.file_cache_path, - :file_backup_path => config.file_backup_path, - :environment => config.environment, - :encrypted_data_bag_secret => config.encrypted_data_bag_secret - }) - end - - def run_chef_client - command_env = config.binary_env ? "#{config.binary_env} " : "" - command = "#{command_env}#{chef_binary_path("chef-client")} -c #{config.provisioning_path}/client.rb -j #{config.provisioning_path}/dna.json" - - config.attempts.times do |attempt| - if attempt == 0 - env[:ui].info I18n.t("vagrant.provisioners.chef.running_client") - else - env[:ui].info I18n.t("vagrant.provisioners.chef.running_client_again") - end - - exit_status = env[:vm].channel.sudo(command) do |type, data| - # Output the data with the proper color based on the stream. - color = type == :stdout ? :green : :red - - # Note: Be sure to chomp the data to avoid the newlines that the - # Chef outputs. - env[:ui].info(data.chomp, :color => color, :prefix => false) - 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 validation_key_path - File.expand_path(config.validation_key_path, env[:root_path]) - end - - def encrypted_data_bag_secret_key_path - File.expand_path(config.encrypted_data_bag_secret_key_path, env[:root_path]) - end - - def guest_validation_key_path - File.join(config.provisioning_path, "validation.pem") - end - end - end -end diff --git a/lib/vagrant/provisioners/chef_solo.rb b/lib/vagrant/provisioners/chef_solo.rb deleted file mode 100644 index 2782818d3..000000000 --- a/lib/vagrant/provisioners/chef_solo.rb +++ /dev/null @@ -1,234 +0,0 @@ -require "log4r" - -require 'vagrant/provisioners/chef' - -module Vagrant - module Provisioners - # This class implements provisioning via chef-solo. - class ChefSolo < Chef - extend Util::Counter - include Util::Counter - - class Config < Chef::Config - attr_accessor :cookbooks_path - attr_accessor :roles_path - attr_accessor :data_bags_path - attr_accessor :recipe_url - attr_accessor :nfs - attr_accessor :encrypted_data_bag_secret_key_path - attr_accessor :encrypted_data_bag_secret - - def encrypted_data_bag_secret; @encrypted_data_bag_secret || "/tmp/encrypted_data_bag_secret"; end - - def initialize - super - - @__default = ["cookbooks", [:vm, "cookbooks"]] - end - - # Provide defaults in such a way that they won't override the instance - # variable. This is so merging continues to work properly. - def cookbooks_path - @cookbooks_path || _default_cookbook_path - end - - # This stores a reference to the default cookbook path which is used - # later. Do not use this publicly. I apologize for not making it - # "protected" but it has to be called by Vagrant internals later. - def _default_cookbook_path - @__default - end - - def nfs - @nfs || false - end - - def validate(env, errors) - super - - errors.add(I18n.t("vagrant.config.chef.cookbooks_path_empty")) if !cookbooks_path || [cookbooks_path].flatten.empty? - errors.add(I18n.t("vagrant.config.chef.run_list_empty")) if !run_list || run_list.empty? - end - end - - attr_reader :cookbook_folders - attr_reader :role_folders - attr_reader :data_bags_folders - - def self.config_class - Config - end - - def initialize(env, config) - super - @logger = Log4r::Logger.new("vagrant::provisioners::chef_solo") - end - - def prepare - @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") - - share_folders("csc", @cookbook_folders) - share_folders("csr", @role_folders) - share_folders("csdb", @data_bags_folders) - end - - def provision! - # Verify that the proper shared folders exist. - check = [] - [@cookbook_folders, @role_folders, @data_bags_folders].each do |folders| - 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 - end - - verify_shared_folders(check) - - verify_binary(chef_binary_path("chef-solo")) - chown_provisioning_folder - upload_encrypted_data_bag_secret if config.encrypted_data_bag_secret_key_path - setup_json - setup_solo_config - run_chef_solo - end - - # Converts paths to a list of properly expanded paths with types. - def expanded_folders(paths, appended_folder=nil) - return [] if paths.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 |path| - path = [:host, path] if !path.is_a?(Array) - type, path = 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, env[:root_path]) - - # Super hacky but if we're expanded the default cookbook paths, - # and one of the host paths doesn't exist, then just ignore it, - # because that is fine. - if paths.equal?(config._default_cookbook_path) && !File.directory?(local_path) - @logger.info("'cookbooks' folder doesn't exist on defaults. Ignoring.") - next - end - - # Path exists on the host, setup the remote path - remote_path = "#{config.provisioning_path}/chef-solo-#{get_and_update_counter(:cookbooks_path)}" - 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(prefix, folders) - folders.each do |type, local_path, remote_path| - if type == :host - env[:vm].config.vm.share_folder("v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}", - remote_path, local_path, :nfs => config.nfs) - end - end - end - - def upload_encrypted_data_bag_secret - env[:ui].info I18n.t("vagrant.provisioners.chef.upload_encrypted_data_bag_secret_key") - env[:vm].channel.upload(encrypted_data_bag_secret_key_path, - config.encrypted_data_bag_secret) - end - - def setup_solo_config - cookbooks_path = guest_paths(@cookbook_folders) - roles_path = guest_paths(@role_folders).first - data_bags_path = guest_paths(@data_bags_folders).first - - setup_config("provisioners/chef_solo/solo", "solo.rb", { - :node_name => config.node_name, - :provisioning_path => config.provisioning_path, - :cookbooks_path => cookbooks_path, - :recipe_url => config.recipe_url, - :roles_path => roles_path, - :data_bags_path => data_bags_path, - :encrypted_data_bag_secret => config.encrypted_data_bag_secret, - }) - end - - def run_chef_solo - command_env = config.binary_env ? "#{config.binary_env} " : "" - command = "#{command_env}#{chef_binary_path("chef-solo")} -c #{config.provisioning_path}/solo.rb -j #{config.provisioning_path}/dna.json" - - config.attempts.times do |attempt| - if attempt == 0 - env[:ui].info I18n.t("vagrant.provisioners.chef.running_solo") - else - env[:ui].info I18n.t("vagrant.provisioners.chef.running_solo_again") - end - - exit_status = env[:vm].channel.sudo(command, :error_check => false) do |type, data| - # Output the data with the proper color based on the stream. - color = type == :stdout ? :green : :red - - # Note: Be sure to chomp the data to avoid the newlines that the - # Chef outputs. - env[:ui].info(data.chomp, :color => color, :prefix => false) - 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 !env[:vm].channel.test("test -d #{folder}") - raise ChefError, :missing_shared_folders - end - end - end - - def encrypted_data_bag_secret_key_path - File.expand_path(config.encrypted_data_bag_secret_key_path, env[:root_path]) - end - - protected - - # Extracts only the remote paths from a list of folders - def guest_paths(folders) - folders.map { |parts| parts[2] } - end - end - end -end diff --git a/lib/vagrant/provisioners/puppet.rb b/lib/vagrant/provisioners/puppet.rb deleted file mode 100644 index 44f49b0e3..000000000 --- a/lib/vagrant/provisioners/puppet.rb +++ /dev/null @@ -1,175 +0,0 @@ -require "log4r" - -module Vagrant - module Provisioners - class PuppetError < Vagrant::Errors::VagrantError - error_namespace("vagrant.provisioners.puppet") - end - - class Puppet < Base - class Config < Vagrant::Config::Base - attr_accessor :manifest_file - attr_accessor :manifests_path - attr_accessor :module_path - attr_accessor :pp_path - attr_accessor :options - attr_accessor :facter - - def manifest_file; @manifest_file || "default.pp"; end - def manifests_path; @manifests_path || "manifests"; end - def pp_path; @pp_path || "/tmp/vagrant-puppet"; end - def options; @options ||= []; end - def facter; @facter ||= {}; end - - # Returns the manifests path expanded relative to the root path of the - # environment. - def expanded_manifests_path(root_path) - Pathname.new(manifests_path).expand_path(root_path) - end - - # Returns the module paths as an array of paths expanded relative to the - # root path. - def expanded_module_paths(root_path) - return [] if !module_path - - # Get all the paths and expand them relative to the root path, returning - # the array of expanded paths - paths = module_path - paths = [paths] if !paths.is_a?(Array) - paths.map do |path| - Pathname.new(path).expand_path(root_path) - end - end - - def validate(env, errors) - # Calculate the manifests and module paths based on env - this_expanded_manifests_path = expanded_manifests_path(env.root_path) - this_expanded_module_paths = expanded_module_paths(env.root_path) - - # Manifests path/file validation - if !this_expanded_manifests_path.directory? - errors.add(I18n.t("vagrant.provisioners.puppet.manifests_path_missing", - :path => this_expanded_manifests_path)) - else - expanded_manifest_file = this_expanded_manifests_path.join(manifest_file) - if !expanded_manifest_file.file? - errors.add(I18n.t("vagrant.provisioners.puppet.manifest_missing", - :manifest => expanded_manifest_file.to_s)) - end - end - - # Module paths validation - this_expanded_module_paths.each do |path| - if !path.directory? - errors.add(I18n.t("vagrant.provisioners.puppet.module_path_missing", :path => path)) - end - end - end - end - - def self.config_class - Config - end - - def initialize(env, config) - super - - @logger = Log4r::Logger.new("vagrant::provisioners::puppet") - end - - def prepare - # Calculate the paths we're going to use based on the environment - @expanded_manifests_path = config.expanded_manifests_path(env[:root_path]) - @expanded_module_paths = config.expanded_module_paths(env[:root_path]) - @manifest_file = File.join(manifests_guest_path, config.manifest_file) - - set_module_paths - share_manifests - share_module_paths - end - - def provision! - # Check that the shared folders are properly shared - check = [manifests_guest_path] - @module_paths.each do |host_path, guest_path| - check << guest_path - end - - verify_shared_folders(check) - - # Verify Puppet is installed and run it - verify_binary("puppet") - run_puppet_client - end - - def share_manifests - env[:vm].config.vm.share_folder("manifests", manifests_guest_path, @expanded_manifests_path) - end - - def share_module_paths - count = 0 - @module_paths.each do |from, to| - # Sorry for the cryptic key here, but VirtualBox has a strange limit on - # maximum size for it and its something small (around 10) - env[:vm].config.vm.share_folder("v-pp-m#{count}", to, from) - count += 1 - end - end - - def set_module_paths - @module_paths = {} - @expanded_module_paths.each_with_index do |path, i| - @module_paths[path] = File.join(config.pp_path, "modules-#{i}") - end - end - - def manifests_guest_path - File.join(config.pp_path, "manifests") - end - - def verify_binary(binary) - env[:vm].channel.sudo("which #{binary}", - :error_class => PuppetError, - :error_key => :not_detected, - :binary => binary) - end - - def run_puppet_client - options = [config.options].flatten - options << "--modulepath '#{@module_paths.values.join(':')}'" if !@module_paths.empty? - options << @manifest_file - options = options.join(" ") - - # Build up the custom facts if we have any - facter = "" - if !config.facter.empty? - facts = [] - config.facter.each do |key, value| - facts << "FACTER_#{key}='#{value}'" - end - - facter = "#{facts.join(" ")} " - end - - command = "cd #{manifests_guest_path} && #{facter}puppet apply #{options}" - - env[:ui].info I18n.t("vagrant.provisioners.puppet.running_puppet", - :manifest => @manifest_file) - - env[:vm].channel.sudo(command) do |type, data| - env[:ui].info(data.chomp, :prefix => false) - end - end - - def verify_shared_folders(folders) - folders.each do |folder| - @logger.debug("Checking for shared folder: #{folder}") - if !env[:vm].channel.test("test -d #{folder}") - raise PuppetError, :missing_shared_folders - end - end - end - end - end -end - diff --git a/lib/vagrant/provisioners/puppet_server.rb b/lib/vagrant/provisioners/puppet_server.rb deleted file mode 100644 index ade4f65ee..000000000 --- a/lib/vagrant/provisioners/puppet_server.rb +++ /dev/null @@ -1,78 +0,0 @@ -module Vagrant - module Provisioners - class PuppetServerError < Vagrant::Errors::VagrantError - error_namespace("vagrant.provisioners.puppet_server") - end - - class PuppetServer < Base - class Config < Vagrant::Config::Base - attr_accessor :puppet_server - attr_accessor :puppet_node - attr_accessor :options - attr_accessor :facter - - def facter; @facter ||= {}; end - def puppet_server; @puppet_server || "puppet"; end - def options; @options ||= []; end - end - - def self.config_class - Config - end - - def provision! - verify_binary("puppetd") - run_puppetd_client - end - - def verify_binary(binary) - env[:vm].channel.sudo("which #{binary}", - :error_class => PuppetServerError, - :error_key => :not_detected, - :binary => binary) - end - - def run_puppetd_client - options = config.options - options = [options] if !options.is_a?(Array) - - # Intelligently set the puppet node cert name based on certain - # external parameters. - cn = nil - if config.puppet_node - # If a node name is given, we use that directly for the certname - cn = config.puppet_node - elsif env[:vm].config.vm.host_name - # If a host name is given, we explicitly set the certname to - # nil so that the hostname becomes the cert name. - cn = nil - else - # Otherwise, we default to the name of the box. - cn = env[:vm].config.vm.box - end - - # Add the certname option if there is one - options += ["--certname", cn] if cn - options = options.join(" ") - - # Build up the custom facts if we have any - facter = "" - if !config.facter.empty? - facts = [] - config.facter.each do |key, value| - facts << "FACTER_#{key}='#{value}'" - end - - facter = "#{facts.join(" ")} " - end - - command = "#{facter}puppetd #{options} --server #{config.puppet_server}" - - env[:ui].info I18n.t("vagrant.provisioners.puppet_server.running_puppetd") - env[:vm].channel.sudo(command) do |type, data| - env[:ui].info(data.chomp, :prefix => false) - end - end - end - end -end diff --git a/plugins/chef/plugin.rb b/plugins/chef/plugin.rb new file mode 100644 index 000000000..b7327d702 --- /dev/null +++ b/plugins/chef/plugin.rb @@ -0,0 +1,21 @@ +require "vagrant" + +module VagrantPlugins + module Chef + module Provisioner + autoload :ChefSolo, File.expand_path("../provisioner/chef_solo", __FILE__) + autoload :ChefClient, File.expand_path("../provisioner/chef_client", __FILE__) + end + + class Plugin < Vagrant.plugin("1") + name "chef" + description <<-DESC + Provides support for provisioning your virtual machines with + Chef via `chef-solo` or `chef-client`. + DESC + + provisioner("chef_solo") { Provisioner::ChefSolo } + provisioner("chef_client") { Provisioner::ChefClient } + end + end +end diff --git a/plugins/chef/provisioner/base.rb b/plugins/chef/provisioner/base.rb new file mode 100644 index 000000000..925c6c2ba --- /dev/null +++ b/plugins/chef/provisioner/base.rb @@ -0,0 +1,166 @@ +require 'tempfile' + +module VagrantPlugins + module Chef + module Provisioner + # This class is a base class where the common functionality shared between + # chef-solo and chef-client provisioning are stored. This is **not an actual + # provisioner**. Instead, {ChefSolo} or {ChefServer} should be used. + class Base < Vagrant::Provisioners::Base + include Vagrant::Util::Counter + + def initialize(env, config) + super + + config.provisioning_path ||= "/tmp/vagrant-chef-#{get_and_update_counter(:provisioning_path)}" + end + + def prepare + raise ChefError, :invalid_provisioner + end + + def verify_binary(binary) + # Checks for the existence of chef binary and error if it + # doesn't exist. + env[:vm].channel.sudo("which #{binary}", + :error_class => ChefError, + :error_key => :chef_not_detected, + :binary => binary) + end + + # Returns the path to the Chef binary, taking into account the + # `binary_path` configuration option. + def chef_binary_path(binary) + return binary if !config.binary_path + return File.join(config.binary_path, binary) + end + + def chown_provisioning_folder + env[:vm].channel.sudo("mkdir -p #{config.provisioning_path}") + env[:vm].channel.sudo("chown #{env[:vm].config.ssh.username} #{config.provisioning_path}") + end + + def setup_config(template, filename, template_vars) + config_file = TemplateRenderer.render(template, { + :log_level => config.log_level.to_sym, + :http_proxy => config.http_proxy, + :http_proxy_user => config.http_proxy_user, + :http_proxy_pass => config.http_proxy_pass, + :https_proxy => config.https_proxy, + :https_proxy_user => config.https_proxy_user, + :https_proxy_pass => config.https_proxy_pass, + :no_proxy => config.no_proxy + }.merge(template_vars)) + + # Create a temporary file to store the data so we + # can upload it + temp = Tempfile.new("vagrant") + temp.write(config_file) + temp.close + + remote_file = File.join(config.provisioning_path, filename) + env[:vm].channel.sudo("rm #{remote_file}", :error_check => false) + env[:vm].channel.upload(temp.path, remote_file) + end + + def setup_json + env[:ui].info I18n.t("vagrant.provisioners.chef.json") + + # Set up our configuration that is passed to the attributes by default + data = { :config => env[:global_config].to_hash } + + # Add our default share directory if it exists + default_share = env[:vm].config.vm.shared_folders["v-root"] + data[:directory] = default_share[:guestpath] if default_share + + # And wrap it under the "vagrant" namespace + data = { :vagrant => data } + + # Merge with the "extra data" which isn't put under the + # vagrant namespace by default + data.merge!(config.merged_json) + + json = data.to_json + + # Create a temporary file to store the data so we + # can upload it + temp = Tempfile.new("vagrant") + temp.write(json) + temp.close + + env[:vm].channel.upload(temp.path, File.join(config.provisioning_path, "dna.json")) + end + + class ChefError < Errors::VagrantError + error_namespace("vagrant.provisioners.chef") + end + + # This is the configuration which is available through `config.chef` + class Config < Vagrant::Config::Base + # Shared config + attr_accessor :node_name + attr_accessor :provisioning_path + attr_accessor :log_level + attr_accessor :json + 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 :no_proxy + attr_accessor :binary_path + attr_accessor :binary_env + attr_accessor :attempts + attr_writer :run_list + + # Provide defaults in such a way that they won't override the instance + # variable. This is so merging continues to work properly. + def attempts; @attempts || 1; end + def json; @json ||= {}; end + def log_level; @log_level || :info; end + + # This returns the json that is merged with the defaults and the + # user set data. + def merged_json + original = { :instance_role => "vagrant" } + original[:run_list] = @run_list if @run_list + original.merge(json || {}) + end + + # Returns the run list, but also sets it up to be empty if it + # hasn't been defined already. + def run_list + @run_list ||= [] + 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 + + def validate(env, errors) + super + + errors.add(I18n.t("vagrant.config.chef.vagrant_as_json_key")) if json.has_key?(:vagrant) + end + + def instance_variables_hash + # Overridden so that the 'json' key could be removed, since its just + # merged into the config anyways + result = super + result.delete("json") + result + end + end + end + end + end +end diff --git a/plugins/chef/provisioner/chef_client.rb b/plugins/chef/provisioner/chef_client.rb new file mode 100644 index 000000000..aa1e8faae --- /dev/null +++ b/plugins/chef/provisioner/chef_client.rb @@ -0,0 +1,134 @@ +require 'pathname' + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module Chef + module Provisioner + # This class implements provisioning via chef-client, allowing provisioning + # with a chef server. + class ChefClient < Base + class Config < Base::Config + attr_accessor :chef_server_url + attr_accessor :validation_key_path + attr_accessor :validation_client_name + attr_accessor :client_key_path + attr_accessor :file_cache_path + attr_accessor :file_backup_path + attr_accessor :environment + attr_accessor :encrypted_data_bag_secret_key_path + attr_accessor :encrypted_data_bag_secret + + # Provide defaults in such a way that they won't override the instance + # variable. This is so merging continues to work properly. + def validation_client_name; @validation_client_name || "chef-validator"; end + def client_key_path; @client_key_path || "/etc/chef/client.pem"; end + def file_cache_path; @file_cache_path || "/srv/chef/file_store"; end + def file_backup_path; @file_backup_path || "/srv/chef/cache"; end + def encrypted_data_bag_secret; @encrypted_data_bag_secret || "/tmp/encrypted_data_bag_secret"; end + + def validate(env, errors) + super + + errors.add(I18n.t("vagrant.config.chef.server_url_empty")) if !chef_server_url || chef_server_url.strip == "" + errors.add(I18n.t("vagrant.config.chef.validation_key_path")) if !validation_key_path + errors.add(I18n.t("vagrant.config.chef.run_list_empty")) if @run_list && @run_list.empty? + end + end + + def self.config_class + Config + end + + def prepare + raise ChefError, :server_validation_key_required if config.validation_key_path.nil? + raise ChefError, :server_validation_key_doesnt_exist if !File.file?(validation_key_path) + raise ChefError, :server_url_required if config.chef_server_url.nil? + end + + def provision! + verify_binary(chef_binary_path("chef-client")) + chown_provisioning_folder + create_client_key_folder + upload_validation_key + upload_encrypted_data_bag_secret if config.encrypted_data_bag_secret_key_path + setup_json + setup_server_config + run_chef_client + end + + def create_client_key_folder + env[:ui].info I18n.t("vagrant.provisioners.chef.client_key_folder") + path = Pathname.new(config.client_key_path) + + env[:vm].channel.sudo("mkdir -p #{path.dirname}") + end + + def upload_validation_key + env[:ui].info I18n.t("vagrant.provisioners.chef.upload_validation_key") + env[:vm].channel.upload(validation_key_path, guest_validation_key_path) + end + + def upload_encrypted_data_bag_secret + env[:ui].info I18n.t("vagrant.provisioners.chef.upload_encrypted_data_bag_secret_key") + env[:vm].channel.upload(encrypted_data_bag_secret_key_path, + config.encrypted_data_bag_secret) + end + + def setup_server_config + setup_config("provisioners/chef_client/client", "client.rb", { + :node_name => config.node_name, + :chef_server_url => config.chef_server_url, + :validation_client_name => config.validation_client_name, + :validation_key => guest_validation_key_path, + :client_key => config.client_key_path, + :file_cache_path => config.file_cache_path, + :file_backup_path => config.file_backup_path, + :environment => config.environment, + :encrypted_data_bag_secret => config.encrypted_data_bag_secret + }) + end + + def run_chef_client + command_env = config.binary_env ? "#{config.binary_env} " : "" + command = "#{command_env}#{chef_binary_path("chef-client")} -c #{config.provisioning_path}/client.rb -j #{config.provisioning_path}/dna.json" + + config.attempts.times do |attempt| + if attempt == 0 + env[:ui].info I18n.t("vagrant.provisioners.chef.running_client") + else + env[:ui].info I18n.t("vagrant.provisioners.chef.running_client_again") + end + + exit_status = env[:vm].channel.sudo(command) do |type, data| + # Output the data with the proper color based on the stream. + color = type == :stdout ? :green : :red + + # Note: Be sure to chomp the data to avoid the newlines that the + # Chef outputs. + env[:ui].info(data.chomp, :color => color, :prefix => false) + 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 validation_key_path + File.expand_path(config.validation_key_path, env[:root_path]) + end + + def encrypted_data_bag_secret_key_path + File.expand_path(config.encrypted_data_bag_secret_key_path, env[:root_path]) + end + + def guest_validation_key_path + File.join(config.provisioning_path, "validation.pem") + end + end + end + end +end diff --git a/plugins/chef/provisioner/chef_solo.rb b/plugins/chef/provisioner/chef_solo.rb new file mode 100644 index 000000000..14ca4906a --- /dev/null +++ b/plugins/chef/provisioner/chef_solo.rb @@ -0,0 +1,236 @@ +require "log4r" + +require File.expand_path("../base", __FILE__) + +module VagrantPlugins + module Chef + module Provisioner + # This class implements provisioning via chef-solo. + class ChefSolo < Base + extend Vagrant::Util::Counter + include Vagrant::Util::Counter + + class Config < Base::Config + attr_accessor :cookbooks_path + attr_accessor :roles_path + attr_accessor :data_bags_path + attr_accessor :recipe_url + attr_accessor :nfs + attr_accessor :encrypted_data_bag_secret_key_path + attr_accessor :encrypted_data_bag_secret + + def encrypted_data_bag_secret; @encrypted_data_bag_secret || "/tmp/encrypted_data_bag_secret"; end + + def initialize + super + + @__default = ["cookbooks", [:vm, "cookbooks"]] + end + + # Provide defaults in such a way that they won't override the instance + # variable. This is so merging continues to work properly. + def cookbooks_path + @cookbooks_path || _default_cookbook_path + end + + # This stores a reference to the default cookbook path which is used + # later. Do not use this publicly. I apologize for not making it + # "protected" but it has to be called by Vagrant internals later. + def _default_cookbook_path + @__default + end + + def nfs + @nfs || false + end + + def validate(env, errors) + super + + errors.add(I18n.t("vagrant.config.chef.cookbooks_path_empty")) if !cookbooks_path || [cookbooks_path].flatten.empty? + errors.add(I18n.t("vagrant.config.chef.run_list_empty")) if !run_list || run_list.empty? + end + end + + attr_reader :cookbook_folders + attr_reader :role_folders + attr_reader :data_bags_folders + + def self.config_class + Config + end + + def initialize(env, config) + super + @logger = Log4r::Logger.new("vagrant::provisioners::chef_solo") + end + + def prepare + @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") + + share_folders("csc", @cookbook_folders) + share_folders("csr", @role_folders) + share_folders("csdb", @data_bags_folders) + end + + def provision! + # Verify that the proper shared folders exist. + check = [] + [@cookbook_folders, @role_folders, @data_bags_folders].each do |folders| + 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 + end + + verify_shared_folders(check) + + verify_binary(chef_binary_path("chef-solo")) + chown_provisioning_folder + upload_encrypted_data_bag_secret if config.encrypted_data_bag_secret_key_path + setup_json + setup_solo_config + run_chef_solo + end + + # Converts paths to a list of properly expanded paths with types. + def expanded_folders(paths, appended_folder=nil) + return [] if paths.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 |path| + path = [:host, path] if !path.is_a?(Array) + type, path = 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, env[:root_path]) + + # Super hacky but if we're expanded the default cookbook paths, + # and one of the host paths doesn't exist, then just ignore it, + # because that is fine. + if paths.equal?(config._default_cookbook_path) && !File.directory?(local_path) + @logger.info("'cookbooks' folder doesn't exist on defaults. Ignoring.") + next + end + + # Path exists on the host, setup the remote path + remote_path = "#{config.provisioning_path}/chef-solo-#{get_and_update_counter(:cookbooks_path)}" + 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(prefix, folders) + folders.each do |type, local_path, remote_path| + if type == :host + env[:vm].config.vm.share_folder("v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}", + remote_path, local_path, :nfs => config.nfs) + end + end + end + + def upload_encrypted_data_bag_secret + env[:ui].info I18n.t("vagrant.provisioners.chef.upload_encrypted_data_bag_secret_key") + env[:vm].channel.upload(encrypted_data_bag_secret_key_path, + config.encrypted_data_bag_secret) + end + + def setup_solo_config + cookbooks_path = guest_paths(@cookbook_folders) + roles_path = guest_paths(@role_folders).first + data_bags_path = guest_paths(@data_bags_folders).first + + setup_config("provisioners/chef_solo/solo", "solo.rb", { + :node_name => config.node_name, + :provisioning_path => config.provisioning_path, + :cookbooks_path => cookbooks_path, + :recipe_url => config.recipe_url, + :roles_path => roles_path, + :data_bags_path => data_bags_path, + :encrypted_data_bag_secret => config.encrypted_data_bag_secret, + }) + end + + def run_chef_solo + command_env = config.binary_env ? "#{config.binary_env} " : "" + command = "#{command_env}#{chef_binary_path("chef-solo")} -c #{config.provisioning_path}/solo.rb -j #{config.provisioning_path}/dna.json" + + config.attempts.times do |attempt| + if attempt == 0 + env[:ui].info I18n.t("vagrant.provisioners.chef.running_solo") + else + env[:ui].info I18n.t("vagrant.provisioners.chef.running_solo_again") + end + + exit_status = env[:vm].channel.sudo(command, :error_check => false) do |type, data| + # Output the data with the proper color based on the stream. + color = type == :stdout ? :green : :red + + # Note: Be sure to chomp the data to avoid the newlines that the + # Chef outputs. + env[:ui].info(data.chomp, :color => color, :prefix => false) + 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 !env[:vm].channel.test("test -d #{folder}") + raise ChefError, :missing_shared_folders + end + end + end + + def encrypted_data_bag_secret_key_path + File.expand_path(config.encrypted_data_bag_secret_key_path, env[:root_path]) + end + + protected + + # Extracts only the remote paths from a list of folders + def guest_paths(folders) + folders.map { |parts| parts[2] } + end + end + end + end +end diff --git a/plugins/kernel/config/vm_provisioner.rb b/plugins/kernel/config/vm_provisioner.rb index 8553f2599..52d4b5d22 100644 --- a/plugins/kernel/config/vm_provisioner.rb +++ b/plugins/kernel/config/vm_provisioner.rb @@ -13,9 +13,19 @@ module VagrantPlugins @logger.debug("Provisioner config: #{shortcut}") @shortcut = shortcut @provisioner = shortcut - @provisioner = Vagrant.provisioners.get(shortcut) if shortcut.is_a?(Symbol) @config = nil + # If the shorcut is a symbol, we look through the registered + # plugins to see if any define a provisioner we want. + if shortcut.is_a?(Symbol) + Vagrant.plugin("1").registered.each do |plugin| + if plugin.provisioner.has_key?(shortcut) + @provisioner = plugin.provisioner[shortcut] + break + end + end + end + @logger.info("Provisioner class: #{provisioner}") configure(options, &block) if @provisioner end diff --git a/plugins/puppet/plugin.rb b/plugins/puppet/plugin.rb new file mode 100644 index 000000000..560ba7dad --- /dev/null +++ b/plugins/puppet/plugin.rb @@ -0,0 +1,21 @@ +require "vagrant" + +module VagrantPlugins + module Pupppet + module Provisioner + autoload :Puppet, File.expand_path("../provisioner/puppet", __FILE__) + autoload :PuppetServer, File.expand_path("../provisioner/puppet_server", __FILE__) + end + + class Plugin < Vagrant.plugin("1") + name "puppet" + description <<-DESC + Provides support for provisioning your virtual machines with + Puppet either using `puppet apply` or a Puppet server. + DESC + + provisioner("puppet") { Provisioner::Puppet } + provisioner("puppet_server") { Provisioner::PuppetServer } + end + end +end diff --git a/plugins/puppet/provisioner/puppet.rb b/plugins/puppet/provisioner/puppet.rb new file mode 100644 index 000000000..44fa1b020 --- /dev/null +++ b/plugins/puppet/provisioner/puppet.rb @@ -0,0 +1,176 @@ +require "log4r" + +module VagrantPlugins + module Puppet + module Provisioner + class PuppetError < Vagrant::Errors::VagrantError + error_namespace("vagrant.provisioners.puppet") + end + + class Puppet < Vagrant::Provisioners::Base + class Config < Vagrant::Config::Base + attr_accessor :manifest_file + attr_accessor :manifests_path + attr_accessor :module_path + attr_accessor :pp_path + attr_accessor :options + attr_accessor :facter + + def manifest_file; @manifest_file || "default.pp"; end + def manifests_path; @manifests_path || "manifests"; end + def pp_path; @pp_path || "/tmp/vagrant-puppet"; end + def options; @options ||= []; end + def facter; @facter ||= {}; end + + # Returns the manifests path expanded relative to the root path of the + # environment. + def expanded_manifests_path(root_path) + Pathname.new(manifests_path).expand_path(root_path) + end + + # Returns the module paths as an array of paths expanded relative to the + # root path. + def expanded_module_paths(root_path) + return [] if !module_path + + # Get all the paths and expand them relative to the root path, returning + # the array of expanded paths + paths = module_path + paths = [paths] if !paths.is_a?(Array) + paths.map do |path| + Pathname.new(path).expand_path(root_path) + end + end + + def validate(env, errors) + # Calculate the manifests and module paths based on env + this_expanded_manifests_path = expanded_manifests_path(env.root_path) + this_expanded_module_paths = expanded_module_paths(env.root_path) + + # Manifests path/file validation + if !this_expanded_manifests_path.directory? + errors.add(I18n.t("vagrant.provisioners.puppet.manifests_path_missing", + :path => this_expanded_manifests_path)) + else + expanded_manifest_file = this_expanded_manifests_path.join(manifest_file) + if !expanded_manifest_file.file? + errors.add(I18n.t("vagrant.provisioners.puppet.manifest_missing", + :manifest => expanded_manifest_file.to_s)) + end + end + + # Module paths validation + this_expanded_module_paths.each do |path| + if !path.directory? + errors.add(I18n.t("vagrant.provisioners.puppet.module_path_missing", :path => path)) + end + end + end + end + + def self.config_class + Config + end + + def initialize(env, config) + super + + @logger = Log4r::Logger.new("vagrant::provisioners::puppet") + end + + def prepare + # Calculate the paths we're going to use based on the environment + @expanded_manifests_path = config.expanded_manifests_path(env[:root_path]) + @expanded_module_paths = config.expanded_module_paths(env[:root_path]) + @manifest_file = File.join(manifests_guest_path, config.manifest_file) + + set_module_paths + share_manifests + share_module_paths + end + + def provision! + # Check that the shared folders are properly shared + check = [manifests_guest_path] + @module_paths.each do |host_path, guest_path| + check << guest_path + end + + verify_shared_folders(check) + + # Verify Puppet is installed and run it + verify_binary("puppet") + run_puppet_client + end + + def share_manifests + env[:vm].config.vm.share_folder("manifests", manifests_guest_path, @expanded_manifests_path) + end + + def share_module_paths + count = 0 + @module_paths.each do |from, to| + # Sorry for the cryptic key here, but VirtualBox has a strange limit on + # maximum size for it and its something small (around 10) + env[:vm].config.vm.share_folder("v-pp-m#{count}", to, from) + count += 1 + end + end + + def set_module_paths + @module_paths = {} + @expanded_module_paths.each_with_index do |path, i| + @module_paths[path] = File.join(config.pp_path, "modules-#{i}") + end + end + + def manifests_guest_path + File.join(config.pp_path, "manifests") + end + + def verify_binary(binary) + env[:vm].channel.sudo("which #{binary}", + :error_class => PuppetError, + :error_key => :not_detected, + :binary => binary) + end + + def run_puppet_client + options = [config.options].flatten + options << "--modulepath '#{@module_paths.values.join(':')}'" if !@module_paths.empty? + options << @manifest_file + options = options.join(" ") + + # Build up the custom facts if we have any + facter = "" + if !config.facter.empty? + facts = [] + config.facter.each do |key, value| + facts << "FACTER_#{key}='#{value}'" + end + + facter = "#{facts.join(" ")} " + end + + command = "cd #{manifests_guest_path} && #{facter}puppet apply #{options}" + + env[:ui].info I18n.t("vagrant.provisioners.puppet.running_puppet", + :manifest => @manifest_file) + + env[:vm].channel.sudo(command) do |type, data| + env[:ui].info(data.chomp, :prefix => false) + end + end + + def verify_shared_folders(folders) + folders.each do |folder| + @logger.debug("Checking for shared folder: #{folder}") + if !env[:vm].channel.test("test -d #{folder}") + raise PuppetError, :missing_shared_folders + end + end + end + end + end + end +end diff --git a/plugins/puppet/provisioner/puppet_server.rb b/plugins/puppet/provisioner/puppet_server.rb new file mode 100644 index 000000000..11d74b94d --- /dev/null +++ b/plugins/puppet/provisioner/puppet_server.rb @@ -0,0 +1,80 @@ +module VagrantPlugins + module Puppet + module Provisioner + class PuppetServerError < Vagrant::Errors::VagrantError + error_namespace("vagrant.provisioners.puppet_server") + end + + class PuppetServer < Base + class Config < Vagrant::Config::Base + attr_accessor :puppet_server + attr_accessor :puppet_node + attr_accessor :options + attr_accessor :facter + + def facter; @facter ||= {}; end + def puppet_server; @puppet_server || "puppet"; end + def options; @options ||= []; end + end + + def self.config_class + Config + end + + def provision! + verify_binary("puppetd") + run_puppetd_client + end + + def verify_binary(binary) + env[:vm].channel.sudo("which #{binary}", + :error_class => PuppetServerError, + :error_key => :not_detected, + :binary => binary) + end + + def run_puppetd_client + options = config.options + options = [options] if !options.is_a?(Array) + + # Intelligently set the puppet node cert name based on certain + # external parameters. + cn = nil + if config.puppet_node + # If a node name is given, we use that directly for the certname + cn = config.puppet_node + elsif env[:vm].config.vm.host_name + # If a host name is given, we explicitly set the certname to + # nil so that the hostname becomes the cert name. + cn = nil + else + # Otherwise, we default to the name of the box. + cn = env[:vm].config.vm.box + end + + # Add the certname option if there is one + options += ["--certname", cn] if cn + options = options.join(" ") + + # Build up the custom facts if we have any + facter = "" + if !config.facter.empty? + facts = [] + config.facter.each do |key, value| + facts << "FACTER_#{key}='#{value}'" + end + + facter = "#{facts.join(" ")} " + end + + command = "#{facter}puppetd #{options} --server #{config.puppet_server}" + + env[:ui].info I18n.t("vagrant.provisioners.puppet_server.running_puppetd") + env[:vm].channel.sudo(command) do |type, data| + env[:ui].info(data.chomp, :prefix => false) + end + end + end + end + end +end diff --git a/plugins/shell/plugin.rb b/plugins/shell/plugin.rb new file mode 100644 index 000000000..d3136bf3e --- /dev/null +++ b/plugins/shell/plugin.rb @@ -0,0 +1,17 @@ +require "vagrant" + +module VagrantPlugins + module Shell + autoload :Provisioner, File.expand_path("../provisioner", __FILE__) + + class Plugin < Vagrant.plugin("1") + name "shell" + description <<-DESC + Provides support for provisioning your virtual machines with + shell scripts. + DESC + + provisioner("shell") { Provisioner } + end + end +end diff --git a/lib/vagrant/provisioners/shell.rb b/plugins/shell/provisioner.rb similarity index 95% rename from lib/vagrant/provisioners/shell.rb rename to plugins/shell/provisioner.rb index c1c035167..6811361de 100644 --- a/lib/vagrant/provisioners/shell.rb +++ b/plugins/shell/provisioner.rb @@ -1,8 +1,9 @@ -require 'tempfile' +require "pathname" +require "tempfile" -module Vagrant - module Provisioners - class Shell < Base +module VagrantPlugins + module Shell + class Provisioner < Vagrant::Provisioners::Base class Config < Vagrant::Config::Base attr_accessor :inline attr_accessor :path diff --git a/test/unit/vagrant/plugin/v1_test.rb b/test/unit/vagrant/plugin/v1_test.rb index 33b0d7c42..ab513f030 100644 --- a/test/unit/vagrant/plugin/v1_test.rb +++ b/test/unit/vagrant/plugin/v1_test.rb @@ -79,6 +79,34 @@ describe Vagrant::Plugin::V1 do end end + describe "provisioners" do + it "should register provisioner classes" do + plugin = Class.new(described_class) do + provisioner("foo") { "bar" } + end + + plugin.provisioner[:foo].should == "bar" + end + + it "should lazily register provisioner classes" do + # Below would raise an error if the value of the config class was + # evaluated immediately. By asserting that this does not raise an + # error, we verify that the value is actually lazily loaded + plugin = nil + expect { + plugin = Class.new(described_class) do + provisioner("foo") { raise StandardError, "FAIL!" } + end + }.to_not raise_error + + # Now verify when we actually get the configuration key that + # a proper error is raised. + expect { + plugin.provisioner[:foo] + }.to raise_error(StandardError) + end + end + describe "plugin registration" do it "should have no registered plugins" do described_class.registered.should be_empty diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index f69f2e8ba..a1116498e 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -13,10 +13,6 @@ describe Vagrant do described_class.hosts.should be_a(Vagrant::Registry) end - it "has a registry for provisioners" do - described_class.provisioners.should be_a(Vagrant::Registry) - end - describe "plugin superclass" do it "returns the proper class for version 1" do described_class.plugin("1").should == Vagrant::Plugin::V1