mirror of
https://github.com/hashicorp/vagrant.git
synced 2026-02-20 08:20:34 -05:00
Remove customized require behaviors and modify the bin executable to check for missing tools that Vagrant expects to exist when running outside of an installer.
267 lines
8.5 KiB
Ruby
267 lines
8.5 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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
|
|
|
|
# Exit code generated when user is invalid. Can occur
|
|
# after a hostname update
|
|
INVALID_USERID_EXITCODE = -2147024809
|
|
|
|
# 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
|
|
result = execute_with_rescue(shell, command, &block)
|
|
if result.exitcode == INVALID_USERID_EXITCODE && result.stderr.include?(":UserId:")
|
|
uname = shell.username
|
|
ename = elevated_username
|
|
if uname != ename
|
|
@logger.warn("elevated command failed due to username error")
|
|
@logger.warn("retrying command using machine prefixed username - #{ename}")
|
|
begin
|
|
shell.username = ename
|
|
result = execute_with_rescue(shell, command, &block)
|
|
ensure
|
|
shell.username = uname
|
|
end
|
|
end
|
|
end
|
|
result
|
|
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?(String) && File.directory?(from)
|
|
if from.end_with?(".")
|
|
from = from[0, from.length - 1]
|
|
else
|
|
to = File.join(to, File.basename(File.expand_path(from)))
|
|
end
|
|
end
|
|
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 IO::TimeoutError
|
|
raise Errors::ConnectionTimeout
|
|
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 username.include?("\\")
|
|
return username
|
|
end
|
|
computername = ""
|
|
powershell("Write-Output $env:computername") do |type, data|
|
|
computername << data if type == :stdout
|
|
end
|
|
computername.strip!
|
|
if computername.empty?
|
|
return username
|
|
end
|
|
"#{computername}\\#{username}"
|
|
end
|
|
end #WinShell class
|
|
end
|
|
end
|