Chris Roberts c8f431cf44 Prepend computer name to user when created scheduled tasks
When running a shell provisioner elevated with winrm a scheduled
task is created to bypass permissions issues. If the name of the
computer has changed this may no longer work. To prevent errors
this PR updates the implementation to fetch the computer name
and prepends it to the username before creating the task.
2018-11-08 14:21:20 -08:00

246 lines
7.7 KiB
Ruby

require "timeout"
require "log4r"
require "vagrant/util/retryable"
require "vagrant/util/silence_warnings"
Vagrant::Util::SilenceWarnings.silence! do
require "winrm"
end
require "winrm-elevated"
require "winrm-fs"
module VagrantPlugins
module CommunicatorWinRM
class WinRMShell
include Vagrant::Util::Retryable
# These are the exceptions that we retry because they represent
# errors that are generally fixed from a retry and don't
# necessarily represent immediate failure cases.
@@exceptions_to_retry_on = [
HTTPClient::KeepAliveDisconnected,
WinRM::WinRMHTTPTransportError,
WinRM::WinRMAuthorizationError,
WinRM::WinRMWSManFault,
Errno::EACCES,
Errno::EADDRINUSE,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::ENETUNREACH,
Errno::EHOSTUNREACH,
Timeout::Error
]
attr_reader :logger
attr_reader :host
attr_reader :port
attr_reader :username
attr_reader :password
attr_reader :execution_time_limit
attr_reader :config
def initialize(host, port, config)
@logger = Log4r::Logger.new("vagrant::communication::winrmshell")
@logger.debug("initializing WinRMShell")
@host = host
@port = port
@username = config.username
@password = config.password
@execution_time_limit = config.execution_time_limit
@config = config
end
def powershell(command, opts = {}, &block)
connection.shell(:powershell) do |shell|
execute_with_rescue(shell, command, &block)
end
end
def cmd(command, opts = {}, &block)
shell_opts = {}
shell_opts[:codepage] = @config.codepage if @config.codepage
connection.shell(:cmd, shell_opts) do |shell|
execute_with_rescue(shell, command, &block)
end
end
def elevated(command, opts = {}, &block)
connection.shell(:elevated) do |shell|
shell.interactive_logon = opts[:interactive] || false
uname = shell.username
begin
shell.username = elevated_username
execute_with_rescue(shell, command, &block)
ensure
shell.username = uname
end
end
end
def wql(query, opts = {}, &block)
retryable(tries: @config.max_tries, on: @@exceptions_to_retry_on, sleep: @config.retry_delay) do
connection.run_wql(query)
end
rescue => e
raise_winrm_exception(e, "run_wql", query)
end
# @param from [Array<String>, String] a single path or folder, or an
# array of paths and folders to upload to the guest
# @param to [String] a path or folder on the guest to upload to
# @return [FixNum] Total size transfered from host to guest
def upload(from, to)
file_manager = WinRM::FS::FileManager.new(connection)
if from.is_a?(Array)
# Preserve return FixNum of bytes transfered
return_bytes = 0
from.each do |file|
return_bytes += file_manager.upload(file, to)
end
return return_bytes
else
file_manager.upload(from, to)
end
end
def download(from, to)
file_manager = WinRM::FS::FileManager.new(connection)
file_manager.download(from, to)
end
protected
def execute_with_rescue(shell, command, &block)
handle_output(shell, command, &block)
rescue => e
raise_winrm_exception(e, shell.class.name.split("::").last, command)
end
def handle_output(shell, command, &block)
output = shell.run(command) do |out, err|
block.call(:stdout, out) if block_given? && out
block.call(:stderr, err) if block_given? && err
end
@logger.debug("Output: #{output.inspect}")
# Verify that we didn't get a parser error, and if so we should
# set the exit code to 1. Parse errors return exit code 0 so we
# need to do this.
if output.exitcode == 0
if output.stderr.include?("ParserError")
@logger.warn("Detected ParserError, setting exit code to 1")
output.exitcode = 1
end
end
return output
end
def raise_winrm_exception(exception, shell = nil, command = nil)
case exception
when WinRM::WinRMAuthorizationError
raise Errors::AuthenticationFailed,
user: @config.username,
password: @config.password,
endpoint: endpoint,
message: exception.message
when WinRM::WinRMHTTPTransportError
raise Errors::ExecutionError,
shell: shell,
command: command,
message: exception.message
when OpenSSL::SSL::SSLError
raise Errors::SSLError, message: exception.message
when HTTPClient::TimeoutError
raise Errors::ConnectionTimeout, message: exception.message
when Errno::ETIMEDOUT
raise Errors::ConnectionTimeout
# This is raised if the connection timed out
when Errno::ECONNREFUSED
# This is raised if we failed to connect the max amount of times
raise Errors::ConnectionRefused
when Errno::ECONNRESET
# This is raised if we failed to connect the max number of times
# due to an ECONNRESET.
raise Errors::ConnectionReset
when Errno::EHOSTDOWN
# This is raised if we get an ICMP DestinationUnknown error.
raise Errors::HostDown
when Errno::EHOSTUNREACH
# This is raised if we can't work out how to route traffic.
raise Errors::NoRoute
else
raise Errors::ExecutionError,
shell: shell,
command: command,
message: exception.message
end
end
def new_connection
@logger.info("Attempting to connect to WinRM...")
@logger.info(" - Host: #{@host}")
@logger.info(" - Port: #{@port}")
@logger.info(" - Username: #{@config.username}")
@logger.info(" - Transport: #{@config.transport}")
client = ::WinRM::Connection.new(endpoint_options)
client.logger = @logger
client
end
def connection
@connection ||= new_connection
end
def endpoint
case @config.transport.to_sym
when :ssl
"https://#{@host}:#{@port}/wsman"
when :plaintext, :negotiate
"http://#{@host}:#{@port}/wsman"
else
raise Errors::WinRMInvalidTransport, transport: @config.transport
end
end
def endpoint_options
{ endpoint: endpoint,
transport: @config.transport,
operation_timeout: @config.timeout,
user: @username,
password: @password,
host: @host,
port: @port,
basic_auth_only: @config.basic_auth_only,
no_ssl_peer_verification: !@config.ssl_peer_verification,
retry_delay: @config.retry_delay,
retry_limit: @config.max_tries }
end
def elevated_username
if @elevated_username
return @elevated_username
end
if username.include?("\\")
return @elevated_username = username
end
computername = ""
powershell("Write-Output $env:computername") do |type, data|
computername << data if type == :stdout
end
computername.strip!
if computername.empty?
return @elevated_username = username
end
@elevated_username = "#{computername}\\#{username}"
end
end #WinShell class
end
end