adds a ps command to vagrant that drops the user into a remote powershell shell

This commit is contained in:
Matt Wrock 2014-08-27 12:17:30 -07:00
parent b420da0678
commit c60a020096
8 changed files with 308 additions and 0 deletions

View File

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

View File

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

View File

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

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

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

View File

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

View File

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