This commit aligns how the file provisioner should work on all host machines. It ensures that a `/.` is only applied if the user intended to upload a folder to a destination under a different name. It ensures that if uploading to a windows guest with a different destination folder name, it does not nest the source folder under that name so that it works the same as it does on linux platforms. It also updates the behavior of the winrm upload communicator by allowing an array of paths to be uploaded instead of a single file or folder to allow for this new functionality for windows guests.
222 lines
7.0 KiB
Ruby
222 lines
7.0 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
|
|
execute_with_rescue(shell, command, &block)
|
|
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
|
|
end #WinShell class
|
|
end
|
|
end
|