From c60a020096e576243e703517511d886fd461a925 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Wed, 27 Aug 2014 12:17:30 -0700 Subject: [PATCH 1/6] adds a ps command to vagrant that drops the user into a remote powershell shell --- plugins/commands/ps/command.rb | 116 ++++++++++++++++++ plugins/commands/ps/errors.rb | 18 +++ plugins/commands/ps/plugin.rb | 31 +++++ .../commands/ps/scripts/enable_psremoting.ps1 | 60 +++++++++ .../ps/scripts/reset_trustedhosts.ps1 | 12 ++ plugins/hosts/windows/cap/ps.rb | 50 ++++++++ plugins/hosts/windows/plugin.rb | 5 + templates/locales/command_ps.yml | 16 +++ 8 files changed, 308 insertions(+) create mode 100644 plugins/commands/ps/command.rb create mode 100644 plugins/commands/ps/errors.rb create mode 100644 plugins/commands/ps/plugin.rb create mode 100644 plugins/commands/ps/scripts/enable_psremoting.ps1 create mode 100644 plugins/commands/ps/scripts/reset_trustedhosts.ps1 create mode 100644 plugins/hosts/windows/cap/ps.rb create mode 100644 templates/locales/command_ps.yml diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb new file mode 100644 index 000000000..33eeec253 --- /dev/null +++ b/plugins/commands/ps/command.rb @@ -0,0 +1,116 @@ +require "optparse" + +require_relative "../../communicators/winrm/helper" + +module VagrantPlugins + module CommandPS + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "connects to machine via powershell remoting" + end + + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant ps [-- extra ps args]" + + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-c", "--command COMMAND", "Execute a powershell command directly") do |c| + options[:command] = c + end + end + + # Parse out the extra args to send to the ps session, which + # is everything after the "--" + split_index = @argv.index("--") + if split_index + options[:extra_args] = @argv.drop(split_index + 1) + @argv = @argv.take(split_index) + end + + # Parse the options and return if we don't have any target. + argv = parse_options(opts) + return if !argv + + # Check if the host even supports ps remoting + raise Errors::HostUnsupported if !@env.host.capability?(:ps_client) + + # Execute ps session if we can + with_target_vms(argv, single_target: true) do |machine| + if !machine.communicate.ready? + raise Vagrant::Errors::VMNotCreatedError + end + + if machine.config.vm.communicator != :winrm #|| !machine.provider.capability?(:winrm_info) + raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady + end + + if !options[:command].nil? + out_code = machine.communicate.execute options[:command] + if out_code == 0 + machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") + end + break + end + + ps_info = VagrantPlugins::CommunicatorWinRM::Helper.winrm_info(machine) + ps_info[:username] = machine.config.winrm.username + ps_info[:password] = machine.config.winrm.password + # Extra arguments if we have any + ps_info[:extra_args] = options[:extra_args] + + result = ready_ps_remoting_for machine, ps_info + + machine.ui.detail( + "Creating powershell session to #{ps_info[:host]}:#{ps_info[:port]}") + machine.ui.detail("Username: #{ps_info[:username]}") + + begin + @env.host.capability(:ps_client, ps_info) + ensure + if !result["PreviousTrustedHosts"].nil? + reset_ps_remoting_for machine, ps_info + end + end + end + end + + def ready_ps_remoting_for(machine, ps_info) + machine.ui.output(I18n.t("vagrant_ps.detecting")) + script_path = File.expand_path("../scripts/enable_psremoting.ps1", __FILE__) + args = [] + args << "-hostname" << ps_info[:host] + args << "-port" << ps_info[:port].to_s + args << "-username" << ps_info[:username] + args << "-password" << ps_info[:password] + result = Vagrant::Util::PowerShell.execute(script_path, *args) + if result.exit_code != 0 + raise Errors::PowershellError, + script: script_path, + stderr: result.stderr + end + + result_output = JSON.parse(result.stdout) + raise Errors::PSRemotingUndetected if !result_output["Success"] + result_output + end + + def reset_ps_remoting_for(machine, ps_info) + machine.ui.output(I18n.t("vagrant_ps.reseting")) + script_path = File.expand_path("../scripts/reset_trustedhosts.ps1", __FILE__) + args = [] + args << "-hostname" << ps_info[:host] + result = Vagrant::Util::PowerShell.execute(script_path, *args) + if result.exit_code != 0 + raise Errors::PowershellError, + script: script_path, + stderr: result.stderr + end + end + end + end +end diff --git a/plugins/commands/ps/errors.rb b/plugins/commands/ps/errors.rb new file mode 100644 index 000000000..c614fb87a --- /dev/null +++ b/plugins/commands/ps/errors.rb @@ -0,0 +1,18 @@ +module VagrantPlugins + module CommandPS + module Errors + # A convenient superclass for all our errors. + class PSError < Vagrant::Errors::VagrantError + error_namespace("vagrant_ps.errors") + end + + class HostUnsupported < PSError + error_key(:host_unsupported) + end + + class PSRemotingUndetected < PSError + error_key(:ps_remoting_undetected) + end + end + end +end diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb new file mode 100644 index 000000000..56b84b3c5 --- /dev/null +++ b/plugins/commands/ps/plugin.rb @@ -0,0 +1,31 @@ +require "vagrant" + +module VagrantPlugins + module CommandPS + autoload :Errors, File.expand_path("../errors", __FILE__) + + class Plugin < Vagrant.plugin("2") + name "ps command" + description <<-DESC + The ps command opens a remote powershell session to the + machine if it supports powershell remoting. + DESC + + command("ps") do + require File.expand_path("../command", __FILE__) + init! + Command + end + + protected + + def self.init! + return if defined?(@_init) + I18n.load_path << File.expand_path( + "templates/locales/command_ps.yml", Vagrant.source_root) + I18n.reload! + @_init = true + end + end + end +end diff --git a/plugins/commands/ps/scripts/enable_psremoting.ps1 b/plugins/commands/ps/scripts/enable_psremoting.ps1 new file mode 100644 index 000000000..4d7ded704 --- /dev/null +++ b/plugins/commands/ps/scripts/enable_psremoting.ps1 @@ -0,0 +1,60 @@ +Param( + [string]$hostname, + [string]$port, + [string]$username, + [string]$password +) +# If we are in this script, we know basic winrm is working +# If the user is not using a domain acount and chances are +# they are not, PS Remoting will not work if the guest is not +# listed in the trusted hosts. + +$encrypted_password = ConvertTo-SecureString $password -asplaintext -force +$creds = New-Object System.Management.Automation.PSCredential ( + "$hostname\\$username", $encrypted_password) + +$result = @{ + Success = $false + PreviousTrustedHosts = $null +} +try { + invoke-command -computername $hostname ` + -Credential $creds ` + -Port $port ` + -ScriptBlock {} ` + -ErrorAction Stop + $result.Success = $true +} catch{} + +if(!$result.Success) { + $newHosts = @() + $result.PreviousTrustedHosts=( + Get-Item "wsman:\localhost\client\trustedhosts").Value + $hostArray=$result.PreviousTrustedHosts.Split(",").Trim() + if($hostArray -contains "*") { + $result.PreviousTrustedHosts = $null + } + elseif(!($hostArray -contains $hostname)) { + $strNewHosts = $hostname + if($result.PreviousTrustedHosts.Length -gt 0){ + $strNewHosts = $result.PreviousTrustedHosts + "," + $strNewHosts + } + Set-Item -Path "wsman:\localhost\client\trustedhosts" ` + -Value $strNewHosts -Force + + try { + invoke-command -computername $hostname ` + -Credential $creds ` + -Port $port ` + -ScriptBlock {} ` + -ErrorAction Stop + $result.Success = $true + } catch{ + Set-Item -Path "wsman:\localhost\client\trustedhosts" ` + -Value $result.PreviousTrustedHosts -Force + $result.PreviousTrustedHosts = $null + } + } +} + +Write-Output $(ConvertTo-Json $result) diff --git a/plugins/commands/ps/scripts/reset_trustedhosts.ps1 b/plugins/commands/ps/scripts/reset_trustedhosts.ps1 new file mode 100644 index 000000000..5865a4e77 --- /dev/null +++ b/plugins/commands/ps/scripts/reset_trustedhosts.ps1 @@ -0,0 +1,12 @@ +Param( + [string]$hostname +) + +$trustedHosts = ( + Get-Item "wsman:\localhost\client\trustedhosts").Value.Replace( + $hostname, '') +$trustedHosts = $trustedHosts.Replace(",,","") +if($trustedHosts.EndsWith(",")){ + $trustedHosts = $trustedHosts.Substring(0,$trustedHosts.length-1) +} +Set-Item "wsman:\localhost\client\trustedhosts" -Value $trustedHosts -Force \ No newline at end of file diff --git a/plugins/hosts/windows/cap/ps.rb b/plugins/hosts/windows/cap/ps.rb new file mode 100644 index 000000000..939235915 --- /dev/null +++ b/plugins/hosts/windows/cap/ps.rb @@ -0,0 +1,50 @@ +require "pathname" +require "tmpdir" + +require "vagrant/util/subprocess" + +module VagrantPlugins + module HostWindows + module Cap + class PS + def self.ps_client(env, ps_info) + logger = Log4r::Logger.new("vagrant::hosts::windows") + + command = <<-EOS + $plain_password = "#{ps_info[:password]}" + $username = "#{ps_info[:username]}" + $port = "#{ps_info[:port]}" + $hostname = "#{ps_info[:host]}" + $password = ConvertTo-SecureString $plain_password -asplaintext -force + $creds = New-Object System.Management.Automation.PSCredential ("$hostname\\$username", $password) + function prompt { kill $PID } + Enter-PSSession -ComputerName $hostname -Credential $creds -Port $port + EOS + + logger.debug("Starting remote powershell with command:\n#{command}") + command = command.chars.to_a.join("\x00").chomp + command << "\x00" unless command[-1].eql? "\x00" + if(defined?(command.encode)) + command = command.encode('ASCII-8BIT') + command = Base64.strict_encode64(command) + else + command = Base64.encode64(command).chomp + end + + args = ["-NoProfile"] + args << "-ExecutionPolicy" + args << "Bypass" + args << "-NoExit" + args << "-EncodedCommand" + args << command + if ps_info[:extra_args] + args << ps_info[:extra_args] + end + + # Launch it + Vagrant::Util::Subprocess.execute("powershell", *args) + end + end + end + end +end diff --git a/plugins/hosts/windows/plugin.rb b/plugins/hosts/windows/plugin.rb index fe3b28923..97e8ad134 100644 --- a/plugins/hosts/windows/plugin.rb +++ b/plugins/hosts/windows/plugin.rb @@ -20,6 +20,11 @@ module VagrantPlugins require_relative "cap/rdp" Cap::RDP end + + host_capability("windows", "ps_client") do + require_relative "cap/ps" + Cap::PS + end end end end diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml new file mode 100644 index 000000000..e30e5246c --- /dev/null +++ b/templates/locales/command_ps.yml @@ -0,0 +1,16 @@ +en: + vagrant_ps: + detecting: |- + Detecting if a remote powershell connection can be made with the guest... + reseting: |- + Reseting WinRM TrustedHosts to their original value. + + errors: + host_unsupported: |- + Your host does not support powershell. A remote powershell connection + can only be made from a windows host. + + ps_remoting_undetected: |- + Unable to establish a remote powershell connection with the guest. + Check if the firewall rules on the guest allow connections to the + windows remote management service. From 1cd10330933fce64608ce2d47bbb72ad0756533f Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 00:11:06 -0700 Subject: [PATCH 2/6] fixes from @sethvargo comments. --- plugins/commands/ps/command.rb | 10 +++++----- plugins/commands/ps/plugin.rb | 7 +++---- plugins/communicators/winrm/shell.rb | 2 +- templates/locales/command_ps.yml | 8 ++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index 33eeec253..3bcc41ba8 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -45,16 +45,16 @@ module VagrantPlugins raise Vagrant::Errors::VMNotCreatedError end - if machine.config.vm.communicator != :winrm #|| !machine.provider.capability?(:winrm_info) + if machine.config.vm.communicator != :winrm raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady end if !options[:command].nil? - out_code = machine.communicate.execute options[:command] + out_code = machine.communicate.execute(options[:command]) if out_code == 0 machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") end - break + next end ps_info = VagrantPlugins::CommunicatorWinRM::Helper.winrm_info(machine) @@ -63,7 +63,7 @@ module VagrantPlugins # Extra arguments if we have any ps_info[:extra_args] = options[:extra_args] - result = ready_ps_remoting_for machine, ps_info + result = ready_ps_remoting_for(machine, ps_info) machine.ui.detail( "Creating powershell session to #{ps_info[:host]}:#{ps_info[:port]}") @@ -73,7 +73,7 @@ module VagrantPlugins @env.host.capability(:ps_client, ps_info) ensure if !result["PreviousTrustedHosts"].nil? - reset_ps_remoting_for machine, ps_info + reset_ps_remoting_for(machine, ps_info) end end end diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb index 56b84b3c5..847b82c47 100644 --- a/plugins/commands/ps/plugin.rb +++ b/plugins/commands/ps/plugin.rb @@ -7,12 +7,12 @@ module VagrantPlugins class Plugin < Vagrant.plugin("2") name "ps command" description <<-DESC - The ps command opens a remote powershell session to the + The ps command opens a remote PowerShell session to the machine if it supports powershell remoting. DESC command("ps") do - require File.expand_path("../command", __FILE__) + require_relative "../command" init! Command end @@ -21,8 +21,7 @@ module VagrantPlugins def self.init! return if defined?(@_init) - I18n.load_path << File.expand_path( - "templates/locales/command_ps.yml", Vagrant.source_root) + I18n.load_path << File.expand_path("templates/locales/command_ps.yml", Vagrant.source_root) I18n.reload! @_init = true end diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index d9d5f28ce..784660b5e 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -9,7 +9,7 @@ Vagrant::Util::SilenceWarnings.silence! do require "winrm" end -require "winrm-fs/file_manager" +require "winrm-fs" module VagrantPlugins module CommunicatorWinRM diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml index e30e5246c..51bf666cc 100644 --- a/templates/locales/command_ps.yml +++ b/templates/locales/command_ps.yml @@ -1,16 +1,16 @@ en: vagrant_ps: detecting: |- - Detecting if a remote powershell connection can be made with the guest... + Detecting if a remote PowerShell connection can be made with the guest... reseting: |- Reseting WinRM TrustedHosts to their original value. errors: host_unsupported: |- - Your host does not support powershell. A remote powershell connection + Your host does not support PowerShell. A remote PowerShell connection can only be made from a windows host. ps_remoting_undetected: |- - Unable to establish a remote powershell connection with the guest. + Unable to establish a remote PowerShell connection with the guest. Check if the firewall rules on the guest allow connections to the - windows remote management service. + Windows remote management service. From 47e57a7cd941c1a2f24e287ab175b9728d5e042b Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 04:17:56 -0700 Subject: [PATCH 3/6] fix relative path --- plugins/commands/ps/plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb index 847b82c47..b108658bc 100644 --- a/plugins/commands/ps/plugin.rb +++ b/plugins/commands/ps/plugin.rb @@ -12,7 +12,7 @@ module VagrantPlugins DESC command("ps") do - require_relative "../command" + require_relative "command" init! Command end From cf6d4ef5a1943ec656695440d452b160aa74f676 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 04:18:23 -0700 Subject: [PATCH 4/6] clean up command encoding --- plugins/hosts/windows/cap/ps.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugins/hosts/windows/cap/ps.rb b/plugins/hosts/windows/cap/ps.rb index 939235915..9960f689e 100644 --- a/plugins/hosts/windows/cap/ps.rb +++ b/plugins/hosts/windows/cap/ps.rb @@ -22,21 +22,13 @@ module VagrantPlugins EOS logger.debug("Starting remote powershell with command:\n#{command}") - command = command.chars.to_a.join("\x00").chomp - command << "\x00" unless command[-1].eql? "\x00" - if(defined?(command.encode)) - command = command.encode('ASCII-8BIT') - command = Base64.strict_encode64(command) - else - command = Base64.encode64(command).chomp - end args = ["-NoProfile"] args << "-ExecutionPolicy" args << "Bypass" args << "-NoExit" args << "-EncodedCommand" - args << command + args << ::WinRM::PowershellScript.new(command).encoded if ps_info[:extra_args] args << ps_info[:extra_args] end From 740877065a8fc0b777cc7aa51b4fb074307affbd Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 05:03:29 -0700 Subject: [PATCH 5/6] marshall back command output when passing a command to ps --- plugins/commands/ps/command.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index 3bcc41ba8..ee021d0ed 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -50,9 +50,11 @@ module VagrantPlugins end if !options[:command].nil? - out_code = machine.communicate.execute(options[:command]) + out_code = machine.communicate.execute(options[:command].dup) do |type,data| + machine.ui.detail(data) if type == :stdout + end if out_code == 0 - machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") + machine.ui.success("Command: #{options[:command]} executed succesfully with output code #{out_code}.") end next end From e6daf2f17279c7ec1452cec1123ad13dccf52890 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 22:24:05 -0700 Subject: [PATCH 6/6] fix pscommand error messaging --- plugins/commands/ps/command.rb | 4 ++-- plugins/commands/ps/errors.rb | 10 +++++++--- templates/locales/command_ps.yml | 13 ++++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index ee021d0ed..63963491a 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -91,7 +91,7 @@ module VagrantPlugins args << "-password" << ps_info[:password] result = Vagrant::Util::PowerShell.execute(script_path, *args) if result.exit_code != 0 - raise Errors::PowershellError, + raise Errors::PowerShellError, script: script_path, stderr: result.stderr end @@ -108,7 +108,7 @@ module VagrantPlugins args << "-hostname" << ps_info[:host] result = Vagrant::Util::PowerShell.execute(script_path, *args) if result.exit_code != 0 - raise Errors::PowershellError, + raise Errors::PowerShellError, script: script_path, stderr: result.stderr end diff --git a/plugins/commands/ps/errors.rb b/plugins/commands/ps/errors.rb index c614fb87a..4be70551a 100644 --- a/plugins/commands/ps/errors.rb +++ b/plugins/commands/ps/errors.rb @@ -2,17 +2,21 @@ module VagrantPlugins module CommandPS module Errors # A convenient superclass for all our errors. - class PSError < Vagrant::Errors::VagrantError + class PSCommandError < Vagrant::Errors::VagrantError error_namespace("vagrant_ps.errors") end - class HostUnsupported < PSError + class HostUnsupported < PSCommandError error_key(:host_unsupported) end - class PSRemotingUndetected < PSError + class PSRemotingUndetected < PSCommandError error_key(:ps_remoting_undetected) end + + class PowerShellError < PSCommandError + error_key(:powershell_error) + end end end end diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml index 51bf666cc..179aae842 100644 --- a/templates/locales/command_ps.yml +++ b/templates/locales/command_ps.yml @@ -3,7 +3,7 @@ en: detecting: |- Detecting if a remote PowerShell connection can be made with the guest... reseting: |- - Reseting WinRM TrustedHosts to their original value. + Resetting WinRM TrustedHosts to their original value. errors: host_unsupported: |- @@ -14,3 +14,14 @@ en: Unable to establish a remote PowerShell connection with the guest. Check if the firewall rules on the guest allow connections to the Windows remote management service. + + powershell_error: |- + An error occurred while executing a PowerShell script. This error + is shown below. Please read the error message and see if this is + a configuration error with your system. If it is not, then please + report a bug. + + Script: %{script} + Error: + + %{stderr}