class Kitchen::Transport::Winrm::Connection
A Connection
instance can be generated and re-generated, given new connection details such as connection port, hostname, credentials, etc. This object is responsible for carrying out the actions on the remote host such as executing commands, transferring files, etc.
@author Fletcher Nichol <fnichol@nichol.ca>
Constants
- PING_COMMAND
- RESCUE_EXCEPTIONS_ON_ESTABLISH
Attributes
@return [Integer] how many times to retry when failing to execute
a command or transfer files
@api private
@return [Float] how many seconds to wait before attempting a retry
when failing to execute a command or transfer files
@api private
@return [Boolean] whether to use winrm-elevated for running commands @api private
@return [String] display name for the associated instance @api private
@return [String] local path to the root of the project @api private
@return [Integer] how many times to retry when invoking
`#wait_until_ready` before failing
@api private
@return [Integer] the TCP port number to use when connection to the
remote WinRM host
@api private
Public Class Methods
(see Base::Connection#initialize)
Kitchen::Transport::Base::Connection::new
# File lib/kitchen/transport/winrm.rb, line 88 def initialize(config = {}) super(config) @unelevated_session = nil @elevated_session = nil end
Public Instance Methods
(see Base::Connection#close
)
# File lib/kitchen/transport/winrm.rb, line 95 def close @unelevated_session.close if @unelevated_session @elevated_session.close if @elevated_session ensure @unelevated_session = nil @elevated_session = nil @file_transporter = nil end
(see Base::Connection#download
)
# File lib/kitchen/transport/winrm.rb, line 162 def download(remotes, local) # ensure the parent dir of the local target exists FileUtils.mkdir_p(File.dirname(local)) Array(remotes).each do |remote| file_manager.download(remote, local) end end
(see Base::Connection#execute
)
# File lib/kitchen/transport/winrm.rb, line 105 def execute(command) return if command.nil? string_to_mask = "[WinRM] #{self} (#{command})" masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password}) logger.debug(masked_string) exit_code, stderr = execute_with_exit_code(command) if logger.debug? && exit_code == 0 log_stderr_on_warn(stderr) elsif exit_code != 0 log_stderr_on_warn(stderr) raise Transport::WinrmFailed.new( "WinRM exited (#{exit_code}) for command: [#{command}]", exit_code ) end end
@return [Winrm::FileManager] a file transporter @api private
# File lib/kitchen/transport/winrm.rb, line 173 def file_manager @file_manager ||= WinRM::FS::FileManager.new(connection) end
(see Base::Connection#login_command
)
# File lib/kitchen/transport/winrm.rb, line 142 def login_command case RbConfig::CONFIG["host_os"] when /darwin/ login_command_for_mac when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ login_command_for_windows when /linux/ login_command_for_linux else raise ActionFailed, "Remote login not supported in #{self.class} " \ "from host OS '#{RbConfig::CONFIG["host_os"]}'." end end
Kitchen::Transport::Base::Connection#retry?
# File lib/kitchen/transport/winrm.rb, line 125 def retry?(current_try, max_retries, retryable_exit_codes, exception) # Avoid duplicating Kitchen::Transport::Base#retry? result = super return result if result == true case exception when WinRM::WinRMHTTPTransportError return current_try <= max_retries && [400, 500].include?(exception.status_code) when WinRM::WinRMWSManFault return current_try <= max_retries end false end
(see Base::Connection#upload
)
# File lib/kitchen/transport/winrm.rb, line 157 def upload(locals, remote) file_transporter.upload(locals, remote) end
(see Base::Connection#wait_until_ready
)
# File lib/kitchen/transport/winrm.rb, line 178 def wait_until_ready delay = 3 unelevated_session( retry_limit: max_wait_until_ready / delay, retry_delay: delay ) execute(PING_COMMAND.dup) rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e retries ||= connection_retries.to_i raise e if (retries -= 1) < 0 logger.debug("[WinRM] PING_COMMAND failed. Retrying...") logger.debug("#{e.class}::#{e.message}") sleep(connection_retry_sleep.to_i) retry end
Private Instance Methods
Creates a winrm Connection
instance
@param retry_options [Hash] retry options for the initial connection @return [Winrm::Connection] the winrm connection @api private
# File lib/kitchen/transport/winrm.rb, line 399 def connection(retry_options = {}) @connection ||= begin opts = { retry_limit: connection_retries.to_i, retry_delay: connection_retry_sleep.to_i, }.merge(retry_options) ::WinRM::Connection.new(options.merge(opts)).tap do |conn| conn.logger = logger end end end
Writes an RDP document to the local file system.
@param opts [Hash] file options @option opts [true,false] :mac whether or not the document is for a
Mac system
@api private
# File lib/kitchen/transport/winrm.rb, line 243 def create_rdp_doc(opts = {}) content = Util.outdent!(<<-RDP) full address:s:#{URI.parse(options[:endpoint]).host}:#{rdp_port} prompt for credentials:i:1 username:s:#{options[:user]} RDP content.prepend("drivestoredirect:s:*\n") if opts[:mac] File.open(rdp_doc_path, "wb") { |f| f.write(content) } if logger.debug? debug("Creating RDP document for #{instance_name} (#{rdp_doc_path})") debug("------------") IO.read(rdp_doc_path).each_line { |l| debug(l.chomp.to_s) } debug("------------") end end
Creates an elevated session for running commands via a scheduled task
@return [Winrm::Shells::Elevated] the elevated shell @api private
# File lib/kitchen/transport/winrm.rb, line 387 def elevated_session(retry_options = {}) @elevated_session ||= connection(retry_options).shell(:elevated).tap do |shell| shell.username = options[:elevated_username] shell.password = options[:elevated_password] end end
Execute a Powershell script over WinRM and return the command's exit code and standard error.
@param command [String] Powershell script to execute @return [[Integer,String]] an array containing the exit code of the
script and the standard error stream
@api private
# File lib/kitchen/transport/winrm.rb, line 268 def execute_with_exit_code(command) if elevated session = elevated_session command = "$env:temp='#{unelevated_temp_dir}';#{command}" else session = unelevated_session end begin response = session.run(command) do |stdout, _| logger << stdout if stdout end [response.exitcode, response.stderr] ensure close end end
@return [Winrm::FileTransporter] a file transporter @api private
# File lib/kitchen/transport/winrm.rb, line 292 def file_transporter @file_transporter ||= WinRM::FS::Core::FileTransporter.new(unelevated_session) end
(see Base#init_options)
Kitchen::Transport::Base::Connection#init_options
# File lib/kitchen/transport/winrm.rb, line 297 def init_options(options) super @instance_name = @options.delete(:instance_name) @kitchen_root = @options.delete(:kitchen_root) @rdp_port = @options.delete(:rdp_port) @connection_retries = @options.delete(:connection_retries) @connection_retry_sleep = @options.delete(:connection_retry_sleep) @operation_timeout = @options.delete(:operation_timeout) @receive_timeout = @options.delete(:receive_timeout) @max_wait_until_ready = @options.delete(:max_wait_until_ready) @elevated = @options.delete(:elevated) end
Logs formatted standard error output at the warning level.
@param stderr [String] standard error output @api private
# File lib/kitchen/transport/winrm.rb, line 314 def log_stderr_on_warn(stderr) error_regexp = /<S S=\"Error\">/ if error_regexp.match(stderr) stderr .split(error_regexp)[1..-2] .map! { |line| line.sub(%r{_x000D__x000A_</S>}, "").rstrip } .each { |line| logger.warn(line) } else stderr .split("\r\n") .each { |line| logger.warn(line) } end end
Builds a `LoginCommand` for use by Linux-based platforms.
@return [LoginCommand] a login command @api private
# File lib/kitchen/transport/winrm.rb, line 333 def login_command_for_linux xfreerdp = Util.command_exists? "xfreerdp" unless xfreerdp raise WinrmFailed, "xfreerdp binary not found. Please install freerdp2-x11 on Debian-based systems or freerdp on Redhat-based systems." end args = %W{/u:#{options[:user]}} args += %W{/p:#{options[:password]}} if options.key?(:password) args += %W{/v:#{URI.parse(options[:endpoint]).host}:#{rdp_port}} args += %W{/cert-tofu} # always accept certificate LoginCommand.new(xfreerdp, args) end
Builds a `LoginCommand` for use by Mac-based platforms.
@return [LoginCommand] a login command @api private
# File lib/kitchen/transport/winrm.rb, line 351 def login_command_for_mac create_rdp_doc(mac: true) LoginCommand.new("open", rdp_doc_path) end
Builds a `LoginCommand` for use by Windows-based platforms.
@return [LoginCommand] a login command @api private
# File lib/kitchen/transport/winrm.rb, line 361 def login_command_for_windows create_rdp_doc LoginCommand.new("mstsc", rdp_doc_path) end
@return [String] path to the local RDP document @api private
# File lib/kitchen/transport/winrm.rb, line 369 def rdp_doc_path File.join(kitchen_root, ".kitchen", "#{instance_name}.rdp") end
String representation of object, reporting its connection details and configuration.
@api private
# File lib/kitchen/transport/winrm.rb, line 416 def to_s "<#{options.inspect}>" end
Establishes a remote shell session, or establishes one when invoked the first time.
@param retry_options [Hash] retry options for the initial connection @return [Winrm::Shells::Powershell] the command shell session @api private
# File lib/kitchen/transport/winrm.rb, line 379 def unelevated_session(retry_options = {}) @unelevated_session ||= connection(retry_options).shell(:powershell) end
# File lib/kitchen/transport/winrm.rb, line 286 def unelevated_temp_dir @unelevated_temp_dir ||= unelevated_session.run("$env:temp").stdout.chomp end