class Proxy::RemoteExecution::Ssh::Runners::MultiplexedSSHConnection

Attributes

logger[R]

Public Class Methods

new(options, logger:) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 48
def initialize(options, logger:)
  @logger = logger

  @id = options.fetch(:id)
  @host = options.fetch(:hostname)
  @script = options.fetch(:script)
  @ssh_user = options.fetch(:ssh_user, 'root')
  @ssh_port = options.fetch(:ssh_port, 22)
  @ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
  @key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
  @host_public_key = options.fetch(:host_public_key, nil)
  @verify_host = options.fetch(:verify_host, nil)
  @client_private_key_file = settings.ssh_identity_key_file

  @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
  @socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
  @socket = nil
end

Public Instance Methods

command(cmd) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 93
def command(cmd)
  raise "Cannot build command to run over multiplexed connection without having an established connection" unless connected?

  ['/usr/bin/ssh', reuse_ssh_options, cmd].flatten
end
connected?() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 89
def connected?
  !@socket.nil?
end
disconnect!() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 78
def disconnect!
  return unless connected?

  cmd = command(%w[-O exit])
  log_command(cmd, label: "Closing shared connection")
  pm = Proxy::Dynflow::ProcessManager.new(cmd)
  set_pm_debug_logging(pm)
  pm.run!
  @socket = nil
end
establish!() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 67
def establish!
  @available_auth_methods ||= available_authentication_methods
  method = @available_auth_methods.find do |method|
    if try_auth_method(method)
      @available_auth_methods.unshift(method).uniq!
      true
    end
  end
  method || raise("Could not establish connection to remote host using any available authentication method, tried #{@available_auth_methods.map(&:name).join(', ')}")
end

Private Instance Methods

available_authentication_methods() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 132
def available_authentication_methods
  methods = []
  methods << AuthenticationMethod.new('password', password: @ssh_password) if @ssh_password
  if verify_key_passphrase
    methods << AuthenticationMethod.new('publickey', password: @key_passphrase, prompt: 'passphrase')
  end
  methods << AuthenticationMethod.new('gssapi-with-mic') if settings[:kerberos_auth]
  raise "There are no available authentication methods" if methods.empty?
  methods
end
establish_ssh_options() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 143
def establish_ssh_options
  return @establish_ssh_options if @establish_ssh_options
  ssh_options = []
  ssh_options << "-o User=#{@ssh_user}"
  ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
  ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
  ssh_options << "-o IdentitiesOnly=yes"
  ssh_options << "-o StrictHostKeyChecking=no"
  ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
  ssh_options << "-o LogLevel=#{ssh_log_level(true)}"
  ssh_options << "-o ControlMaster=auto"
  ssh_options << "-o ControlPath=#{socket_file}"
  ssh_options << "-o ControlPersist=yes"
  ssh_options << "-o ProxyCommand=none"
  @establish_ssh_options = ssh_options
end
reuse_ssh_options() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 160
def reuse_ssh_options
  ["-o", "ControlPath=#{@socket}", "-o", "LogLevel=#{ssh_log_level(false)}", @host]
end
settings() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 128
def settings
  Proxy::RemoteExecution::Ssh::Plugin.settings
end
socket_file() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 164
def socket_file
  File.join(@socket_working_dir, @id)
end
ssh_log_level(new_connection) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 192
def ssh_log_level(new_connection)
  new_connection ? settings[:ssh_log_level] : 'quiet'
end
try_auth_method(method) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 101
def try_auth_method(method)
  # running "ssh -f -N" instead of "ssh true" would be cleaner, but ssh
  # does not close its stderr which trips up the process manager which
  # expects all FDs to be closed

  full_command = [method.ssh_command_prefix, '/usr/bin/ssh', establish_ssh_options, method.ssh_options, @host, 'true'].flatten
  log_command(full_command)
  pm = Proxy::Dynflow::ProcessManager.new(full_command)
  pm.start!
  if pm.status
    raise pm.stderr.to_s
  else
    set_pm_debug_logging(pm)
    pm.stdin.io.close
    pm.run!
  end

  if pm.status.zero?
    logger.debug("Established connection using authentication method #{method.name}")
    @socket = socket_file
    true
  else
    logger.debug("Failed to establish connection using authentication method #{method.name}")
    false
  end
end
verify_key_passphrase() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb, line 168
def verify_key_passphrase
  command = ['/usr/bin/ssh-keygen', '-y', '-f', File.expand_path(@client_private_key_file)]
  log_command(command, label: "Checking if private key has passphrase")
  pm = Proxy::Dynflow::ProcessManager.new(command)
  pm.start!

  raise pm.stderr.to_s if pm.status

  pm.stdin.io.close
  pm.run!

  if pm.status.zero?
    logger.debug("Private key is not protected with a passphrase")
    @key_passphrase = nil
  else
    logger.debug("Private key is protected with a passphrase")
  end

  return true if pm.status.zero? || @key_passphrase

  logger.debug("Private key is protected with a passphrase, but no passphrase was provided")
  false
end