diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb new file mode 100644 index 000000000..63963491a --- /dev/null +++ b/plugins/commands/ps/command.rb @@ -0,0 +1,118 @@ +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 + raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady + end + + if !options[:command].nil? + 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.success("Command: #{options[:command]} executed succesfully with output code #{out_code}.") + end + next + 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..4be70551a --- /dev/null +++ b/plugins/commands/ps/errors.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module CommandPS + module Errors + # A convenient superclass for all our errors. + class PSCommandError < Vagrant::Errors::VagrantError + error_namespace("vagrant_ps.errors") + end + + class HostUnsupported < PSCommandError + error_key(:host_unsupported) + end + + class PSRemotingUndetected < PSCommandError + error_key(:ps_remoting_undetected) + end + + class PowerShellError < PSCommandError + error_key(:powershell_error) + end + end + end +end diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb new file mode 100644 index 000000000..b108658bc --- /dev/null +++ b/plugins/commands/ps/plugin.rb @@ -0,0 +1,30 @@ +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_relative "command" + 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/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 9b0f4e302..14da0adc2 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/plugins/hosts/windows/cap/ps.rb b/plugins/hosts/windows/cap/ps.rb new file mode 100644 index 000000000..9960f689e --- /dev/null +++ b/plugins/hosts/windows/cap/ps.rb @@ -0,0 +1,42 @@ +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}") + + args = ["-NoProfile"] + args << "-ExecutionPolicy" + args << "Bypass" + args << "-NoExit" + args << "-EncodedCommand" + args << ::WinRM::PowershellScript.new(command).encoded + 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 d441f229f..84a38836b 100644 --- a/plugins/hosts/windows/plugin.rb +++ b/plugins/hosts/windows/plugin.rb @@ -25,6 +25,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..179aae842 --- /dev/null +++ b/templates/locales/command_ps.yml @@ -0,0 +1,27 @@ +en: + vagrant_ps: + detecting: |- + Detecting if a remote PowerShell connection can be made with the guest... + reseting: |- + Resetting 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. + + 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}