class Kitchen::SSH
Class to help establish SSH
connections, issue remote commands, and transfer files between a local system and remote node.
@author Fletcher Nichol <fnichol@nichol.ca>
Constants
- SOCKET_EXCEPTIONS
TCP socket exceptions
Attributes
@return [String] the remote hostname @api private
@return [Logger] the logger to use @api private
@return [Hash] SSH
options, passed to `Net::SSH.start`
@return [String] the username for the remote host @api private
Public Class Methods
Constructs a new SSH
object.
@example basic usage
ssh = Kitchen::SSH.new("remote.example.com", "root") ssh.exec("sudo apt-get update") ssh.upload!("/tmp/data.txt", "/var/lib/data.txt") ssh.shutdown
@example block usage
Kitchen::SSH.new("remote.example.com", "root") do |ssh| ssh.exec("sudo apt-get update") ssh.upload!("/tmp/data.txt", "/var/lib/data.txt") end
@param hostname [String] the remote hostname (IP address, FQDN, etc.) @param username [String] the username for the remote host @param options [Hash] configuration options @option options [Logger] :logger the logger to use
(default: `::Logger.new(STDOUT)`)
@yield [self] if a block is given then the constructed object yields
itself and calls `#shutdown` at the end, closing the remote connection
# File lib/kitchen/ssh.rb, line 62 def initialize(hostname, username, options = {}) @hostname = hostname @username = username @options = options.dup @logger = @options.delete(:logger) || ::Logger.new(STDOUT) if block_given? yield self shutdown end end
Public Instance Methods
Execute a command on the remote host.
@param cmd [String] command string to execute @raise [SSHFailed] if the command does not exit with a 0 code
# File lib/kitchen/ssh.rb, line 78 def exec(cmd) string_to_mask = "[SSH] #{self} (#{cmd})" masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password}) logger.debug(masked_string) exit_code = exec_with_exit(cmd) if exit_code != 0 raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]" end end
Builds a LoginCommand
which can be used to open an interactive session on the remote host.
@return [LoginCommand] the login command
# File lib/kitchen/ssh.rb, line 160 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 Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key} } } args += %W{ -p #{port} } args += %W{ #{username}@#{hostname} } LoginCommand.new("ssh", args) end
Shuts down the session connection, if it is still active.
# File lib/kitchen/ssh.rb, line 140 def shutdown 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.shutdown! ensure @session = nil end
# File lib/kitchen/ssh.rb, line 107 def upload(local, remote, options = {}, &progress) require "net/scp" unless defined?(Net::SCP) if progress.nil? progress = lambda do |_ch, name, sent, total| if sent == total logger.debug("Async Uploaded #{name} (#{total} bytes)") end end end session.scp.upload(local, remote, options, &progress) end
Uploads a local file to remote host.
@param local [String] path to local file @param remote [String] path to remote file destination @param options [Hash] configuration options that are passed to
`Net::SCP.upload`
@see net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
# File lib/kitchen/ssh.rb, line 96 def upload!(local, remote, options = {}, &progress) require "net/scp" unless defined?(Net::SCP) if progress.nil? progress = lambda do |_ch, name, sent, total| logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total end end session.scp.upload!(local, remote, options, &progress) end
# File lib/kitchen/ssh.rb, line 134 def upload_path(local, remote, options = {}, &progress) options = { recursive: true }.merge(options) upload(local, remote, options, &progress) end
Uploads a recursive directory to remote host.
@param local [String] path to local file or directory @param remote [String] path to remote file destination @param options [Hash] configuration options that are passed to
`Net::SCP.upload`
@option options [true,false] :recursive recursive copy (default: `true`) @see net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
# File lib/kitchen/ssh.rb, line 128 def upload_path!(local, remote, options = {}, &progress) options = { recursive: true }.merge(options) upload!(local, remote, options, &progress) end
Blocks until the remote host's SSH
TCP port is listening.
# File lib/kitchen/ssh.rb, line 152 def wait logger.info("Waiting for #{hostname}:#{port}...") until test_ssh end
Private Instance Methods
Establish a connection session to the remote host.
@return [Net::SSH::Connection::Session] the SSH
connection session @api private
# File lib/kitchen/ssh.rb, line 211 def establish_connection rescue_exceptions = [ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout ] retries = options[:ssh_retries] || 3 begin string_to_mask = "[SSH] opening connection to #{self}" masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password}) logger.debug(masked_string) Net::SSH.start(hostname, username, options) rescue *rescue_exceptions => e retries -= 1 if retries > 0 logger.info("[SSH] connection failed, retrying (#{e.inspect})") sleep options[:ssh_timeout] || 1 retry else logger.warn("[SSH] connection failed, terminating (#{e.inspect})") raise end end end
Execute a remote command and return the command's exit code.
@param cmd [String] command string to execute @return [Integer] the exit code of the command @api private
# File lib/kitchen/ssh.rb, line 256 def exec_with_exit(cmd) exit_code = nil session.open_channel do |channel| channel.request_pty channel.exec(cmd) 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
@return [Integer] SSH
port (default: 22) @api private
# File lib/kitchen/ssh.rb, line 247 def port options.fetch(:port, 22) end
Builds the Net::SSH session connection or returns the existing one if built.
@return [Net::SSH::Connection::Session] the SSH
connection session @api private
# File lib/kitchen/ssh.rb, line 203 def session @session ||= establish_connection end
Test a remote TCP socket (presumably SSH
) for connectivity.
@return [true,false] a truthy value if the socket is ready and false
otherwise
@api private
# File lib/kitchen/ssh.rb, line 284 def test_ssh socket = TCPSocket.new(hostname, port) IO.select([socket], nil, nil, 5) rescue *SOCKET_EXCEPTIONS sleep options[:ssh_timeout] || 2 false rescue Errno::EPERM, Errno::ETIMEDOUT false ensure socket && socket.close end
String representation of object, reporting its connection details and configuration.
@api private
# File lib/kitchen/ssh.rb, line 241 def to_s "#{username}@#{hostname}:#{port}<#{options.inspect}>" end