class Kitchen::Transport::Ssh::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

hostname[R]

@return [String] the hostname or IP address of the remote SSH host @api private

max_ssh_sessions[R]

@return [Integer] cap on number of parallel ssh sessions we can use @api private

max_wait_until_ready[R]

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

`#wait_until_ready` before failing

@api private

port[R]

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

remote SSH host

@api private

ssh_gateway[R]

@return [String] The ssh gateway to use when connecting to the

remote SSH host

@api private

ssh_gateway_port[R]

@return [Integer] The port to use when using an ssh gateway @api private

ssh_gateway_username[R]

@return [String] The username to use when using an ssh gateway @api private

ssh_http_proxy[R]

@return [String] The kitchen ssh proxy to use when connecting to the

remote SSH host via http proxy

@api private

ssh_http_proxy_password[R]

@return [String] The password to use when using an kitchen ssh proxy

remote SSH host via http proxy

@api private

ssh_http_proxy_port[R]

@return [Integer] The port to use when using an kitchen ssh proxy

remote SSH host via http proxy

@api private

ssh_http_proxy_user[R]

@return [String] The username to use when using an kitchen ssh proxy

remote SSH host via http proxy

@api private

username[R]

@return [String] the username to use when connecting to the remote

SSH host

@api private

Public Class Methods

new(config = {}) click to toggle source

(see Base::Connection#initialize)

# File lib/kitchen/transport/ssh.rb, line 122
def initialize(config = {})
  super(config)
  @session = nil
end

Public Instance Methods

close() click to toggle source

(see Base::Connection#close)

# File lib/kitchen/transport/ssh.rb, line 128
def close
  return if @session.nil?

  string_to_mask = "[SSH] closing connection to #{self}"
  masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
  logger.debug(masked_string)
  session.close
ensure
  @session = nil
end
download(remotes, local) click to toggle source

(see Base::Connection#download)

# File lib/kitchen/transport/ssh.rb, line 202
def download(remotes, local)
  # ensure the parent dir of the local target exists
  FileUtils.mkdir_p(File.dirname(local))

  Array(remotes).each do |file|
    logger.debug("Attempting to download '#{file}' as file")
    session.scp.download!(file, local)
  rescue Net::SCP::Error
    begin
      logger.debug("Attempting to download '#{file}' as directory")
      session.scp.download!(file, local, recursive: true)
    rescue Net::SCP::Error
      logger.warn(
        "SCP download failed for file or directory '#{file}', perhaps it does not exist?"
      )
    end
  end
rescue Net::SSH::Exception => ex
  raise SshFailed, "SCP download failed (#{ex.message})"
end
execute(command) click to toggle source

(see Base::Connection#execute)

# File lib/kitchen/transport/ssh.rb, line 140
def execute(command)
  return if command.nil?

  string_to_mask = "[SSH] #{self} (#{command})"
  masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
  logger.debug(masked_string)
  exit_code = execute_with_exit_code(command)

  if exit_code != 0
    raise Transport::SshFailed.new(
      "SSH exited (#{exit_code}) for command: [#{command}]",
      exit_code
    )
  end
rescue Net::SSH::Exception => ex
  raise SshFailed, "SSH command failed (#{ex.message})"
end
login_command() click to toggle source

(see Base::Connection#login_command)

# File lib/kitchen/transport/ssh.rb, line 159
def login_command
  args  = %w{ -o UserKnownHostsFile=/dev/null }
  args += %w{ -o StrictHostKeyChecking=no }
  args += %w{ -o IdentitiesOnly=yes } if options[:keys]
  args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
  if options.key?(:forward_agent)
    args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} }
  end
  if ssh_gateway
    gateway_command = "ssh -q #{ssh_gateway_username}@#{ssh_gateway} nc #{hostname} #{port}"
    args += %W{ -o ProxyCommand=#{gateway_command} -p #{ssh_gateway_port} }
  end
  Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key} } }
  args += %W{ -p #{port} }
  args += %W{ #{username}@#{hostname} }

  LoginCommand.new("ssh", args)
end
upload(locals, remote) click to toggle source

(see Base::Connection#upload)

# File lib/kitchen/transport/ssh.rb, line 179
def upload(locals, remote)
  logger.debug("TIMING: scp async upload (Kitchen::Transport::Ssh)")
  elapsed = Benchmark.measure do
    waits = []
    Array(locals).map do |local|
      opts = File.directory?(local) ? { recursive: true } : {}

      waits.push(
        session.scp.upload(local, remote, opts) do |_ch, name, sent, total|
          logger.debug("Async Uploaded #{name} (#{total} bytes)") if sent == total
        end
      )
      waits.shift.wait while waits.length >= max_ssh_sessions
    end
    waits.each(&:wait)
  end
  delta = Util.duration(elapsed.real)
  logger.debug("TIMING: scp async upload (Kitchen::Transport::Ssh) took #{delta}")
rescue Net::SSH::Exception => ex
  raise SshFailed, "SCP upload failed (#{ex.message})"
end
wait_until_ready() click to toggle source

(see Base::Connection#wait_until_ready)

# File lib/kitchen/transport/ssh.rb, line 224
def wait_until_ready
  delay = 3
  session(
    retries: max_wait_until_ready / delay,
    delay: delay,
    message: "Waiting for SSH service on #{hostname}:#{port}, " \
      "retrying in #{delay} seconds"
  )
  execute(PING_COMMAND.dup)
end

Private Instance Methods

establish_connection(opts) click to toggle source

Establish an SSH session on the remote host.

@param opts [Hash] retry options @option opts [Integer] :retries the number of times to retry before

failing

@option opts [Float] :delay the number of seconds to wait until

attempting a retry

@option opts [String] :message an optional message to be logged on

debug (overriding the default) when a rescuable exception is raised

@return [Net::SSH::Connection::Session] the SSH connection session @api private

# File lib/kitchen/transport/ssh.rb, line 342
def establish_connection(opts)
  retry_connection(opts) do
    Net::SSH.start(hostname, username, options)
  end
end
establish_connection_via_gateway(opts) click to toggle source

Establish an SSH session on the remote host using a gateway host.

@param opts [Hash] retry options @option opts [Integer] :retries the number of times to retry before

failing

@option opts [Float] :delay the number of seconds to wait until

attempting a retry

@option opts [String] :message an optional message to be logged on

debug (overriding the default) when a rescuable exception is raised

@return [Net::SSH::Connection::Session] the SSH connection session @api private

# File lib/kitchen/transport/ssh.rb, line 323
def establish_connection_via_gateway(opts)
  retry_connection(opts) do
    gateway_options = options.merge(port: ssh_gateway_port)
    Net::SSH::Gateway.new(ssh_gateway,
      ssh_gateway_username, gateway_options).ssh(hostname, username, options)
  end
end
execute_with_exit_code(command) click to toggle source

Execute a remote command over SSH and return the command's exit code.

@param command [String] command string to execute @return [Integer] the exit code of the command @api private

# File lib/kitchen/transport/ssh.rb, line 389
def execute_with_exit_code(command)
  exit_code = nil
  session.open_channel do |channel|
    channel.request_pty

    channel.exec(command) do |_ch, _success|
      channel.on_data do |_ch, data|
        logger << data
      end

      channel.on_extended_data do |_ch, _type, data|
        logger << data
      end

      channel.on_request("exit-status") do |_ch, data|
        exit_code = data.read_long
      end
    end
  end
  session.loop
  exit_code
end
init_options(options) click to toggle source

(see Base::Connection#init_options)

# File lib/kitchen/transport/ssh.rb, line 413
def init_options(options)
  super
  @username                = @options.delete(:username)
  @hostname                = @options.delete(:hostname)
  @port                    = @options[:port] # don't delete from options
  @connection_retries      = @options.delete(:connection_retries)
  @connection_retry_sleep  = @options.delete(:connection_retry_sleep)
  @max_ssh_sessions        = @options.delete(:max_ssh_sessions)
  @max_wait_until_ready    = @options.delete(:max_wait_until_ready)
  @ssh_gateway             = @options.delete(:ssh_gateway)
  @ssh_gateway_username    = @options.delete(:ssh_gateway_username)
  @ssh_gateway_port        = @options.delete(:ssh_gateway_port)
  @ssh_http_proxy          = @options.delete(:ssh_http_proxy)
  @ssh_http_proxy_user     = @options.delete(:ssh_http_proxy_user)
  @ssh_http_proxy_password = @options.delete(:ssh_http_proxy_password)
  @ssh_http_proxy_port     = @options.delete(:ssh_http_proxy_port)
end
retry_connection(opts) { || ... } click to toggle source

Connect to a host executing passed block and properly handling retries.

@param opts [Hash] retry options @option opts [Integer] :retries the number of times to retry before

failing

@option opts [Float] :delay the number of seconds to wait until

attempting a retry

@option opts [String] :message an optional message to be logged on

debug (overriding the default) when a rescuable exception is raised

@return [Net::SSH::Connection::Session] the SSH connection session @api private

# File lib/kitchen/transport/ssh.rb, line 359
def retry_connection(opts)
  log_msg = "[SSH] opening connection to #{self}"
  log_msg += " via #{ssh_gateway_username}@#{ssh_gateway}:#{ssh_gateway_port}" if ssh_gateway
  masked_string = Util.mask_values(log_msg, %w{password ssh_http_proxy_password})

  logger.debug(masked_string)
  yield
rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
  if (opts[:retries] -= 1) > 0
    message = if opts[:message]
                logger.debug("[SSH] connection failed (#{e.inspect})")
                opts[:message]
              else
                "[SSH] connection failed, retrying in #{opts[:delay]} seconds " \
                  "(#{e.inspect})"
              end
    logger.info(message)
    sleep(opts[:delay])
    retry
  else
    logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
    raise SshFailed, "SSH session could not be established"
  end
end
session(retry_options = {}) click to toggle source

Returns a connection session, or establishes one when invoked the first time.

@param retry_options [Hash] retry options for the initial connection @return [Net::SSH::Connection::Session] the SSH connection session @api private

# File lib/kitchen/transport/ssh.rb, line 437
def session(retry_options = {})
  if ssh_gateway
    @session ||= establish_connection_via_gateway({
      retries: connection_retries.to_i,
      delay: connection_retry_sleep.to_i,
    }.merge(retry_options))
  else
    @session ||= establish_connection({
      retries: connection_retries.to_i,
      delay: connection_retry_sleep.to_i,
    }.merge(retry_options))
  end
end
to_s() click to toggle source

String representation of object, reporting its connection details and configuration.

@api private

# File lib/kitchen/transport/ssh.rb, line 455
def to_s
  "#{username}@#{hostname}<#{options.inspect}>"
end