2014-03-11 05:38:31 +00:00
|
|
|
require "timeout"
|
|
|
|
|
|
|
|
require "log4r"
|
|
|
|
|
2014-03-14 16:55:27 +00:00
|
|
|
require_relative "helper"
|
2014-03-11 05:38:31 +00:00
|
|
|
require_relative "shell"
|
2014-04-24 03:38:16 +00:00
|
|
|
require_relative "command_filter"
|
2014-03-11 05:38:31 +00:00
|
|
|
|
|
|
|
module VagrantPlugins
|
|
|
|
module CommunicatorWinRM
|
|
|
|
# Provides communication channel for Vagrant commands via WinRM.
|
|
|
|
class Communicator < Vagrant.plugin("2", :communicator)
|
|
|
|
def self.match?(machine)
|
|
|
|
# This is useless, and will likely be removed in the future (this
|
|
|
|
# whole method).
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(machine)
|
2014-04-24 14:50:24 +00:00
|
|
|
@cmd_filter = CommandFilter.new()
|
|
|
|
@logger = Log4r::Logger.new("vagrant::communication::winrm")
|
2014-04-24 03:38:16 +00:00
|
|
|
@machine = machine
|
|
|
|
@shell = nil
|
2014-03-11 05:38:31 +00:00
|
|
|
|
2014-03-14 16:34:08 +00:00
|
|
|
@logger.info("Initializing WinRMCommunicator")
|
2014-03-11 05:38:31 +00:00
|
|
|
end
|
|
|
|
|
2014-12-09 00:09:11 +00:00
|
|
|
def wait_for_ready(timeout)
|
|
|
|
Timeout.timeout(timeout) do
|
|
|
|
# Wait for winrm_info to be ready
|
|
|
|
winrm_info = nil
|
|
|
|
while true
|
|
|
|
winrm_info = Helper.winrm_info(@machine)
|
|
|
|
break if winrm_info
|
|
|
|
sleep 0.5
|
|
|
|
end
|
|
|
|
|
|
|
|
# Got it! Let the user know what we're connecting to.
|
|
|
|
@machine.ui.detail("WinRM address: #{shell.host}:#{shell.port}")
|
|
|
|
@machine.ui.detail("WinRM username: #{shell.username}")
|
|
|
|
@machine.ui.detail("WinRM transport: #{shell.config.transport}")
|
|
|
|
|
|
|
|
last_message = nil
|
|
|
|
last_message_repeat_at = 0
|
|
|
|
while true
|
|
|
|
message = nil
|
|
|
|
begin
|
|
|
|
begin
|
2014-12-09 18:51:45 +00:00
|
|
|
return true if ready?
|
2014-12-09 00:09:11 +00:00
|
|
|
rescue Vagrant::Errors::VagrantError => e
|
|
|
|
@logger.info("WinRM not ready: #{e.inspect}")
|
|
|
|
raise
|
|
|
|
end
|
|
|
|
rescue Errors::ConnectionTimeout
|
|
|
|
message = "Connection timeout."
|
|
|
|
rescue Errors::AuthenticationFailed
|
|
|
|
message = "Authentication failure."
|
|
|
|
rescue Errors::Disconnected
|
|
|
|
message = "Remote connection disconnect."
|
|
|
|
rescue Errors::ConnectionRefused
|
|
|
|
message = "Connection refused."
|
|
|
|
rescue Errors::ConnectionReset
|
|
|
|
message = "Connection reset."
|
|
|
|
rescue Errors::HostDown
|
|
|
|
message = "Host appears down."
|
|
|
|
rescue Errors::NoRoute
|
|
|
|
message = "Host unreachable."
|
|
|
|
rescue Errors::TransientError => e
|
|
|
|
# Any other retriable errors
|
|
|
|
message = e.message
|
|
|
|
end
|
|
|
|
|
|
|
|
# If we have a message to show, then show it. We don't show
|
|
|
|
# repeated messages unless they've been repeating longer than
|
|
|
|
# 10 seconds.
|
|
|
|
if message
|
|
|
|
message_at = Time.now.to_f
|
|
|
|
show_message = true
|
|
|
|
if last_message == message
|
|
|
|
show_message = (message_at - last_message_repeat_at) > 10.0
|
|
|
|
end
|
|
|
|
|
|
|
|
if show_message
|
|
|
|
@machine.ui.detail("Warning: #{message} Retrying...")
|
|
|
|
last_message = message
|
|
|
|
last_message_repeat_at = message_at
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue Timeout::Error
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2014-03-11 05:38:31 +00:00
|
|
|
def ready?
|
2014-03-14 16:34:08 +00:00
|
|
|
@logger.info("Checking whether WinRM is ready...")
|
2014-03-11 05:38:31 +00:00
|
|
|
|
2014-12-09 00:09:11 +00:00
|
|
|
result = Timeout.timeout(@machine.config.winrm.timeout) do
|
2014-04-21 04:00:52 +00:00
|
|
|
shell(true).powershell("hostname")
|
2014-03-11 05:38:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
@logger.info("WinRM is ready!")
|
|
|
|
return true
|
2014-12-09 00:09:11 +00:00
|
|
|
rescue Errors::TransientError => e
|
|
|
|
# We catch a `TransientError` which would signal that something went
|
|
|
|
# that might work if we wait and retry.
|
2014-03-11 05:38:31 +00:00
|
|
|
@logger.info("WinRM not up: #{e.inspect}")
|
|
|
|
|
|
|
|
# We reset the shell to trigger calling of winrm_finder again.
|
2014-12-09 00:09:11 +00:00
|
|
|
# This resolves a problem when using vSphere where the winrm_info was not refreshing
|
2014-03-11 05:38:31 +00:00
|
|
|
# thus never getting the correct hostname.
|
|
|
|
@shell = nil
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2014-04-21 04:00:52 +00:00
|
|
|
def shell(reload=false)
|
|
|
|
@shell = nil if reload
|
2014-03-11 05:38:31 +00:00
|
|
|
@shell ||= create_shell
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute(command, opts={}, &block)
|
2014-04-24 03:38:16 +00:00
|
|
|
# If this is a *nix command with no Windows equivilant, don't run it
|
|
|
|
command = @cmd_filter.filter(command)
|
|
|
|
return 0 if command.empty?
|
|
|
|
|
2014-03-11 05:38:31 +00:00
|
|
|
opts = {
|
2014-05-07 16:47:47 +00:00
|
|
|
command: command,
|
|
|
|
elevated: false,
|
2014-04-24 15:26:38 +00:00
|
|
|
error_check: true,
|
2014-06-24 17:09:11 +00:00
|
|
|
error_class: Errors::WinRMBadExitStatus,
|
|
|
|
error_key: nil, # use the error_class message key
|
2014-05-07 16:47:47 +00:00
|
|
|
good_exit: 0,
|
2014-04-24 15:26:38 +00:00
|
|
|
shell: :powershell,
|
2014-03-11 05:38:31 +00:00
|
|
|
}.merge(opts || {})
|
2014-04-22 18:29:22 +00:00
|
|
|
|
2014-05-07 16:47:47 +00:00
|
|
|
opts[:good_exit] = Array(opts[:good_exit])
|
|
|
|
|
2014-04-24 15:24:12 +00:00
|
|
|
if opts[:elevated]
|
2014-05-19 15:04:59 +00:00
|
|
|
guest_script_path = create_elevated_shell_script(command)
|
|
|
|
command = "powershell -executionpolicy bypass -file #{guest_script_path}"
|
2014-04-24 15:24:12 +00:00
|
|
|
end
|
|
|
|
|
2014-04-22 18:29:22 +00:00
|
|
|
output = shell.send(opts[:shell], command, &block)
|
2014-04-24 04:49:28 +00:00
|
|
|
execution_output(output, opts)
|
2014-03-11 05:38:31 +00:00
|
|
|
end
|
|
|
|
alias_method :sudo, :execute
|
|
|
|
|
|
|
|
def test(command, opts=nil)
|
2014-12-16 16:20:01 +00:00
|
|
|
# If this is a *nix command (which we know about) with no Windows
|
|
|
|
# equivilant, assume failure
|
2014-04-24 03:38:16 +00:00
|
|
|
command = @cmd_filter.filter(command)
|
|
|
|
return false if command.empty?
|
2014-03-11 05:38:31 +00:00
|
|
|
|
2014-12-16 16:20:01 +00:00
|
|
|
opts = {
|
|
|
|
command: command,
|
|
|
|
elevated: false,
|
|
|
|
error_check: false,
|
|
|
|
}.merge(opts || {})
|
|
|
|
|
|
|
|
# If we're passed a *nix command which PS can't parse we get exit code
|
|
|
|
# 0, but output in stderr. We need to check both exit code and stderr.
|
|
|
|
output = shell.send(:powershell, command)
|
|
|
|
return output[:exitcode] == 0 && flatten_stderr(output).length == 0
|
2014-03-11 05:38:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def upload(from, to)
|
2014-04-21 04:00:52 +00:00
|
|
|
@logger.info("Uploading: #{from} to #{to}")
|
2014-03-11 05:38:31 +00:00
|
|
|
shell.upload(from, to)
|
|
|
|
end
|
|
|
|
|
|
|
|
def download(from, to)
|
2014-04-21 04:00:52 +00:00
|
|
|
@logger.info("Downloading: #{from} to #{to}")
|
2014-03-11 05:38:31 +00:00
|
|
|
shell.download(from, to)
|
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
|
|
|
# This creates anew WinRMShell based on the information we know
|
|
|
|
# about this machine.
|
|
|
|
def create_shell
|
2014-05-21 03:12:50 +00:00
|
|
|
winrm_info = Helper.winrm_info(@machine)
|
2014-03-11 05:38:31 +00:00
|
|
|
|
|
|
|
WinRMShell.new(
|
2014-05-21 03:12:50 +00:00
|
|
|
winrm_info[:host],
|
2014-07-22 23:30:29 +00:00
|
|
|
winrm_info[:port],
|
|
|
|
@machine.config.winrm
|
2014-03-11 05:38:31 +00:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2014-05-19 15:04:59 +00:00
|
|
|
# Creates and uploads a PowerShell script which wraps the specified
|
|
|
|
# command in a scheduled task. The scheduled task allows commands to
|
|
|
|
# run on the guest as a true local admin without any of the restrictions
|
|
|
|
# that WinRM puts in place.
|
|
|
|
#
|
2014-05-21 03:12:50 +00:00
|
|
|
# @return The path to elevated_shell.ps1 on the guest
|
2014-05-19 15:04:59 +00:00
|
|
|
def create_elevated_shell_script(command)
|
|
|
|
path = File.expand_path("../scripts/elevated_shell.ps1", __FILE__)
|
|
|
|
script = Vagrant::Util::TemplateRenderer.render(path, options: {
|
|
|
|
username: shell.username,
|
|
|
|
password: shell.password,
|
2014-08-07 12:49:36 +00:00
|
|
|
command: command.gsub("\"", "`\""),
|
2014-05-19 15:04:59 +00:00
|
|
|
})
|
|
|
|
guest_script_path = "c:/tmp/vagrant-elevated-shell.ps1"
|
|
|
|
file = Tempfile.new(["vagrant-elevated-shell", "ps1"])
|
|
|
|
begin
|
|
|
|
file.write(script)
|
|
|
|
file.fsync
|
|
|
|
file.close
|
|
|
|
upload(file.path, guest_script_path)
|
|
|
|
ensure
|
|
|
|
file.close
|
|
|
|
file.unlink
|
|
|
|
end
|
|
|
|
guest_script_path
|
|
|
|
end
|
|
|
|
|
2014-04-24 04:49:28 +00:00
|
|
|
# Handles the raw WinRM shell result and converts it to a
|
|
|
|
# standard Vagrant communicator result
|
|
|
|
def execution_output(output, opts)
|
|
|
|
if opts[:shell] == :wql
|
|
|
|
return output
|
2014-05-07 16:47:47 +00:00
|
|
|
elsif opts[:error_check] && \
|
2014-05-07 17:09:14 +00:00
|
|
|
!opts[:good_exit].include?(output[:exitcode])
|
2014-04-24 04:49:28 +00:00
|
|
|
raise_execution_error(output, opts)
|
|
|
|
end
|
|
|
|
output[:exitcode]
|
|
|
|
end
|
|
|
|
|
|
|
|
def raise_execution_error(output, opts)
|
2014-06-24 17:09:11 +00:00
|
|
|
# WinRM can return multiple stderr and stdout entries
|
|
|
|
error_opts = opts.merge(
|
2014-12-16 16:20:01 +00:00
|
|
|
stdout: flatten_stdout(output),
|
|
|
|
stderr: flatten_stderr(output)
|
2014-06-24 17:09:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# Use a different error message key if the caller gave us one,
|
|
|
|
# otherwise use the error's default message
|
|
|
|
error_opts[:_key] = opts[:error_key] if opts[:error_key]
|
|
|
|
|
|
|
|
# Raise the error, use the type the caller gave us or the comm default
|
2014-03-11 05:38:31 +00:00
|
|
|
raise opts[:error_class], error_opts
|
|
|
|
end
|
2014-12-16 16:20:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
# TODO: Replace with WinRM Output class when WinRM 1.3 is released
|
|
|
|
def flatten_stderr(output)
|
|
|
|
output[:data].map do | line |
|
|
|
|
line[:stderr]
|
|
|
|
end.compact.join
|
|
|
|
end
|
|
|
|
|
|
|
|
def flatten_stdout(output)
|
|
|
|
output[:data].map do | line |
|
|
|
|
line[:flatten_stdout]
|
|
|
|
end.compact.join
|
|
|
|
end
|
2014-03-11 05:38:31 +00:00
|
|
|
end #WinRM class
|
|
|
|
end
|
|
|
|
end
|