diff --git a/README.md b/README.md index a79d4840..79f74ce7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ consider the [vagrant-wrapper][vagrant_wrapper] gem which helps manage both styles of Vagrant installations ([background details][vagrant_wrapper_background]). +If you are creating Windows VMs over a WinRM Transport, then the +[vagrant-winrm][vagrant_winrm] Vagrant plugin must be installed. As a +consequence, the minimum version of Vagrant required is 1.6 or higher. + ### Virtualbox and/or VMware Fusion/Workstation Currently this driver supports VirtualBox and VMware Fusion/Workstation. @@ -442,6 +446,7 @@ Apache 2.0 (see [LICENSE][license]) [vagrant_config_vmware]: http://docs.vagrantup.com/v2/vmware/configuration.html [vagrant_providers]: http://docs.vagrantup.com/v2/providers/index.html [vagrant_versioning]: https://docs.vagrantup.com/v2/boxes/versioning.html +[vagrant_winrm]: https://github.com/criteo/vagrant-winrm [vagrant_wrapper]: https://github.com/org-binbab/gem-vagrant-wrapper [vagrant_wrapper_background]: https://github.com/org-binbab/gem-vagrant-wrapper#background---aka-the-vagrant-gem-enigma [vmware_plugin]: http://www.vagrantup.com/vmware diff --git a/lib/kitchen/driver/vagrant.rb b/lib/kitchen/driver/vagrant.rb index aa4fc727..6966930d 100644 --- a/lib/kitchen/driver/vagrant.rb +++ b/lib/kitchen/driver/vagrant.rb @@ -48,10 +48,6 @@ class Vagrant < Kitchen::Driver::Base default_config :network, [] - default_config :password do |driver| - "vagrant" if driver.winrm_transport? - end - default_config :pre_create_command, nil default_config :provision, false @@ -64,10 +60,6 @@ class Vagrant < Kitchen::Driver::Base default_config :synced_folders, [] - default_config :username do |driver| - "vagrant" if driver.winrm_transport? - end - default_config :vagrantfile_erb, File.join(File.dirname(__FILE__), "../../../templates/Vagrantfile.erb") expand_path_for :vagrantfile_erb @@ -176,6 +168,18 @@ def winrm_transport? WEBSITE = "http://www.vagrantup.com/downloads.html".freeze MIN_VER = "1.1.0".freeze + class << self + + # @return [true,false] whether or not the vagrant-winrm plugin is + # installed + # @api private + attr_accessor :winrm_plugin_passed + + # @return [String] the version of Vagrant installed on the workstation + # @api private + attr_accessor :vagrant_version + end + # Retuns a list of Vagrant base boxes produced by the Bento project # (https://github.com/chef/bento). # @@ -254,6 +258,19 @@ def finalize_vm_hostname! end end + # Loads any required third party Ruby libraries or runs any shell out + # commands to prepare the plugin. This method will be called in the + # context of the main thread of execution and so does not necessarily + # have to be thread safe. + # + # @raise [ClientError] if any library loading fails or any of the + # dependency requirements cannot be satisfied + # @api private + def load_needed_dependencies! + super + verify_winrm_plugin if winrm_transport? + end + # Renders the Vagrantfile ERb template. # # @return [String] the contents for a Vagrantfile @@ -332,19 +349,14 @@ def run_vagrant_up # @param state [Hash] mutable instance state # @api private def update_state(state) - hash = vagrant_ssh_config + hash = winrm_transport? ? vagrant_config(:winrm) : vagrant_config(:ssh) state[:hostname] = hash["HostName"] - - if winrm_transport? - state[:username] = config[:username] - state[:password] = config[:password] - else - state[:username] = hash["User"] - state[:ssh_key] = hash["IdentityFile"] - state[:port] = hash["Port"] - state[:proxy_command] = hash["ProxyCommand"] if hash["ProxyCommand"] - end + state[:port] = hash["Port"] + state[:username] = hash["User"] + state[:password] = hash["Password"] if hash["Password"] + state[:ssh_key] = hash["IdentityFile"] if hash["IdentityFile"] + state[:proxy_command] = hash["ProxyCommand"] if hash["ProxyCommand"] end # @return [String] full local path to the directory containing the @@ -357,11 +369,13 @@ def vagrant_root ) end + # @param type [Symbol] either `:ssh` or `:winrm` # @return [Hash] key/value pairs resulting from parsing a - # `vagrant ssh-config` local command invocation + # `vagrant ssh-config` or `vagrant winrm-config` local command + # invocation # @api private - def vagrant_ssh_config - lines = run_silently("vagrant ssh-config").split("\n").map do |line| + def vagrant_config(type) + lines = run_silently("vagrant #{type}-config").split("\n").map do |line| tokens = line.strip.partition(" ") [tokens.first, tokens.last.gsub(/"/, "")] end @@ -372,12 +386,43 @@ def vagrant_ssh_config # @raise [UserError] if the `vagrant` command can not be found locally # @api private def vagrant_version - @version ||= run_silently("vagrant --version", :cwd => Dir.pwd). - chomp.split(" ").last + self.class.vagrant_version ||= run_silently( + "vagrant --version", :cwd => Dir.pwd).chomp.split(" ").last rescue Errno::ENOENT raise UserError, "Vagrant #{MIN_VER} or higher is not installed." \ " Please download a package from #{WEBSITE}." end + + # Verify that the vagrant-winrm plugin is installed and a suitable + # version of Vagrant is installed + # + # @api private + def verify_winrm_plugin + if Gem::Version.new(vagrant_version) < Gem::Version.new("1.6") + raise UserError, "Detected an old version of Vagrant " \ + "(#{vagrant_version}) that cannot support the vagrant-winrm " \ + "Vagrant plugin." \ + " Please upgrade to version 1.6 or higher from #{WEBSITE}." + end + + if !winrm_plugin_installed? + raise UserError, "WinRM Transport requires the vagrant-winrm " \ + "Vagrant plugin to properly communicate with this Vagrant VM. " \ + "Please install this plugin with: " \ + "`vagrant plugin install vagrant-winrm' and try again." + end + end + + # @return [true,false] whether or not the vagrant-winrm plugin is + # installed + # @api private + def winrm_plugin_installed? + return true if self.class.winrm_plugin_passed + + self.class.winrm_plugin_passed = run_silently( + "vagrant plugin list", :cwd => Dir.pwd). + split("\n").find { |line| line =~ /^vagrant-winrm\s+/ } + end end end end diff --git a/spec/kitchen/driver/vagrant_spec.rb b/spec/kitchen/driver/vagrant_spec.rb index dd03c9fc..9f24bc13 100644 --- a/spec/kitchen/driver/vagrant_spec.rb +++ b/spec/kitchen/driver/vagrant_spec.rb @@ -63,6 +63,11 @@ before { stub_const("ENV", env) } + after do + driver_object.class.send(:winrm_plugin_passed=, nil) + driver_object.class.send(:vagrant_version=, nil) + end + describe "configuration" do context "for known bento platform names" do @@ -328,32 +333,6 @@ expect(driver[:vm_hostname]).to eq("this-is-a--k") end end - - context "for non-WinRM-based transports" do - - before { allow(transport).to receive(:name).and_return("Coolness") } - - it "sets :username to nil by default" do - expect(driver[:username]).to eq(nil) - end - - it "sets :password to nil by default" do - expect(driver[:password]).to eq(nil) - end - end - - context "for WinRM-based transports" do - - before { allow(transport).to receive(:name).and_return("WinRM") } - - it "sets :username to vagrant by default" do - expect(driver[:username]).to eq("vagrant") - end - - it "sets :password to vagrant by default" do - expect(driver[:password]).to eq("vagrant") - end - end end describe "#verify_dependencies" do @@ -373,7 +352,7 @@ end it "raises a UserError for a missing Vagrant command" do - allow(driver).to receive(:run_command). + allow(driver_object).to receive(:run_command). with("vagrant --version", any_args).and_raise(Errno::ENOENT) expect { driver.verify_dependencies }.to raise_error( @@ -382,6 +361,69 @@ end end + describe "#load_needed_dependencies!" do + + describe "with winrm transport" do + + before { allow(transport).to receive(:name).and_return("WinRM") } + + it "old version of Vagrant raises UserError" do + with_vagrant("1.5.0") + + expect { instance }.to raise_error( + Kitchen::Error, /Please upgrade to version 1.6 or higher/ + ) + end + + it "modern vagrant without plugin installed raises UserError" do + with_modern_vagrant + allow(driver_object).to receive(:run_command). + with("vagrant plugin list", any_args).and_return("nope (1.2.3)") + + expect { instance }.to raise_error( + Kitchen::Error, /vagrant plugin install vagrant-winrm/ + ) + end + + it "modern vagrant with plugin installed succeeds" do + with_modern_vagrant + allow(driver_object).to receive(:run_command). + with("vagrant plugin list", any_args). + and_return("vagrant-winrm (1.2.3)") + + instance + end + end + + describe "without winrm transport" do + + before { allow(transport).to receive(:name).and_return("Anything") } + + it "old version of Vagrant succeeds" do + with_vagrant("1.5.0") + + instance + end + + it "modern vagrant without plugin installed succeeds" do + with_modern_vagrant + allow(driver_object).to receive(:run_command). + with("vagrant plugin list", any_args).and_return("nope (1.2.3)") + + instance + end + + it "modern vagrant with plugin installed succeeds" do + with_modern_vagrant + allow(driver_object).to receive(:run_command). + with("vagrant plugin list", any_args). + and_return("vagrant-winrm (1.2.3)") + + instance + end + end + end + describe "#create" do let(:cmd) { driver.create(state) } @@ -480,35 +522,40 @@ describe "for state" do - let(:output) do - <<-OUTPUT.gsub(/^ {10}/, "") - Host hehe - HostName 192.168.32.64 - User vagrant - Port 2022 - UserKnownHostsFile /dev/null - StrictHostKeyChecking no - PasswordAuthentication no - IdentityFile /path/to/private_key - IdentitiesOnly yes - LogLevel FATAL - OUTPUT - end + context "for non-WinRM-based transports" do - before do - allow(driver).to receive(:run_command). - with("vagrant ssh-config", any_args).and_return(output) - end + let(:output) do + <<-OUTPUT.gsub(/^ {10}/, "") + Host hehe + HostName 192.168.32.64 + User vagrant + Port 2022 + UserKnownHostsFile /dev/null + StrictHostKeyChecking no + PasswordAuthentication no + IdentityFile /path/to/private_key + IdentitiesOnly yes + LogLevel FATAL + OUTPUT + end - it "sets :hostname from ssh-config" do - cmd + before do + allow(transport).to receive(:name).and_return("Coolness") + allow(driver).to receive(:run_command). + with("vagrant ssh-config", any_args).and_return(output) + end - expect(state).to include(:hostname => "192.168.32.64") - end + it "sets :hostname from ssh-config" do + cmd - context "for non-WinRM-based transports" do + expect(state).to include(:hostname => "192.168.32.64") + end + + it "sets :port from ssh-config" do + cmd - before { allow(transport).to receive(:name).and_return("Coolness") } + expect(state).to include(:port => "2022") + end it "sets :username from ssh-config" do cmd @@ -516,16 +563,23 @@ expect(state).to include(:username => "vagrant") end - it "sets :ssh_key from ssh-config" do + it "does not set :password by default" do cmd - expect(state).to include(:ssh_key => "/path/to/private_key") + expect(state.keys).to_not include(:password) end - it "sets :port from ssh-config" do + it "sets :password if Password is in ssh-config" do + output.concat(" Password yep\n") cmd - expect(state).to include(:port => "2022") + expect(state).to include(:password => "yep") + end + + it "sets :ssh_key from ssh-config" do + cmd + + expect(state).to include(:ssh_key => "/path/to/private_key") end it "does not set :proxy_command by default" do @@ -544,20 +598,44 @@ context "for WinRM-based transports" do - before { allow(transport).to receive(:name).and_return("WinRM") } + let(:output) do + <<-OUTPUT.gsub(/^ {10}/, "") + Host hehe + HostName 192.168.32.64 + User vagrant + Password yep + Port 9999 + OUTPUT + end - it "sets :username from config" do - config[:username] = "winuser" + before do + allow(transport).to receive(:name).and_return("WinRM") + allow(driver).to receive(:run_command). + with("vagrant winrm-config", any_args).and_return(output) + end + + it "sets :hostname from winrm-config" do cmd - expect(state).to include(:username => "winuser") + expect(state).to include(:hostname => "192.168.32.64") end - it "sets :password from config" do - config[:password] = "mysecret" + it "sets :port from winrm-config" do cmd - expect(state).to include(:password => "mysecret") + expect(state).to include(:port => "9999") + end + + it "sets :username from winrm-config" do + cmd + + expect(state).to include(:username => "vagrant") + end + + it "sets :password from winrm-config" do + cmd + + expect(state).to include(:password => "yep") end end end @@ -1226,13 +1304,16 @@ def debug_lines end def with_modern_vagrant - allow(driver).to receive(:run_command). - with("vagrant --version", any_args).and_return("Vagrant 1.7.2") + with_vagrant("1.7.2") end def with_unsupported_vagrant - allow(driver).to receive(:run_command). - with("vagrant --version", any_args).and_return("Vagrant 1.0.5") + with_vagrant("1.0.5") + end + + def with_vagrant(version) + allow(driver_object).to receive(:run_command). + with("vagrant --version", any_args).and_return("Vagrant #{version}") end def regexify(str, line = :whole_line)