class Proxy::RemoteExecution::Ssh::Session

Service that handles running external commands for Actions::Command Dynflow action. It runs just one (actor) thread for all the commands running in the system and updates the Dynflow actions periodically.

Constants

EXPECTED_POWER_ACTION_MESSAGES

Public Class Methods

new(options = {}) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 10
def initialize(options = {})
  @clock = options[:clock] || Dynflow::Clock.spawn('proxy-dispatcher-clock')
  @logger = options[:logger] || Logger.new($stderr)
  @connector_class = options[:connector_class] || Connector
  @local_working_dir = options[:local_working_dir] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.local_working_dir
  @remote_working_dir = options[:remote_working_dir] || ::Proxy::RemoteExecution::Ssh::Plugin.settings.remote_working_dir
  @refresh_interval = options[:refresh_interval] || 1
  @client_private_key_file = Proxy::RemoteExecution::Ssh.private_key_file
  @command = options[:command]

  @command_buffer = []
  @refresh_planned = false

  reference.tell(:initialize_command)
end

Public Instance Methods

dispatcher() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 105
def dispatcher
  self.parent
end
finish_command() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 100
def finish_command
  close
  dispatcher.tell([:finish_command, @command])
end
initialize_command() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 26
def initialize_command
  @logger.debug("initalizing command [#{@command}]")
  open_connector
  remote_script = cp_script_to_remote
  output_path = File.join(File.dirname(remote_script), 'output')

  # pipe the output to tee while capturing the exit code
  script = <<-SCRIPT
    exec 4>&1
    exit_code=`((#{su_prefix}#{remote_script}; echo $?>&3 ) | /usr/bin/tee #{output_path} ) 3>&1 >&4`
    exec 4>&-
    exit $exit_code
  SCRIPT
  @logger.debug("executing script:\n#{script.lines.map { |line| "  | #{line}" }.join}")
  @connector.async_run(script) do |data|
    @command_buffer << data
  end
rescue => e
  @logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
  @command_buffer.concat(CommandUpdate.encode_exception("Error initializing command #{@command}", e))
  refresh
ensure
  plan_next_refresh
end
kill() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 88
def kill
  @logger.debug("killing command [#{@command}]")
  if @connector
    @connector.run("pkill -f #{remote_command_file('script')}")
  else
    @logger.debug("connection closed")
  end
rescue => e
  @command_buffer.concat(CommandUpdate.encode_exception("Failed to kill the command", e, false))
  plan_next_refresh
end
refresh() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 51
def refresh
  @connector.refresh if @connector

  unless @command_buffer.empty?
    status = refresh_command_buffer
    if status
      finish_command
    end
  end
rescue Net::SSH::Disconnect => e
  check_expecting_disconnect
  if @expecting_disconnect
    @command_buffer << CommandUpdate::StatusData.new(0)
  else
    @command_buffer.concat(CommandUpdate.encode_exception("Failed to refresh the connector", e, true))
  end
  refresh_command_buffer
  finish_command
rescue => e
  @command_buffer.concat(CommandUpdate.encode_exception("Failed to refresh the connector", e, false))
ensure
  @refresh_planned = false
  plan_next_refresh
end
refresh_command_buffer() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 76
def refresh_command_buffer
  @logger.debug("command #{@command} got new output: #{@command_buffer.inspect}")
  command_update = CommandUpdate.new(@command_buffer)
  check_expecting_disconnect
  @command.suspended_action << command_update
  @command_buffer = []
  if command_update.exit_status
    @logger.debug("command [#{@command}] finished with status #{command_update.exit_status}")
    return command_update.exit_status
  end
end
start_termination(*args) click to toggle source
Calls superclass method
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 109
def start_termination(*args)
  super
  close
  finish_termination
end

Private Instance Methods

check_expecting_disconnect() click to toggle source

when a remote server disconnects, it's hard to tell if it was on purpose (when calling reboot) or it's an error. When it's expected, we expect the script to produce 'restart host' as its last command output

# File lib/smart_proxy_remote_execution_ssh/session.rb, line 205
def check_expecting_disconnect
  last_output = @command_buffer.reverse.find { |d| d.is_a? CommandUpdate::StdoutData }
  return unless last_output
  if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output.data =~ /^#{message}/ }
    @expecting_disconnect = true
  end
end
close() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 189
def close
  @connector.close if @connector
  @connector = nil
end
cp_script_to_remote() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 162
def cp_script_to_remote
  local_script_file = write_command_file_locally('script', sanitize_script(@command.script))
  File.chmod(0777, local_script_file)
  remote_script_file = remote_command_file('script')
  @connector.upload_file(local_script_file, remote_script_file)
  return remote_script_file
end
ensure_local_directory(path) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 153
def ensure_local_directory(path)
  if File.exist?(path)
    raise "#{path} expected to be a directory" unless File.directory?(path)
  else
    FileUtils.mkdir_p(path)
  end
  return path
end
local_command_dir() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 137
def local_command_dir
  File.join(@local_working_dir, 'foreman-proxy', "foreman-ssh-cmd-#{@command.id}")
end
local_command_file(filename) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 141
def local_command_file(filename)
  File.join(local_command_dir, filename)
end
open_connector() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 129
def open_connector
  raise 'Connector already opened' if @connector
  options = { :logger => @logger }
  options[:known_hosts_file] = prepare_known_hosts
  options[:client_private_key_file] = @client_private_key_file
  @connector = @connector_class.new(@command.host, @command.ssh_user, options)
end
plan_next_refresh() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 194
def plan_next_refresh
  if @connector && !@refresh_planned
    @logger.debug("planning to refresh")
    @clock.ping(reference, Time.now + @refresh_interval, :refresh)
    @refresh_planned = true
  end
end
prepare_known_hosts() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 181
def prepare_known_hosts
  path = local_command_file('known_hosts')
  if @command.host_public_key
    write_command_file_locally('known_hosts', "#{@command.host} #{@command.host_public_key}")
  end
  return path
end
remote_command_dir() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 145
def remote_command_dir
  File.join(@remote_working_dir, "foreman-ssh-cmd-#{@command.id}")
end
remote_command_file(filename) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 149
def remote_command_file(filename)
  File.join(remote_command_dir, filename)
end
sanitize_script(script) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 170
def sanitize_script(script)
  script.tr("\r", '')
end
su_prefix() click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 117
def su_prefix
  return if @command.effective_user.nil? || @command.effective_user == @command.ssh_user
  case @command.effective_user_method
  when 'sudo'
    "sudo -n -u #{@command.effective_user} "
  when 'su'
    "su - #{@command.effective_user} -c "
  else
    raise "effective_user_method ''#{@command.effective_user_method}'' not supported"
  end
end
write_command_file_locally(filename, content) click to toggle source
# File lib/smart_proxy_remote_execution_ssh/session.rb, line 174
def write_command_file_locally(filename, content)
  path = local_command_file(filename)
  ensure_local_directory(File.dirname(path))
  File.write(path, content)
  return path
end