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

connection_retries[R]

@return [Integer] how many times to retry when failing to execute

a command or transfer files

@api private

connection_retry_sleep[R]

@return [Float] how many seconds to wait before attempting a retry

when failing to execute a command or transfer files

@api private

elevated[R]

@return [Boolean] whether to use winrm-elevated for running commands @api private

instance_name[R]

@return [String] display name for the associated instance @api private

kitchen_root[R]

@return [String] local path to the root of the project @api private

max_wait_until_ready[R]

@return [Integer] how many times to retry when invoking

`#wait_until_ready` before failing

@api private

rdp_port[R]

@return [Integer] the TCP port number to use when connection to the

remote WinRM host

@api private

Public Class Methods

new(config = {}) click to toggle source

(see Base::Connection#initialize)

# File lib/kitchen/transport/winrm.rb, line 88
def initialize(config = {})
  super(config)
  @unelevated_session = nil
  @elevated_session = nil
end

Public Instance Methods

close() click to toggle source

(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
download(remotes, local) click to toggle source

(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
execute(command) click to toggle source

(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
file_manager() click to toggle source

@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
login_command() click to toggle source

(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
retry?(current_try, max_retries, retryable_exit_codes, exception) click to toggle source
# 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
upload(locals, remote) click to toggle source

(see Base::Connection#upload)

# File lib/kitchen/transport/winrm.rb, line 157
def upload(locals, remote)
  file_transporter.upload(locals, remote)
end
wait_until_ready() click to toggle source

(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

connection(retry_options = {}) click to toggle source

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
create_rdp_doc(opts = {}) click to toggle source

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
elevated_session(retry_options = {}) click to toggle source

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_with_exit_code(command) click to toggle source

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
file_transporter() click to toggle source

@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
init_options(options) click to toggle source

(see Base#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
log_stderr_on_warn(stderr) click to toggle source

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
login_command_for_linux() click to toggle source

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
login_command_for_mac() click to toggle source

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
login_command_for_windows() click to toggle source

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
rdp_doc_path() click to toggle source

@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
to_s() click to toggle source

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
unelevated_session(retry_options = {}) click to toggle source

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
unelevated_temp_dir() click to toggle source
# File lib/kitchen/transport/winrm.rb, line 286
def unelevated_temp_dir
  @unelevated_temp_dir ||= unelevated_session.run("$env:temp").stdout.chomp
end