class Proxy::RemoteExecution::Ssh::Runners::ScriptRunner
Constants
- DEFAULT_REFRESH_INTERVAL
- EXPECTED_POWER_ACTION_MESSAGES
- UNSHARE_PREFIX
Attributes
execution_timeout_interval[R]
Public Class Methods
build(options, suspended_action:)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 123 def self.build(options, suspended_action:) effective_user = options.fetch(:effective_user, nil) ssh_user = options.fetch(:ssh_user, 'root') effective_user_method = options.fetch(:effective_user_method, 'sudo') user_method = if effective_user.nil? || effective_user == ssh_user NoopUserMethod.new elsif effective_user_method == 'sudo' SudoUserMethod.new(effective_user, ssh_user, options.fetch(:secrets, {}).fetch(:effective_user_password, nil)) elsif effective_user_method == 'dzdo' DzdoUserMethod.new(effective_user, ssh_user, options.fetch(:secrets, {}).fetch(:effective_user_password, nil)) elsif effective_user_method == 'su' SuUserMethod.new(effective_user, ssh_user, options.fetch(:secrets, {}).fetch(:effective_user_password, nil)) else raise "effective_user_method '#{effective_user_method}' not supported" end new(options, user_method, suspended_action: suspended_action) end
new(options, user_method, suspended_action: nil)
click to toggle source
Calls superclass method
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 104 def initialize(options, user_method, suspended_action: nil) super suspended_action: suspended_action @host = options.fetch(:hostname) @script = options.fetch(:script) @ssh_user = options.fetch(:ssh_user, 'root') @ssh_port = options.fetch(:ssh_port, 22) @host_public_key = options.fetch(:host_public_key, nil) @execution_timeout_interval = options.fetch(:execution_timeout_interval, nil) @client_private_key_file = settings.ssh_identity_key_file @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir) @remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir.shellescape) @socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir) @cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs) @first_execution = options.fetch(:first_execution, false) @user_method = user_method @options = options end
Public Instance Methods
close()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 246 def close run_sync("rm -rf #{remote_command_dir}") if should_cleanup? rescue StandardError => e publish_exception('Error when removing remote working dir', e, false) ensure close_session if @process_manager FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs end
close_session()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 240 def close_session raise 'Control socket file does not exist' unless File.exist?(socket_file) @logger.debug("Sending exit request for session #{@ssh_user}@#{@host}") @connection.disconnect! end
initialization_script()
click to toggle source
the script that initiates the execution
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 209 def initialization_script # pipe the output to tee while capturing the exit code in a file @remote_script_wrapper end
kill()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 221 def kill if @process_manager&.started? run_sync("kill $(cat #{@pid_path})") else logger.debug('connection closed') end rescue StandardError => e publish_exception('Unexpected error', e, false) end
preflight_checks()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 165 def preflight_checks script = cp_script_to_remote("#!/bin/sh\nexec true") ensure_remote_command(script, error: 'Failed to execute script on remote machine, exit code: %{exit_code}.' ) unless @user_method.is_a? NoopUserMethod ensure_remote_command("#{@user_method.cli_command_prefix} #{script}", error: 'Failed to change to effective user, exit code: %{exit_code}', tty: true, user_method: @user_method, close_stdin: false) end # The path should already be escaped ensure_remote_command("rm #{script}") end
prepare_start()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 181 def prepare_start @remote_script = cp_script_to_remote @output_path = File.join(File.dirname(@remote_script), 'output') @exit_code_path = File.join(File.dirname(@remote_script), 'exit_code') @pid_path = File.join(File.dirname(@remote_script), 'pid') su_method = @user_method.instance_of?(SuUserMethod) wrapper = <<~SCRIPT if [ "$1" = "inner" ]; then echo \$$ > #{@pid_path} ( #{@user_method.cli_command_prefix}#{su_method ? "'exec #{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"} echo \$? >#{@exit_code_path} ) | tee #{@output_path} else UNSHARE='' if #{UNSHARE_PREFIX} true >/dev/null 2>/dev/null; then UNSHARE='#{UNSHARE_PREFIX}' fi exec $UNSHARE "$0" inner fi SCRIPT @remote_script_wrapper = upload_data( wrapper, File.join(File.dirname(@remote_script), 'script-wrapper'), 555) end
publish_data(data, type, pm = nil)
click to toggle source
Calls superclass method
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 255 def publish_data(data, type, pm = nil) pm ||= @process_manager data = data.dup if data.frozen? super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data) @user_method.on_data(data, pm.stdin) if pm end
refresh()
click to toggle source
Calls superclass method
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 214 def refresh return if @process_manager.nil? super ensure check_expecting_disconnect end
start()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 146 def start Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution ensure_local_directory(@socket_working_dir) @connection = MultiplexedSSHConnection.new(@options.merge(:id => @id), logger: logger) @connection.establish! preflight_checks prepare_start script = initialization_script logger.debug("executing script:\n#{indent_multiline(script)}") trigger(script) rescue StandardError, NotImplementedError => e logger.error("error while initializing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}") publish_exception('Error initializing command', e) end
timeout()
click to toggle source
Calls superclass method
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 231 def timeout @logger.debug('job timed out') super end
timeout_interval()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 236 def timeout_interval execution_timeout_interval end
trigger(*args)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 161 def trigger(*args) run_async(*args) 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/runners/script_runner.rb, line 404 def check_expecting_disconnect last_output = @continuous_output.raw_outputs.find { |d| d['output_type'] == 'stdout' } return unless last_output if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output['output'] =~ /^#{message}/ } @expecting_disconnect = true end end
cp_script_to_remote(script = @script, name = 'script')
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 349 def cp_script_to_remote(script = @script, name = 'script') path = remote_command_file(name) @logger.debug("copying script to #{path}:\n#{indent_multiline(script)}") upload_data(sanitize_script(script), path, 555) end
ensure_local_directory(path)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 340 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
ensure_remote_command(cmd, error: nil, **kwargs)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 383 def ensure_remote_command(cmd, error: nil, **kwargs) if (pm = run_sync(cmd, **kwargs)).status != 0 msg = error || 'Failed to run command %{command} on remote machine, exit code: %{exit_code}' raise(msg % { command: cmd, exit_code: pm.status }) end end
ensure_remote_directory(path)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 377 def ensure_remote_directory(path) ensure_remote_command("mkdir -p #{path}", error: "Unable to create directory #{path} on remote system, exit code: %{exit_code}" ) end
indent_multiline(string)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 264 def indent_multiline(string) string.lines.map { |line| " | #{line}" }.join end
local_command_dir()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 320 def local_command_dir File.join(@local_working_dir, 'foreman-proxy', "foreman-ssh-cmd-#{@id}") end
local_command_file(filename)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 324 def local_command_file(filename) File.join(ensure_local_directory(local_command_dir), filename) end
prepare_known_hosts()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 312 def prepare_known_hosts path = local_command_file('known_hosts') if @host_public_key write_command_file_locally('known_hosts', "#{@host} #{@host_public_key}") end return path end
remote_command_dir()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 332 def remote_command_dir File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}") end
remote_command_file(filename)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 336 def remote_command_file(filename) File.join(remote_command_dir, filename) end
run_async(command)
click to toggle source
Initiates run of the remote command and yields the data when available. The yielding doesn't happen automatically, but as part of calling the `refresh` method.
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 279 def run_async(command) raise 'Async command already in progress' if @process_manager&.started? @user_method.reset cmd = @connection.command([tty_flag(true), command].flatten.compact) log_command(cmd) initialize_command(*cmd) true end
run_started?()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 290 def run_started? @process_manager&.started? && @user_method.sent_all_data? end
run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 298 def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil) cmd = @connection.command([tty_flag(tty), command].flatten.compact) log_command(cmd) pm = Proxy::Dynflow::ProcessManager.new(cmd) set_pm_debug_logging(pm, user_method: user_method) pm.start! unless pm.status pm.stdin.io.puts(stdin) if stdin pm.stdin.io.close if close_stdin pm.run! end pm end
sanitize_script(script)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 390 def sanitize_script(script) script.tr("\r", '') end
settings()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 272 def settings Proxy::RemoteExecution::Ssh::Plugin.settings end
should_cleanup?()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 268 def should_cleanup? @process_manager && @cleanup_working_dirs end
socket_file()
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 328 def socket_file File.join(ensure_local_directory(@socket_working_dir), @id) end
tty_flag(tty)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 294 def tty_flag(tty) '-tt' if tty end
upload_data(data, path, permissions = 555)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 355 def upload_data(data, path, permissions = 555) ensure_remote_directory File.dirname(path) # We use tee here to pipe stdin coming from ssh to a file at $path, while silencing its output # This is used to write to $path with elevated permissions, solutions using cat and output redirection # would not work, because the redirection would happen in the non-elevated shell. command = "tee #{path} >/dev/null && chmod #{permissions} #{path}" @logger.debug("Sending data to #{path} on remote host:\n#{data}") ensure_remote_command(command, stdin: data, error: "Unable to upload file to #{path} on remote system, exit code: %{exit_code}" ) path end
upload_file(local_path, remote_path)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 371 def upload_file(local_path, remote_path) mode = File.stat(local_path).mode.to_s(8)[-3..-1] @logger.debug("Uploading local file: #{local_path} as #{remote_path} with #{mode} permissions") upload_data(File.read(local_path), remote_path, mode) end
write_command_file_locally(filename, content)
click to toggle source
# File lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb, line 394 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