diff --git a/config/default.rb b/config/default.rb index 55e64872f..dc24ca140 100644 --- a/config/default.rb +++ b/config/default.rb @@ -16,15 +16,8 @@ Vagrant::Config.run do |config| config.vm.project_directory = "/vagrant" config.vm.forward_port("ssh", 22, 2222) config.vm.disk_image_format = 'VMDK' + config.vm.provisioner = nil config.package.name = 'vagrant' config.package.extension = '.box' - - config.chef.enabled = false - config.chef.cookbooks_path = "cookbooks" - config.chef.provisioning_path = "/tmp/vagrant-chef" - config.chef.json = { - :instance_role => "vagrant", - :recipes => ["vagrant_main"] - } end diff --git a/lib/vagrant.rb b/lib/vagrant.rb index c26475201..a93685a6f 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -4,7 +4,8 @@ PROJECT_ROOT = File.join(libdir, '..') unless defined?(PROJECT_ROOT) # The libs which must be loaded prior to the rest %w{tempfile open-uri json pathname logger uri net/http virtualbox net/ssh archive/tar/minitar - net/scp fileutils vagrant/util vagrant/actions/base vagrant/downloaders/base vagrant/actions/runner}.each do |f| + net/scp fileutils vagrant/util vagrant/actions/base vagrant/downloaders/base vagrant/actions/runner + vagrant/config vagrant/provisioners/base vagrant/provisioners/chef}.each do |f| require f end diff --git a/lib/vagrant/actions/vm/provision.rb b/lib/vagrant/actions/vm/provision.rb index 3750fb21a..14b765614 100644 --- a/lib/vagrant/actions/vm/provision.rb +++ b/lib/vagrant/actions/vm/provision.rb @@ -2,69 +2,47 @@ module Vagrant module Actions module VM class Provision < Base + attr_reader :provisioner + + def intialize(*args) + super + + @provisioner = nil + end + def prepare - Vagrant.config.vm.share_folder("vagrant-provisioning", cookbooks_path, File.expand_path(Vagrant.config.chef.cookbooks_path, Env.root_path)) + provisioner = Vagrant.config.vm.provisioner + + if provisioner.nil? + logger.info("Provisioning not enabled, ignoring this step") + return + end + + if provisioner.is_a?(Class) + @provisioner = provisioner.new + raise ActionException.new("Provisioners must be an instance of Vagrant::Provisioners::Base") unless @provisioner.is_a?(Provisioners::Base) + elsif provisioner.is_a?(Symbol) + # We have a few hard coded provisioners for built-ins + mapping = { + :chef_solo => Provisioners::ChefSolo, + :chef_server => Provisioners::ChefServer + } + + provisioner_klass = mapping[provisioner] + raise ActionException.new("Unknown provisioner type: #{provisioner}") if provisioner_klass.nil? + @provisioner = provisioner_klass.new + end + + logger.info "Provisioning enabled with #{@provisioner.class}" + @provisioner.prepare end def execute! - chown_provisioning_folder - setup_json - setup_solo_config - run_chef_solo - end - - def chown_provisioning_folder - logger.info "Setting permissions on provisioning folder..." - SSH.execute do |ssh| - ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}") + if provisioner + logger.info "Beginning provisioning process..." + provisioner.provision! end end - - def setup_json - logger.info "Generating JSON and uploading..." - - # Set up initial configuration - data = { - :config => Vagrant.config, - :directory => Vagrant.config.vm.project_directory, - } - - # 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!(Vagrant.config.chef.json) - - json = data.to_json - - SSH.upload!(StringIO.new(json), File.join(Vagrant.config.chef.provisioning_path, "dna.json")) - end - - def setup_solo_config - solo_file = <<-solo -file_cache_path "#{Vagrant.config.chef.provisioning_path}" -cookbook_path "#{cookbooks_path}" -solo - - logger.info "Uploading chef-solo configuration script..." - SSH.upload!(StringIO.new(solo_file), File.join(Vagrant.config.chef.provisioning_path, "solo.rb")) - end - - def run_chef_solo - logger.info "Running chef recipes..." - SSH.execute do |ssh| - ssh.exec!("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json") do |channel, data, stream| - # TODO: Very verbose. It would be easier to save the data and only show it during - # an error, or when verbosity level is set high - logger.info("#{stream}: #{data}") - end - end - end - - def cookbooks_path - File.join(Vagrant.config.chef.provisioning_path, "cookbooks") - end end end end diff --git a/lib/vagrant/actions/vm/reload.rb b/lib/vagrant/actions/vm/reload.rb index d699e0ff5..940cf183b 100644 --- a/lib/vagrant/actions/vm/reload.rb +++ b/lib/vagrant/actions/vm/reload.rb @@ -5,7 +5,7 @@ module Vagrant def prepare steps = [ForwardPorts, SharedFolders, Boot] steps.unshift(Halt) if @runner.vm.running? - steps << Provision if Vagrant.config.chef.enabled + steps << Provision if !Vagrant.config.vm.provisioner.nil? steps.each do |action_klass| @runner.add_action(action_klass) diff --git a/lib/vagrant/actions/vm/up.rb b/lib/vagrant/actions/vm/up.rb index 190fc64f1..6556e4863 100644 --- a/lib/vagrant/actions/vm/up.rb +++ b/lib/vagrant/actions/vm/up.rb @@ -17,7 +17,7 @@ msg # Up is a "meta-action" so it really just queues up a bunch # of other actions in its place: steps = [Import, ForwardPorts, SharedFolders, Boot] - steps << Provision if Vagrant.config.chef.enabled + steps << Provision if !Vagrant.config.vm.provisioner.nil? steps.insert(0, MoveHardDrive) if Vagrant.config.vm.hd_location steps.each do |action_klass| diff --git a/lib/vagrant/config.rb b/lib/vagrant/config.rb index 0b6459536..afeaa78e4 100644 --- a/lib/vagrant/config.rb +++ b/lib/vagrant/config.rb @@ -14,7 +14,7 @@ module Vagrant end def configures(key, klass) - @@config.class.configures(key, klass) + config.class.configures(key, klass) end def config @@ -75,11 +75,12 @@ module Vagrant attr_reader :shared_folders attr_accessor :hd_location attr_accessor :disk_image_format - + attr_accessor :provisioner def initialize @forwarded_ports = {} @shared_folders = {} + @provisioner = nil end def forward_port(name, guestport, hostport, protocol="TCP") @@ -112,25 +113,6 @@ module Vagrant attr_accessor :extension end - class ChefConfig < Base - attr_accessor :cookbooks_path - attr_accessor :provisioning_path - attr_accessor :json - attr_accessor :enabled - - def initialize - @enabled = false - end - - def to_json - # Overridden so that the 'json' key could be removed, since its just - # merged into the config anyways - data = instance_variables_hash - data.delete(:json) - data.to_json - end - end - class VagrantConfig < Base attr_accessor :dotfile_name attr_accessor :log_output @@ -159,7 +141,6 @@ module Vagrant configures :package, PackageConfig configures :ssh, SSHConfig configures :vm, VMConfig - configures :chef, ChefConfig configures :vagrant, VagrantConfig def initialize diff --git a/lib/vagrant/provisioners/base.rb b/lib/vagrant/provisioners/base.rb new file mode 100644 index 000000000..cb7a650cf --- /dev/null +++ b/lib/vagrant/provisioners/base.rb @@ -0,0 +1,22 @@ +module Vagrant + module Provisioners + # The base class for a "provisioner." A provisioner is responsible for + # provisioning a Vagrant system. This has been abstracted out to provide + # support for multiple solutions such as Chef Solo, Chef Client, and + # Puppet. + class Base + include Vagrant::Util + + # This is the method called to "prepare" the provisioner. This is called + # before any actions are run by the action runner (see {Vagrant::Actions::Runner}). + # This can be used to setup shared folders, forward ports, etc. Whatever is + # necessary on a "meta" level. + def prepare; end + + # This is the method called to provision the system. This method + # is expected to do whatever necessary to provision the system (create files, + # SSH, etc.) + def provision!; end + end + end +end \ No newline at end of file diff --git a/lib/vagrant/provisioners/chef.rb b/lib/vagrant/provisioners/chef.rb new file mode 100644 index 000000000..b59a8f804 --- /dev/null +++ b/lib/vagrant/provisioners/chef.rb @@ -0,0 +1,102 @@ +module Vagrant + module Provisioners + # This class is a base class where the common functinality 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 + # This is the configuration which is available through `config.chef` + class ChefConfig < Vagrant::Config::Base + # Chef server specific config + attr_accessor :chef_server_url + attr_accessor :validation_key_path + attr_accessor :validation_client_name + attr_accessor :client_key_path + + # Chef solo specific config + attr_accessor :cookbooks_path + + # Shared config + attr_accessor :provisioning_path + attr_accessor :json + + def initialize + @validation_client_name = "chef-validator" + @client_key_path = "/etc/chef/client.pem" + + @cookbooks_path = "cookbooks" + @provisioning_path = "/tmp/vagrant-chef" + @json = { + :instance_role => "vagrant", + :run_list => ["recipe[vagrant_main]"] + } + end + + # Returns the run list for the provisioning + def run_list + json[:run_list] + end + + # Sets the run list to the specified value + def run_list=(value) + json[:run_list] = value + 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 to_json + # Overridden so that the 'json' key could be removed, since its just + # merged into the config anyways + data = instance_variables_hash + data.delete(:json) + data.to_json + end + end + + # Tell the Vagrant configure class about our custom configuration + Config.configures :chef, ChefConfig + + def prepare + raise Actions::ActionException.new("Vagrant::Provisioners::Chef is not a valid provisioner! Use ChefSolo or ChefServer instead.") + end + + def chown_provisioning_folder + logger.info "Setting permissions on chef provisioning folder..." + SSH.execute do |ssh| + ssh.exec!("sudo mkdir -p #{Vagrant.config.chef.provisioning_path}") + ssh.exec!("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}") + end + end + + def setup_json + logger.info "Generating chef JSON and uploading..." + + # Set up initial configuration + data = { + :config => Vagrant.config, + :directory => Vagrant.config.vm.project_directory, + } + + # 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!(Vagrant.config.chef.json) + + json = data.to_json + + SSH.upload!(StringIO.new(json), File.join(Vagrant.config.chef.provisioning_path, "dna.json")) + end + end + end +end \ No newline at end of file diff --git a/lib/vagrant/provisioners/chef_server.rb b/lib/vagrant/provisioners/chef_server.rb new file mode 100644 index 000000000..88e8a93cc --- /dev/null +++ b/lib/vagrant/provisioners/chef_server.rb @@ -0,0 +1,96 @@ +module Vagrant + module Provisioners + # This class implements provisioning via chef-client, allowing provisioning + # with a chef server. + class ChefServer < Chef + def prepare + if Vagrant.config.chef.validation_key_path.nil? + raise Actions::ActionException.new(<<-msg) +Chef server provisioning requires that the `config.chef.validation_key_path` configuration +be set to a path on your local machine of the validation key used to register the +VM with the chef server. +msg + elsif !File.file?(Vagrant.config.chef.validation_key_path) + raise Actions::ActionException.new(<<-msg) +The validation key set for `config.chef.validation_key_path` does not exist! This +file needs to exist so it can be uploaded to the virtual machine. It is +currently set to "#{Vagrant.config.chef.validation_key_path}" +msg + end + + if Vagrant.config.chef.chef_server_url.nil? + raise Actions::ActionException.new(<<-msg) +Chef server provisioning requires that the `config.chef.chef_server_url` be set to the +URL of your chef server. Examples include "http://12.12.12.12:4000" and +"http://myserver.com:4000" (the port of course can be different, but 4000 is the default) +msg + end + end + + def provision! + chown_provisioning_folder + create_client_key_folder + upload_validation_key + setup_json + setup_config + run_chef_client + end + + def create_client_key_folder + logger.info "Creating folder to hold client key..." + path = Pathname.new(Vagrant.config.chef.client_key_path) + + SSH.execute do |ssh| + ssh.exec!("sudo mkdir -p #{path.dirname}") + end + end + + def upload_validation_key + logger.info "Uploading chef client validation key..." + SSH.upload!(validation_key_path, guest_validation_key_path) + end + + def setup_config + solo_file = <<-solo +log_level :info +log_location STDOUT +ssl_verify_mode :verify_none +chef_server_url "#{Vagrant.config.chef.chef_server_url}" + +validation_client_name "#{Vagrant.config.chef.validation_client_name}" +validation_key "#{guest_validation_key_path}" +client_key "#{Vagrant.config.chef.client_key_path}" + +file_store_path "/srv/chef/file_store" +file_cache_path "/srv/chef/cache" + +pid_file "/var/run/chef/chef-client.pid" + +Mixlib::Log::Formatter.show_time = true +solo + + logger.info "Uploading chef-client configuration script..." + SSH.upload!(StringIO.new(solo_file), File.join(Vagrant.config.chef.provisioning_path, "client.rb")) + end + + def run_chef_client + logger.info "Running chef-client..." + SSH.execute do |ssh| + ssh.exec!("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-client -c client.rb -j dna.json") do |channel, data, stream| + # TODO: Very verbose. It would be easier to save the data and only show it during + # an error, or when verbosity level is set high + logger.info("#{stream}: #{data}") + end + end + end + + def validation_key_path + File.expand_path(Vagrant.config.chef.validation_key_path, Env.root_path) + end + + def guest_validation_key_path + File.join(Vagrant.config.chef.provisioning_path, "validation.pem") + end + end + end +end \ No newline at end of file diff --git a/lib/vagrant/provisioners/chef_solo.rb b/lib/vagrant/provisioners/chef_solo.rb new file mode 100644 index 000000000..df68c6a4c --- /dev/null +++ b/lib/vagrant/provisioners/chef_solo.rb @@ -0,0 +1,67 @@ +module Vagrant + module Provisioners + # This class implements provisioning via chef-solo. + class ChefSolo < Chef + def prepare + share_cookbook_folders + end + + def provision! + chown_provisioning_folder + setup_json + setup_solo_config + run_chef_solo + end + + def share_cookbook_folders + host_cookbook_paths.each_with_index do |cookbook, i| + Vagrant.config.vm.share_folder("vagrant-chef-solo-#{i}", cookbook_path(i), cookbook) + end + end + + def setup_solo_config + solo_file = <<-solo +file_cache_path "#{Vagrant.config.chef.provisioning_path}" +cookbook_path #{cookbooks_path} +solo + + logger.info "Uploading chef-solo configuration script..." + SSH.upload!(StringIO.new(solo_file), File.join(Vagrant.config.chef.provisioning_path, "solo.rb")) + end + + def run_chef_solo + logger.info "Running chef-solo..." + SSH.execute do |ssh| + ssh.exec!("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json") do |channel, data, stream| + # TODO: Very verbose. It would be easier to save the data and only show it during + # an error, or when verbosity level is set high + logger.info("#{stream}: #{data}") + end + end + end + + def host_cookbook_paths + cookbooks = Vagrant.config.chef.cookbooks_path + cookbooks = [cookbooks] unless cookbooks.is_a?(Array) + cookbooks.collect! { |cookbook| File.expand_path(cookbook, Env.root_path) } + return cookbooks + end + + def cookbook_path(i) + File.join(Vagrant.config.chef.provisioning_path, "cookbooks-#{i}") + end + + def cookbooks_path + result = [] + host_cookbook_paths.each_with_index do |host_path, i| + result << cookbook_path(i) + end + + # We're lucky that ruby's string and array syntax for strings is the + # same as JSON, so we can just convert to JSON here and use that + result = result.to_s if result.length == 1 + result.to_json + end + end + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 4c14f3b43..17dc61e40 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -42,6 +42,10 @@ class Test::Unit::TestCase config.package.name = 'vagrant' config.package.extension = '.box' + # Chef + config.chef.chef_server_url = "http://localhost:4000" + config.chef.validation_key_path = "validation.pem" + config.chef.client_key_path = "/zoo/foo/bar.pem" config.chef.cookbooks_path = "cookbooks" config.chef.provisioning_path = "/tmp/hobo-chef" config.chef.json = { diff --git a/test/vagrant/actions/vm/provision_test.rb b/test/vagrant/actions/vm/provision_test.rb index 64e6b808c..630296634 100644 --- a/test/vagrant/actions/vm/provision_test.rb +++ b/test/vagrant/actions/vm/provision_test.rb @@ -2,103 +2,107 @@ require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') class ProvisionActionTest < Test::Unit::TestCase setup do - @mock_vm, @vm, @action = mock_action(Vagrant::Actions::VM::Provision) - - Vagrant::SSH.stubs(:execute) - Vagrant::SSH.stubs(:upload!) - + @runner, @vm, @action = mock_action(Vagrant::Actions::VM::Provision) mock_config end - context "shared folders" do - should "setup shared folder on VM for the cookbooks" do - File.expects(:expand_path).with(Vagrant.config.chef.cookbooks_path, Vagrant::Env.root_path).returns("foo") - @action.expects(:cookbooks_path).returns("bar") - Vagrant.config.vm.expects(:share_folder).with("vagrant-provisioning", "bar", "foo").once - @action.prepare + context "initialization" do + should "have a nil provisioner by default" do + assert_nil @action.provisioner end end - context "cookbooks path" do - should "return the proper cookbook path" do - cookbooks_path = File.join(Vagrant.config.chef.provisioning_path, "cookbooks") - assert_equal cookbooks_path, @action.cookbooks_path + context "executing" do + should "do nothing if the provisioner is nil" do + @action.expects(:provisioner).returns(nil) + assert_nothing_raised { @action.execute! } + end + + should "call `provision!` on the provisioner" do + provisioner = mock("provisioner") + provisioner.expects(:provision!).once + @action.expects(:provisioner).twice.returns(provisioner) + @action.execute! end end - context "permissions on provisioning folder" do - should "chown the folder to the ssh user" do - ssh = mock("ssh") - ssh.expects(:exec!).with("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}") - Vagrant::SSH.expects(:execute).yields(ssh) - @action.chown_provisioning_folder - end - end - - context "generating and uploading json" do - def assert_json - Vagrant::SSH.expects(:upload!).with do |json, path| - data = JSON.parse(json.read) - yield data - true + context "preparing" do + context "with a nil provisioner" do + setup do + mock_config do |config| + config.vm.provisioner = nil + end end - @action.setup_json - end - - should "merge in the extra json specified in the config" do - Vagrant.config.chef.json = { :foo => "BAR" } - assert_json do |data| - assert_equal "BAR", data["foo"] + should "not set a provisioner if set to nil" do + @action.prepare + assert_nil @action.provisioner end end - should "add the directory as a special case to the JSON" do - assert_json do |data| - assert_equal Vagrant.config.vm.project_directory, data["vagrant"]["directory"] + context "with a Class provisioner" do + setup do + @instance = mock("instance") + @instance.stubs(:is_a?).with(Vagrant::Provisioners::Base).returns(true) + @instance.stubs(:prepare) + @klass = mock("klass") + @klass.stubs(:is_a?).with(Class).returns(true) + @klass.stubs(:new).returns(@instance) + + mock_config do |config| + config.vm.provisioner = @klass + end + end + + should "set the provisioner to an instantiation of the class" do + @klass.expects(:new).once.returns(@instance) + assert_nothing_raised { @action.prepare } + assert_equal @instance, @action.provisioner + end + + should "call prepare on the instance" do + @instance.expects(:prepare).once + @action.prepare + end + + should "raise an exception if the class is not a subclass of the provisioner base" do + @instance.expects(:is_a?).with(Vagrant::Provisioners::Base).returns(false) + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } end end - should "add the config to the JSON" do - assert_json do |data| - assert_equal Vagrant.config.vm.project_directory, data["vagrant"]["config"]["vm"]["project_directory"] + context "with a Symbol provisioner" do + def provisioner_expectation(symbol, provisioner) + mock_config do |config| + config.vm.provisioner = symbol + end + + instance = mock("instance") + instance.expects(:prepare).once + provisioner.expects(:new).returns(instance) + assert_nothing_raised { @action.prepare } + assert_equal instance, @action.provisioner end - end - should "upload a StringIO to dna.json" do - StringIO.expects(:new).with(anything).returns("bar") - File.expects(:join).with(Vagrant.config.chef.provisioning_path, "dna.json").once.returns("baz") - Vagrant::SSH.expects(:upload!).with("bar", "baz").once - @action.setup_json - end - end + should "raise an ActionException if its an unknown symbol" do + mock_config do |config| + config.vm.provisioner = :this_will_never_exist + end - context "generating and uploading chef solo configuration file" do - should "upload properly generate the configuration file using configuration data" do - expected_config = <<-config -file_cache_path "#{Vagrant.config.chef.provisioning_path}" -cookbook_path "#{@action.cookbooks_path}" -config + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } + end - StringIO.expects(:new).with(expected_config).once - @action.setup_solo_config - end + should "set :chef_solo to the ChefSolo provisioner" do + provisioner_expectation(:chef_solo, Vagrant::Provisioners::ChefSolo) + end - should "upload this file as solo.rb to the provisioning folder" do - @action.expects(:cookbooks_path).returns("cookbooks") - StringIO.expects(:new).returns("foo") - File.expects(:join).with(Vagrant.config.chef.provisioning_path, "solo.rb").once.returns("bar") - Vagrant::SSH.expects(:upload!).with("foo", "bar").once - @action.setup_solo_config - end - end - - context "running chef solo" do - should "cd into the provisioning directory and run chef solo" do - ssh = mock("ssh") - ssh.expects(:exec!).with("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json").once - Vagrant::SSH.expects(:execute).yields(ssh) - @action.run_chef_solo + should "set :chef_server to the ChefServer provisioner" do + provisioner_expectation(:chef_server, Vagrant::Provisioners::ChefServer) + end end end end diff --git a/test/vagrant/actions/vm/reload_test.rb b/test/vagrant/actions/vm/reload_test.rb index 9218390ff..4bd912d71 100644 --- a/test/vagrant/actions/vm/reload_test.rb +++ b/test/vagrant/actions/vm/reload_test.rb @@ -33,7 +33,8 @@ class ReloadActionTest < Test::Unit::TestCase should "add in the provisioning step if enabled" do mock_config do |config| - config.chef.enabled = true + # Dummy provisioner to test + config.vm.provisioner = "foo" end @default_order.push(Vagrant::Actions::VM::Provision) diff --git a/test/vagrant/actions/vm/up_test.rb b/test/vagrant/actions/vm/up_test.rb index 520c70194..c39dfedca 100644 --- a/test/vagrant/actions/vm/up_test.rb +++ b/test/vagrant/actions/vm/up_test.rb @@ -47,7 +47,7 @@ class UpActionTest < Test::Unit::TestCase should "add in the provisioning step if enabled" do mock_config do |config| - config.chef.enabled = true + config.vm.provisioner = "foo" end @default_order.push(Vagrant::Actions::VM::Provision) diff --git a/test/vagrant/config_test.rb b/test/vagrant/config_test.rb index 311296e3e..547e07cb3 100644 --- a/test/vagrant/config_test.rb +++ b/test/vagrant/config_test.rb @@ -117,18 +117,6 @@ class ConfigTest < Test::Unit::TestCase end end - context "chef config" do - setup do - @config = Vagrant::Config::ChefConfig.new - @config.json = "HEY" - end - - should "not include the 'json' key in the config dump" do - result = JSON.parse(@config.to_json) - assert !result.has_key?("json") - end - end - context "top config class" do setup do @configures_list = [] diff --git a/test/vagrant/provisioners/base_test.rb b/test/vagrant/provisioners/base_test.rb new file mode 100644 index 000000000..403665cc1 --- /dev/null +++ b/test/vagrant/provisioners/base_test.rb @@ -0,0 +1,27 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'test_helper') + +class BaseProvisionerTest < Test::Unit::TestCase + should "include the util class so subclasses have access to it" do + assert Vagrant::Provisioners::Base.include?(Vagrant::Util) + end + + context "base instance" do + setup do + @base = Vagrant::Provisioners::Base.new + end + + should "implement provision! which does nothing" do + assert_nothing_raised do + assert @base.respond_to?(:provision!) + @base.provision! + end + end + + should "implement prepare which does nothing" do + assert_nothing_raised do + assert @base.respond_to?(:prepare) + @base.prepare + end + end + end +end diff --git a/test/vagrant/provisioners/chef_server_test.rb b/test/vagrant/provisioners/chef_server_test.rb new file mode 100644 index 000000000..7212a3604 --- /dev/null +++ b/test/vagrant/provisioners/chef_server_test.rb @@ -0,0 +1,175 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'test_helper') + +class ChefServerProvisionerTest < Test::Unit::TestCase + setup do + @action = Vagrant::Provisioners::ChefServer.new + + Vagrant::SSH.stubs(:execute) + Vagrant::SSH.stubs(:upload!) + + mock_config + end + + context "provisioning" do + should "run the proper sequence of methods in order" do + prov_seq = sequence("prov_seq") + @action.expects(:chown_provisioning_folder).once.in_sequence(prov_seq) + @action.expects(:create_client_key_folder).once.in_sequence(prov_seq) + @action.expects(:upload_validation_key).once.in_sequence(prov_seq) + @action.expects(:setup_json).once.in_sequence(prov_seq) + @action.expects(:setup_config).once.in_sequence(prov_seq) + @action.expects(:run_chef_client).once.in_sequence(prov_seq) + @action.provision! + end + end + + context "preparing" do + setup do + File.stubs(:file?).returns(true) + end + + should "not raise an exception if validation_key_path is set" do + mock_config do |config| + config.chef.validation_key_path = "7" + end + + assert_nothing_raised { @action.prepare } + end + + should "raise an exception if validation_key_path is nil" do + mock_config do |config| + config.chef.validation_key_path = nil + end + + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } + end + + should "not raise an exception if validation_key_path does exist" do + mock_config do |config| + config.chef.validation_key_path = "7" + end + + File.expects(:file?).with(Vagrant.config.chef.validation_key_path).returns(true) + assert_nothing_raised { @action.prepare } + end + + should "raise an exception if validation_key_path doesn't exist" do + mock_config do |config| + config.chef.validation_key_path = "7" + end + + File.expects(:file?).with(Vagrant.config.chef.validation_key_path).returns(false) + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } + end + + should "not raise an exception if chef_server_url is set" do + mock_config do |config| + config.chef.chef_server_url = "7" + end + + assert_nothing_raised { @action.prepare } + end + + should "raise an exception if chef_server_url is nil" do + mock_config do |config| + config.chef.chef_server_url = nil + end + + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } + end + end + + context "creating the client key folder" do + setup do + @raw_path = "/foo/bar/baz.pem" + mock_config do |config| + config.chef.client_key_path = @raw_path + end + + @path = Pathname.new(@raw_path) + end + + should "create the folder using the dirname of the path" do + ssh = mock("ssh") + ssh.expects(:exec!).with("sudo mkdir -p #{@path.dirname}").once + Vagrant::SSH.expects(:execute).yields(ssh) + @action.create_client_key_folder + end + end + + context "uploading the validation key" do + should "upload the validation key to the provisioning path" do + @action.expects(:validation_key_path).once.returns("foo") + @action.expects(:guest_validation_key_path).once.returns("bar") + Vagrant::SSH.expects(:upload!).with("foo", "bar").once + @action.upload_validation_key + end + end + + context "the validation key path" do + should "expand the configured key path" do + result = mock("result") + File.expects(:expand_path).with(Vagrant.config.chef.validation_key_path, Vagrant::Env.root_path).once.returns(result) + assert_equal result, @action.validation_key_path + end + end + + context "the guest validation key path" do + should "be the provisioning path joined with validation.pem" do + result = mock("result") + File.expects(:join).with(Vagrant.config.chef.provisioning_path, "validation.pem").once.returns(result) + assert_equal result, @action.guest_validation_key_path + end + end + + context "generating and uploading chef client configuration file" do + setup do + @action.stubs(:guest_validation_key_path).returns("foo") + end + + should "upload properly generate the configuration file using configuration data" do + expected_config = <<-config +log_level :info +log_location STDOUT +ssl_verify_mode :verify_none +chef_server_url "#{Vagrant.config.chef.chef_server_url}" + +validation_client_name "#{Vagrant.config.chef.validation_client_name}" +validation_key "#{@action.guest_validation_key_path}" +client_key "#{Vagrant.config.chef.client_key_path}" + +file_store_path "/srv/chef/file_store" +file_cache_path "/srv/chef/cache" + +pid_file "/var/run/chef/chef-client.pid" + +Mixlib::Log::Formatter.show_time = true +config + + StringIO.expects(:new).with(expected_config).once + @action.setup_config + end + + should "upload this file as client.rb to the provisioning folder" do + StringIO.expects(:new).returns("foo") + File.expects(:join).with(Vagrant.config.chef.provisioning_path, "client.rb").once.returns("bar") + Vagrant::SSH.expects(:upload!).with("foo", "bar").once + @action.setup_config + end + end + + context "running chef client" do + should "cd into the provisioning directory and run chef client" do + ssh = mock("ssh") + ssh.expects(:exec!).with("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-client -c client.rb -j dna.json").once + Vagrant::SSH.expects(:execute).yields(ssh) + @action.run_chef_client + end + end +end diff --git a/test/vagrant/provisioners/chef_solo_test.rb b/test/vagrant/provisioners/chef_solo_test.rb new file mode 100644 index 000000000..9859ca771 --- /dev/null +++ b/test/vagrant/provisioners/chef_solo_test.rb @@ -0,0 +1,142 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'test_helper') + +class ChefSoloProvisionerTest < Test::Unit::TestCase + setup do + @action = Vagrant::Provisioners::ChefSolo.new + + Vagrant::SSH.stubs(:execute) + Vagrant::SSH.stubs(:upload!) + + mock_config + end + + context "preparing" do + should "share cookbook folders" do + @action.expects(:share_cookbook_folders).once + @action.prepare + end + end + + context "provisioning" do + should "run the proper sequence of methods in order" do + prov_seq = sequence("prov_seq") + @action.expects(:chown_provisioning_folder).once.in_sequence(prov_seq) + @action.expects(:setup_json).once.in_sequence(prov_seq) + @action.expects(:setup_solo_config).once.in_sequence(prov_seq) + @action.expects(:run_chef_solo).once.in_sequence(prov_seq) + @action.provision! + end + end + + context "sharing cookbook folders" do + setup do + @host_cookbook_paths = ["foo", "bar"] + @action.stubs(:host_cookbook_paths).returns(@host_cookbook_paths) + end + + should "share each cookbook folder" do + share_seq = sequence("share_seq") + @host_cookbook_paths.each_with_index do |cookbook, i| + Vagrant.config.vm.expects(:share_folder).with("vagrant-chef-solo-#{i}", @action.cookbook_path(i), cookbook).in_sequence(share_seq) + end + + @action.share_cookbook_folders + end + end + + context "host cookbooks paths" do + should "expand the path of the cookbooks relative to the environment root path" do + @cookbook = "foo" + @expanded = "bar" + File.expects(:expand_path).with(@cookbook, Vagrant::Env.root_path).returns(@expanded) + + mock_config do |config| + config.chef.cookbooks_path = @cookbook + end + + assert_equal [@expanded], @action.host_cookbook_paths + end + + should "return as an array if was originally a string" do + File.stubs(:expand_path).returns("foo") + + mock_config do |config| + config.chef.cookbooks_path = "foo" + end + + assert_equal ["foo"], @action.host_cookbook_paths + end + + should "return the array of cookbooks if its an array" do + cookbooks = ["foo", "bar"] + mock_config do |config| + config.chef.cookbooks_path = cookbooks + end + + expand_seq = sequence('expand_seq') + cookbooks.each do |cookbook| + File.expects(:expand_path).with(cookbook, Vagrant::Env.root_path).returns(cookbook) + end + + assert_equal cookbooks, @action.host_cookbook_paths + end + end + + context "cookbooks path" do + should "return a proper path to a single cookbook" do + expected = File.join(Vagrant.config.chef.provisioning_path, "cookbooks-5") + assert_equal expected, @action.cookbook_path(5) + end + + should "return array-representation of cookbook paths if multiple" do + @cookbooks = (0..5).inject([]) do |acc, i| + acc << @action.cookbook_path(i) + end + + mock_config do |config| + config.chef.cookbooks_path = @cookbooks + end + + assert_equal @cookbooks.to_json, @action.cookbooks_path + end + + should "return a single string representation if cookbook paths is single" do + @cookbooks = @action.cookbook_path(0) + + mock_config do |config| + config.chef.cookbooks_path = @cookbooks + end + + assert_equal @cookbooks.to_json, @action.cookbooks_path + end + end + + context "generating and uploading chef solo configuration file" do + should "upload properly generate the configuration file using configuration data" do + expected_config = <<-config +file_cache_path "#{Vagrant.config.chef.provisioning_path}" +cookbook_path #{@action.cookbooks_path} +config + + StringIO.expects(:new).with(expected_config).once + @action.setup_solo_config + end + + should "upload this file as solo.rb to the provisioning folder" do + @action.expects(:cookbooks_path).returns("cookbooks") + StringIO.expects(:new).returns("foo") + File.expects(:join).with(Vagrant.config.chef.provisioning_path, "solo.rb").once.returns("bar") + Vagrant::SSH.expects(:upload!).with("foo", "bar").once + @action.setup_solo_config + end + end + + context "running chef solo" do + should "cd into the provisioning directory and run chef solo" do + ssh = mock("ssh") + ssh.expects(:exec!).with("cd #{Vagrant.config.chef.provisioning_path} && sudo chef-solo -c solo.rb -j dna.json").once + Vagrant::SSH.expects(:execute).yields(ssh) + @action.run_chef_solo + end + end +end diff --git a/test/vagrant/provisioners/chef_test.rb b/test/vagrant/provisioners/chef_test.rb new file mode 100644 index 000000000..916dabc46 --- /dev/null +++ b/test/vagrant/provisioners/chef_test.rb @@ -0,0 +1,116 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'test_helper') + +class ChefProvisionerTest < Test::Unit::TestCase + setup do + @action = Vagrant::Provisioners::Chef.new + + Vagrant::SSH.stubs(:execute) + Vagrant::SSH.stubs(:upload!) + + mock_config + end + + context "preparing" do + should "raise an ActionException" do + assert_raises(Vagrant::Actions::ActionException) { + @action.prepare + } + end + end + + context "config" do + setup do + @config = Vagrant::Provisioners::Chef::ChefConfig.new + @config.run_list.clear + end + + should "not include the 'json' key in the config dump" do + result = JSON.parse(@config.to_json) + assert !result.has_key?("json") + end + + should "provide accessors to the run list" do + @config.run_list << "foo" + assert !@config.run_list.empty? + assert_equal ["foo"], @config.run_list + end + + should "provide a writer for the run list" do + data = mock("data") + + assert_nothing_raised { + @config.run_list = data + assert_equal data, @config.run_list + } + end + + should "add a recipe to the run list" do + @config.add_recipe("foo") + assert_equal "recipe[foo]", @config.run_list[0] + end + + should "not wrap the recipe in 'recipe[]' if it was in the name" do + @config.add_recipe("recipe[foo]") + assert_equal "recipe[foo]", @config.run_list[0] + end + + should "add a role to the run list" do + @config.add_role("user") + assert_equal "role[user]", @config.run_list[0] + end + + should "not wrap the role in 'role[]' if it was in the name" do + @config.add_role("role[user]") + assert_equal "role[user]", @config.run_list[0] + end + end + + context "permissions on provisioning folder" do + should "create and chown the folder to the ssh user" do + ssh_seq = sequence("ssh_seq") + ssh = mock("ssh") + ssh.expects(:exec!).with("sudo mkdir -p #{Vagrant.config.chef.provisioning_path}").once.in_sequence(ssh_seq) + ssh.expects(:exec!).with("sudo chown #{Vagrant.config.ssh.username} #{Vagrant.config.chef.provisioning_path}").once.in_sequence(ssh_seq) + Vagrant::SSH.expects(:execute).yields(ssh) + @action.chown_provisioning_folder + end + end + + context "generating and uploading json" do + def assert_json + Vagrant::SSH.expects(:upload!).with do |json, path| + data = JSON.parse(json.read) + yield data + true + end + + @action.setup_json + end + + should "merge in the extra json specified in the config" do + Vagrant.config.chef.json = { :foo => "BAR" } + assert_json do |data| + assert_equal "BAR", data["foo"] + end + end + + should "add the directory as a special case to the JSON" do + assert_json do |data| + assert_equal Vagrant.config.vm.project_directory, data["vagrant"]["directory"] + end + end + + should "add the config to the JSON" do + assert_json do |data| + assert_equal Vagrant.config.vm.project_directory, data["vagrant"]["config"]["vm"]["project_directory"] + end + end + + should "upload a StringIO to dna.json" do + StringIO.expects(:new).with(anything).returns("bar") + File.expects(:join).with(Vagrant.config.chef.provisioning_path, "dna.json").once.returns("baz") + Vagrant::SSH.expects(:upload!).with("bar", "baz").once + @action.setup_json + end + end +end