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
@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 [String] the hostname or IP address of the remote SSH
host @api private
@return [Integer] cap on number of parallel ssh sessions we can use @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 connecting to the
remote SSH host
@api private
@return [String] The ssh gateway to use when connecting to the
remote SSH host
@api private
@return [Integer] The port to use when using an ssh gateway @api private
@return [String] The username to use when using an ssh gateway @api private
@return [String] The kitchen ssh proxy to use when connecting to the
remote SSH host via http proxy
@api private
@return [String] The password to use when using an kitchen ssh proxy
remote SSH host via http proxy
@api private
@return [Integer] The port to use when using an kitchen ssh proxy
remote SSH host via http proxy
@api private
@return [String] The username to use when using an kitchen ssh proxy
remote SSH host via http proxy
@api private
@return [String] the username to use when connecting to the remote
SSH host
@api private
Public Class Methods
(see Base::Connection#initialize)
Kitchen::Transport::Base::Connection::new
# File lib/kitchen/transport/ssh.rb, line 122 def initialize(config = {}) super(config) @session = nil end
Public Instance Methods
(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
(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
(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
(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
(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
(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 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 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 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
(see Base::Connection#init_options
)
Kitchen::Transport::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
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
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
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