Merge pull request #4400 from mwrock/ps-cmd

Add a ps command to vagrant that drops the user into a remote powershell shell
This commit is contained in:
Mitchell Hashimoto 2015-11-18 10:41:36 -08:00
commit 8bbf6f56f4
9 changed files with 317 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -9,7 +9,7 @@ Vagrant::Util::SilenceWarnings.silence! do
require "winrm"
end
require "winrm-fs/file_manager"
require "winrm-fs"
module VagrantPlugins
module CommunicatorWinRM

View File

@ -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

View File

@ -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

View File

@ -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}