diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index c9cfa8327..3bed39f72 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -57,13 +57,23 @@ module VagrantPlugins return 0 if command.empty? opts = { - :error_check => true, - :error_class => Errors::ExecutionError, - :error_key => :execution_error, - :command => command, - :shell => :powershell + error_check: true, + error_class: Errors::ExecutionError, + error_key: :execution_error, + command: command, + shell: :powershell, + elevated: false }.merge(opts || {}) + if opts[:elevated] + path = File.expand_path("../scripts/elevated_shell.ps1", __FILE__) + command = Vagrant::Util::TemplateRenderer.render(path, options: { + username: shell.username, + password: shell.password, + command: command, + }) + end + output = shell.send(opts[:shell], command, &block) execution_output(output, opts) end @@ -121,7 +131,7 @@ module VagrantPlugins # The error classes expect the translation key to be _key, but that makes for an ugly # configuration parameter, so we set it here from `error_key` msg = "Command execution failed with an exit code of #{output[:exitcode]}" - error_opts = opts.merge(:_key => opts[:error_key], :message => msg) + error_opts = opts.merge(_key: opts[:error_key], message: msg) raise opts[:error_class], error_opts end end #WinRM class diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb new file mode 100644 index 000000000..97c5f822a --- /dev/null +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -0,0 +1,95 @@ +$command = "<%= options[:command] %>" +$user = '<%= options[:username] %>' +$password = '<%= options[:password] %>' + +$task_name = "WinRM_Elevated_Shell" +$out_file = "$env:SystemRoot\Temp\WinRM_Elevated_Shell.log" + +if (Test-Path $out_file) { + del $out_file +} + +$task_xml = @' + + + + + {user} + Password + HighestAvailable + + + + IgnoreNew + false + false + true + false + false + + true + false + + true + true + false + false + false + PT2H + 4 + + + + cmd + {arguments} + + + +'@ + +$bytes = [System.Text.Encoding]::Unicode.GetBytes($command) +$encoded_command = [Convert]::ToBase64String($bytes) +$arguments = "/c powershell.exe -EncodedCommand $encoded_command > $out_file 2>&1" + +$task_xml = $task_xml.Replace("{arguments}", $arguments) +$task_xml = $task_xml.Replace("{user}", $user) + +$schedule = New-Object -ComObject "Schedule.Service" +$schedule.Connect() +$task = $schedule.NewTask($null) +$task.XmlText = $task_xml +$folder = $schedule.GetFolder("\") +$folder.RegisterTaskDefinition($task_name, $task, 6, $user, $password, 1, $null) | Out-Null + +$registered_task = $folder.GetTask("\$task_name") +$registered_task.Run($null) | Out-Null + +$timeout = 10 +$sec = 0 +while ( (!($registered_task.state -eq 4)) -and ($sec -lt $timeout) ) { + Start-Sleep -s 1 + $sec++ +} + +# Read the entire file, but only write out new lines we haven't seen before +$numLinesRead = 0 +do { + Start-Sleep -m 100 + + if (Test-Path $out_file) { + $text = (get-content $out_file) + $numLines = ($text | Measure-Object -line).lines + $numLinesToRead = $numLines - $numLinesRead + + if ($numLinesToRead -gt 0) { + $text | select -first $numLinesToRead -skip $numLinesRead | ForEach { + Write-Host "$_" + } + $numLinesRead += $numLinesToRead + } + } +} while (!($registered_task.state -eq 3)) + +$exit_code = $registered_task.LastTaskResult +[System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) | Out-Null +exit $exit_code diff --git a/plugins/provisioners/chef/command_builder.rb b/plugins/provisioners/chef/command_builder.rb index 8a1e26b82..844d48864 100644 --- a/plugins/provisioners/chef/command_builder.rb +++ b/plugins/provisioners/chef/command_builder.rb @@ -1,15 +1,53 @@ module VagrantPlugins module Chef class CommandBuilder - def initialize(machine, config, client_type) - @machine = machine - @config = config - @client_type = client_type + def initialize(config, client_type, is_windows=false, is_ui_colored=false) + @client_type = client_type + @config = config + @is_windows = is_windows + @is_ui_colored = is_ui_colored if client_type != :solo && client_type != :client raise 'Invalid client_type, expected solo or client' end end + + def build_command + "#{command_env}#{chef_binary_path} #{chef_arguments}" + end + + protected + + def command_env + @config.binary_env ? "#{@config.binary_env} " : "" + end + + def chef_binary_path + binary_path = "chef-#{@client_type}" + if @config.binary_path + binary_path = guest_friendly_path(File.join(@config.binary_path, binary_path)) + end + binary_path + end + + def chef_arguments + chef_arguments = "-c #{provisioning_path("#{@client_type}.rb")}" + chef_arguments << " -j #{provisioning_path("dna.json")}" + chef_arguments << " #{@config.arguments}" if @config.arguments + chef_arguments << " --no-color" unless @is_ui_colored + chef_arguments.strip + end + + def provisioning_path(file) + guest_friendly_path(File.join(@config.provisioning_path, file)) + end + + def guest_friendly_path(path) + return path unless @is_windows + path.gsub!("/", "\\") + path = "c:#{path}" if path.start_with?("\\") + path + end end end end diff --git a/plugins/provisioners/chef/command_builder_linux.rb b/plugins/provisioners/chef/command_builder_linux.rb deleted file mode 100644 index d33ed667a..000000000 --- a/plugins/provisioners/chef/command_builder_linux.rb +++ /dev/null @@ -1,47 +0,0 @@ -module VagrantPlugins - module Chef - class CommandBuilderLinux < CommandBuilder - def build_command - if @client_type == :solo - return build_command_solo - else - return build_command_client - end - end - - protected - - def build_command_client - command_env = @config.binary_env ? "#{@config.binary_env} " : "" - command_args = @config.arguments ? " #{@config.arguments}" : "" - - binary_path = "chef-client" - binary_path ||= File.join(@config.binary_path, binary_path) - - return "#{command_env}#{binary_path} " + - "-c #{@config.provisioning_path}/client.rb " + - "-j #{@config.provisioning_path}/dna.json #{command_args}" - end - - def build_command_solo - options = [ - "-c #{@config.provisioning_path}/solo.rb", - "-j #{@config.provisioning_path}/dna.json" - ] - - if !@machine.env.ui.is_a?(Vagrant::UI::Colored) - options << "--no-color" - end - - command_env = @config.binary_env ? "#{@config.binary_env} " : "" - command_args = @config.arguments ? " #{@config.arguments}" : "" - - binary_path = "chef-solo" - binary_path ||= File.join(@config.binary_path, binary_path) - - return "#{command_env}#{binary_path} " + - "#{options.join(" ")} #{command_args}" - end - end - end -end diff --git a/plugins/provisioners/chef/command_builder_windows.rb b/plugins/provisioners/chef/command_builder_windows.rb deleted file mode 100644 index 8bd4f972e..000000000 --- a/plugins/provisioners/chef/command_builder_windows.rb +++ /dev/null @@ -1,83 +0,0 @@ -require "tempfile" - -require "vagrant/util/template_renderer" - -module VagrantPlugins - module Chef - class CommandBuilderWindows < CommandBuilder - def build_command - binary_path = "chef-#{@client_type}" - if @config.binary_path - binary_path = File.join(@config.binary_path, binary_path) - binary_path.gsub!("/", "\\") - binary_path = "c:#{binary_path}" if binary_path.start_with?("\\") - end - - chef_arguments = "-c #{provisioning_path("#{@client_type}.rb")}" - chef_arguments << " -j #{provisioning_path("dna.json")}" - chef_arguments << " #{@config.arguments}" if @config.arguments - - command_env = "" - command_env = "#{@config.binary_env} " if @config.binary_env - - task_ps1_path = provisioning_path("cheftask.ps1") - - opts = { - user: @machine.config.winrm.username, - pass: @machine.config.winrm.password, - chef_arguments: chef_arguments, - chef_binary_path: "#{command_env}#{binary_path}", - chef_stdout_log: provisioning_path("chef-#{@client_type}.log"), - chef_stderr_log: provisioning_path("chef-#{@client_type}.err.log"), - chef_task_exitcode: provisioning_path('cheftask.exitcode'), - chef_task_running: provisioning_path('cheftask.running'), - chef_task_ps1: task_ps1_path, - chef_task_run_ps1: provisioning_path('cheftaskrun.ps1'), - chef_task_xml: provisioning_path('cheftask.xml'), - } - - # Upload the files we'll need - render_and_upload( - "cheftaskrun.ps1", opts[:chef_task_run_ps1], opts) - render_and_upload( - "cheftask.xml", opts[:chef_task_xml], opts) - render_and_upload( - "cheftask.ps1", opts[:chef_task_ps1], opts) - - return <<-EOH - $old = Get-ExecutionPolicy; - Set-ExecutionPolicy Unrestricted -force; - #{task_ps1_path}; - Set-ExecutionPolicy $old -force - EOH - end - - protected - - def provisioning_path(file) - path = "#{@config.provisioning_path}/#{file}" - path.gsub!("/", "\\") - path = "c:#{path}" if path.start_with?("\\") - path - end - - def render_and_upload(template, dest, opts) - path = File.expand_path("../scripts/#{template}", __FILE__) - data = Vagrant::Util::TemplateRenderer.render(path, options) - - file = Tempfile.new("vagrant-chef") - file.binmode - file.write(data) - file.fsync - file.close - - @machine.communicate.upload(file.path, dest) - ensure - if file - file.close - file.unlink - end - end - end - end -end diff --git a/plugins/provisioners/chef/plugin.rb b/plugins/provisioners/chef/plugin.rb index 468f4b242..afe068047 100644 --- a/plugins/provisioners/chef/plugin.rb +++ b/plugins/provisioners/chef/plugin.rb @@ -6,8 +6,6 @@ module VagrantPlugins module Chef root = Pathname.new(File.expand_path("../", __FILE__)) autoload :CommandBuilder, root.join("command_builder") - autoload :CommandBuilderLinux, root.join("command_builder_linux") - autoload :CommandBuilderWindows, root.join("command_builder_windows") class Plugin < Vagrant.plugin("2") name "chef" diff --git a/plugins/provisioners/chef/provisioner/base.rb b/plugins/provisioners/chef/provisioner/base.rb index b1c071a6a..02a4dd52f 100644 --- a/plugins/provisioners/chef/provisioner/base.rb +++ b/plugins/provisioners/chef/provisioner/base.rb @@ -26,9 +26,7 @@ module VagrantPlugins # This returns the command to run Chef for the given client # type. def build_command(client) - builder_klass = CommandBuilderLinux - builder_klass = CommandBuilderWindows if windows? - builder = builder_klass.new(@machine, @config, client) + builder = CommandBuilder.new(@config, client, windows?, @machine.env.ui.is_a?(Vagrant::UI::Colored)) return builder.build_command end diff --git a/plugins/provisioners/chef/provisioner/chef_client.rb b/plugins/provisioners/chef/provisioner/chef_client.rb index 1de5f5d16..935cec6bf 100644 --- a/plugins/provisioners/chef/provisioner/chef_client.rb +++ b/plugins/provisioners/chef/provisioner/chef_client.rb @@ -58,6 +58,10 @@ module VagrantPlugins @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end + if @machine.guest.capability?(:wait_for_reboot) + @machine.guest.capability(:wait_for_reboot) + end + if windows? # This re-establishes our symbolic links if they were # created between now and a reboot @@ -75,7 +79,8 @@ module VagrantPlugins @machine.ui.info I18n.t("vagrant.provisioners.chef.running_client_again") end - exit_status = @machine.communicate.sudo(command, :error_check => false) do |type, data| + opts = { error_check: false, elevated: true } + exit_status = @machine.communicate.sudo(command, opts) do |type, data| # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red diff --git a/plugins/provisioners/chef/provisioner/chef_solo.rb b/plugins/provisioners/chef/provisioner/chef_solo.rb index 99037c1f0..db926e11c 100644 --- a/plugins/provisioners/chef/provisioner/chef_solo.rb +++ b/plugins/provisioners/chef/provisioner/chef_solo.rb @@ -134,6 +134,10 @@ module VagrantPlugins @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end + if @machine.guest.capability?(:wait_for_reboot) + @machine.guest.capability(:wait_for_reboot) + end + if windows? # This re-establishes our symbolic links if they were # created between now and a reboot @@ -151,7 +155,8 @@ module VagrantPlugins @machine.ui.info I18n.t("vagrant.provisioners.chef.running_solo_again") end - exit_status = @machine.communicate.sudo(command, :error_check => false) do |type, data| + opts = { error_check: false, elevated: true } + exit_status = @machine.communicate.sudo(command, opts) do |type, data| # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red diff --git a/plugins/provisioners/chef/scripts/cheftask.ps1.erb b/plugins/provisioners/chef/scripts/cheftask.ps1.erb deleted file mode 100644 index f025425ff..000000000 --- a/plugins/provisioners/chef/scripts/cheftask.ps1.erb +++ /dev/null @@ -1,48 +0,0 @@ -# kill the task so we can recreate it -schtasks /delete /tn "chef-solo" /f 2>&1 | out-null - -# Ensure the chef task running file doesn't exist from a previous failure -if (Test-Path "<%= options[:chef_task_running] %>") { - del "<%= options[:chef_task_running] %>" -} - -# schedule the task to run once in the far distant future -schtasks /create /tn 'chef-solo' /xml '<%= options[:chef_task_xml] %>' /ru '<%= options[:user] %>' /rp '<%= options[:pass] %>' | Out-Null - -# start the scheduled task right now -schtasks /run /tn "chef-solo" | Out-Null - -# wait for run_chef.ps1 to start or timeout after 1 minute -$timeoutSeconds = 60 -$elapsedSeconds = 0 -while ( (!(Test-Path "<%= options[:chef_task_running] %>")) -and ($elapsedSeconds -lt $timeoutSeconds) ) { - Start-Sleep -s 1 - $elapsedSeconds++ -} - -if ($elapsedSeconds -ge $timeoutSeconds) { - Write-Error "Timed out waiting for chef scheduled task to start" - exit -2 -} - -# read the entire file, but only write out new lines we haven't seen before -$numLinesRead = 0 -$success = $TRUE -while (Test-Path "<%= options[:chef_task_running] %>") { - Start-Sleep -m 100 - - if (Test-Path "<%= options[:chef_stdout_log] %>") { - $text = (get-content "<%= options[:chef_stdout_log] %>") - $numLines = ($text | Measure-Object -line).lines - $numLinesToRead = $numLines - $numLinesRead - - if ($numLinesToRead -gt 0) { - $text | select -first $numLinesToRead -skip $numLinesRead | ForEach { - Write-Host "$_" - } - $numLinesRead += $numLinesToRead - } - } -} - -exit Get-Content "<%= options[:chef_task_exitcode] %>" diff --git a/plugins/provisioners/chef/scripts/cheftask.xml.erb b/plugins/provisioners/chef/scripts/cheftask.xml.erb deleted file mode 100644 index 6b598ec4c..000000000 --- a/plugins/provisioners/chef/scripts/cheftask.xml.erb +++ /dev/null @@ -1,45 +0,0 @@ - - - - 2013-06-21T22:41:43 - Administrator - - - - 2045-01-01T12:00:00 - true - - - - - vagrant - Password - HighestAvailable - - - - IgnoreNew - false - false - true - false - false - - true - false - - true - true - false - false - false - PT2H - 4 - - - - powershell - -file <%= options[:chef_task_run_ps1] %> - - - diff --git a/plugins/provisioners/chef/scripts/cheftaskrun.ps1.erb b/plugins/provisioners/chef/scripts/cheftaskrun.ps1.erb deleted file mode 100644 index 0d4a43bf4..000000000 --- a/plugins/provisioners/chef/scripts/cheftaskrun.ps1.erb +++ /dev/null @@ -1,18 +0,0 @@ -$exitCode = -1 -Set-ExecutionPolicy Unrestricted -force; - -Try -{ - "running" | Out-File "<%= options[:chef_task_running] %>" - $process = (Start-Process "<%= options[:chef_binary_path] %>" -ArgumentList "<%= options[:chef_arguments] %>" -NoNewWindow -PassThru -Wait -RedirectStandardOutput "<%= options[:chef_stdout_log] %>" -RedirectStandardError "<%= options[:chef_stderr_log] %>") - $exitCode = $process.ExitCode -} -Finally -{ - $exitCode | Out-File "<%= options[:chef_task_exitcode] %>" - if (Test-Path "<%= options[:chef_task_running] %>") { - del "<%= options[:chef_task_running] %>" - } -} - -exit $exitCode diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index 84a4e8bd2..d6013be7c 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -17,6 +17,11 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do end end + before do + allow(shell).to receive(:username).and_return('vagrant') + allow(shell).to receive(:password).and_return('password') + end + describe ".ready?" do it "returns true if hostname command executes without error" do expect(shell).to receive(:powershell).with("hostname").and_return({ exitcode: 0 }) @@ -42,6 +47,16 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(subject.execute("dir")).to eq(0) end + it "wraps command in elevated shell script when elevated is true" do + expect(shell).to receive(:powershell) do |cmd| + expect(cmd).to include("$command = \"dir\"") + expect(cmd).to include("$user = 'vagrant'") + expect(cmd).to include("$password = 'password'") + expect(cmd).to include("New-Object -ComObject \"Schedule.Service\"") + end.and_return({ exitcode: 0 }) + expect(subject.execute("dir", { elevated: true })).to eq(0) + end + it "can use cmd shell" do expect(shell).to receive(:cmd).with(kind_of(String)).and_return({ exitcode: 0 }) expect(subject.execute("dir", { :shell => :cmd })).to eq(0) diff --git a/test/unit/plugins/provisioners/chef/command_builder_spec.rb b/test/unit/plugins/provisioners/chef/command_builder_spec.rb new file mode 100644 index 000000000..3fb53693b --- /dev/null +++ b/test/unit/plugins/provisioners/chef/command_builder_spec.rb @@ -0,0 +1,105 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/provisioners/chef/command_builder") + +describe VagrantPlugins::Chef::CommandBuilder do + + let(:machine) { double("machine") } + let(:chef_config) { double("chef_config") } + + before(:each) do + allow(chef_config).to receive(:provisioning_path).and_return('/tmp/vagrant-chef-1') + allow(chef_config).to receive(:arguments).and_return(nil) + allow(chef_config).to receive(:binary_env).and_return(nil) + allow(chef_config).to receive(:binary_path).and_return(nil) + allow(chef_config).to receive(:binary_env).and_return(nil) + end + + describe '.initialize' do + it 'should raise when chef type is not client or solo' do + expect { VagrantPlugins::Chef::CommandBuilder.new(chef_config, :client_bad) }. + to raise_error + end + end + + describe 'build_command' do + describe 'windows' do + subject do + VagrantPlugins::Chef::CommandBuilder.new(chef_config, :client, true) + end + + it "executes the chef-client in PATH by default" do + expect(subject.build_command()).to match(/^chef-client/) + end + + it "executes the chef-client using full path if binary_path is specified" do + allow(chef_config).to receive(:binary_path).and_return( + "c:\\opscode\\chef\\bin\\chef-client") + expect(subject.build_command()).to match(/^c:\\opscode\\chef\\bin\\chef-client\\chef-client/) + end + + it "builds a guest friendly client.rb path" do + expect(subject.build_command()).to include( + '-c c:\\tmp\\vagrant-chef-1\\client.rb') + end + + it "builds a guest friendly solo.json path" do + expect(subject.build_command()).to include( + '-j c:\\tmp\\vagrant-chef-1\\dna.json') + end + + it 'includes Chef arguments if specified' do + allow(chef_config).to receive(:arguments).and_return("-l DEBUG") + expect(subject.build_command()).to include( + '-l DEBUG') + end + + it 'includes --no-color if UI is not colored' do + expect(subject.build_command()).to include( + ' --no-color') + end + end + + describe 'linux' do + subject do + VagrantPlugins::Chef::CommandBuilder.new(chef_config, :client, false) + end + + it "executes the chef-client in PATH by default" do + expect(subject.build_command()).to match(/^chef-client/) + end + + it "executes the chef-client using full path if binary_path is specified" do + allow(chef_config).to receive(:binary_path).and_return( + "/opt/chef/chef-client") + expect(subject.build_command()).to match(/^\/opt\/chef\/chef-client/) + end + + it "builds a guest friendly client.rb path" do + expect(subject.build_command()).to include( + '-c /tmp/vagrant-chef-1/client.rb') + end + + it "builds a guest friendly solo.json path" do + expect(subject.build_command()).to include( + '-j /tmp/vagrant-chef-1/dna.json') + end + + it 'includes Chef arguments if specified' do + allow(chef_config).to receive(:arguments).and_return("-l DEBUG") + expect(subject.build_command()).to include( + '-l DEBUG') + end + + it 'includes --no-color if UI is not colored' do + expect(subject.build_command()).to include( + ' --no-color') + end + + it 'includes environment variables if specified' do + allow(chef_config).to receive(:binary_env).and_return("ENVVAR=VAL") + expect(subject.build_command()).to match(/^ENVVAR=VAL /) + end + end + end +end