foreman_remote_execution_core 1.1.4 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e57511a4f21ec8c2648077cad42df197263a300a5f4cdbc76572d5548e32e4b2
4
- data.tar.gz: ef5e95f9e50e8f94058404e9208069089e9990b2a32af1329eb54026726becbe
3
+ metadata.gz: e1576329ece4e20e9506b989b77557686d030c329c71f70386c7415e93a73e24
4
+ data.tar.gz: 4021d34ddbaa469bb1f4152c0a3c404c976a97910422a6fd60868661992ab774
5
5
  SHA512:
6
- metadata.gz: 8ed29e3e1eb432b340d335d4e5419293d412337fb88ed76fa6fdff2e69e2971ab503ea741a9a0b9d67d0e37feb9da44906e1c42efec46667a631e4ec790813dc
7
- data.tar.gz: b9e438caaeb891d5954ccbf78893620a123b785ba4de7f1ea8001fc016cea6b335ee35520c356b2f0ac9258bb2f5729d491916e355a4978c2fd77477c52e45fd
6
+ metadata.gz: b0ce710cc252baca86acb51cfe294b76f8de319b5516bc8f303252fff52a8ef1e074c34ea00f7bd179b2811dc85daf292035a2736cb421046aa46e2a37137d73
7
+ data.tar.gz: 65092102cb80436b6de9ce165905e8d87c387bf70256b244224d780cd2335f43b0323a06afed5c72e78fa8297566dcbe01f0624dee89b25645d8e2bf292d91a4
@@ -3,16 +3,16 @@ require 'foreman_tasks_core'
3
3
  module ForemanRemoteExecutionCore
4
4
  extend ForemanTasksCore::SettingsLoader
5
5
  register_settings([:remote_execution_ssh, :smart_proxy_remote_execution_ssh_core],
6
- :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
7
- :ssh_user => 'root',
8
- :remote_working_dir => '/var/tmp',
9
- :local_working_dir => '/var/tmp',
10
- :kerberos_auth => false,
11
- :async_ssh => false,
12
- # When set to nil, makes REX use the runner's default interval
13
- :runner_refresh_interval => nil,
14
- :ssh_log_level => :fatal,
15
- :cleanup_working_dirs => true)
6
+ :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
7
+ :ssh_user => 'root',
8
+ :remote_working_dir => '/var/tmp',
9
+ :local_working_dir => '/var/tmp',
10
+ :kerberos_auth => false,
11
+ :async_ssh => false,
12
+ # When set to nil, makes REX use the runner's default interval
13
+ :runner_refresh_interval => nil,
14
+ :ssh_log_level => :fatal,
15
+ :cleanup_working_dirs => true)
16
16
 
17
17
  SSH_LOG_LEVELS = %w(debug info warn error fatal).freeze
18
18
 
@@ -32,7 +32,9 @@ module ForemanRemoteExecutionCore
32
32
  raise "Wrong value '#{@settings[:ssh_log_level]}' for ssh_log_level, must be one of #{SSH_LOG_LEVELS.join(', ')}"
33
33
  end
34
34
 
35
- current = if defined?(SmartProxyDynflowCore)
35
+ current = if defined?(::Proxy::SETTINGS)
36
+ ::Proxy::SETTINGS.log_level.to_s.downcase
37
+ elsif defined?(SmartProxyDynflowCore)
36
38
  SmartProxyDynflowCore::SETTINGS.log_level.to_s.downcase
37
39
  else
38
40
  Rails.configuration.log_level.to_s
@@ -70,6 +72,10 @@ module ForemanRemoteExecutionCore
70
72
  end
71
73
  require 'foreman_remote_execution_core/dispatcher'
72
74
  require 'foreman_remote_execution_core/actions'
75
+
76
+ if defined?(::SmartProxyDynflowCore)
77
+ SmartProxyDynflowCore::TaskLauncherRegistry.register('ssh', ForemanTasksCore::TaskLauncher::Batch)
78
+ end
73
79
  end
74
80
 
75
81
  require 'foreman_remote_execution_core/version'
@@ -6,9 +6,10 @@ module ForemanRemoteExecutionCore
6
6
  def initiate_runner
7
7
  additional_options = {
8
8
  :step_id => run_step_id,
9
- :uuid => execution_plan_id
9
+ :uuid => execution_plan_id,
10
10
  }
11
- ForemanRemoteExecutionCore.runner_class.build(input.merge(additional_options))
11
+ ForemanRemoteExecutionCore.runner_class.build(input.merge(additional_options),
12
+ suspended_action: suspended_action)
12
13
  end
13
14
 
14
15
  def runner_dispatcher
@@ -0,0 +1,110 @@
1
+ #!/bin/sh
2
+ #
3
+ # Control script for the remote execution jobs.
4
+ #
5
+ # The initial script calls `$CONTROL_SCRIPT init-script-finish` once the original script exits.
6
+ # In automatic mode, the exit code is sent back to the proxy on `init-script-finish`.
7
+ #
8
+ # What the script provides is also a manual mode, where the author of the rex script can take
9
+ # full control of the job lifecycle. This allows keeping the marked as running even when
10
+ # the initial script finishes.
11
+ #
12
+ # The manual mode is turned on by calling `$CONTROL_SCRIPT manual-control`. After calling this,
13
+ # one can call `echo message | $CONTROL_SCRIPT update` to send output to the remote execution jobs
14
+ # and `$CONTROL_SCRIPT finish 0` once finished (with 0 as exit code) to send output to the remote execution jobs
15
+ # and `$CONTROL_SCRIPT finish 0` once finished (with 0 as exit code)
16
+ BASE_DIR="$(dirname "$(readlink -f "$0")")"
17
+
18
+ if ! command -v curl >/dev/null; then
19
+ echo 'curl is required' >&2
20
+ exit 1
21
+ fi
22
+
23
+ # send the callback data to proxy
24
+ update() {
25
+ "$BASE_DIR/retrieve.sh" push_update
26
+ }
27
+
28
+ # wait for named pipe $1 to retrieve data. If $2 is provided, it serves as timeout
29
+ # in seconds on how long to wait when reading.
30
+ wait_for_pipe() {
31
+ pipe_path=$1
32
+ if [ -n "$2" ]; then
33
+ timeout="-t $2"
34
+ fi
35
+ if read $timeout <>"$pipe_path"; then
36
+ rm "$pipe_path"
37
+ return 0
38
+ else
39
+ return 1
40
+ fi
41
+ }
42
+
43
+ # function run in background, when receiving update data via STDIN.
44
+ periodic_update() {
45
+ interval=1
46
+ # reading some data from periodic_update_control signals we're done
47
+ while ! wait_for_pipe "$BASE_DIR/periodic_update_control" "$interval"; do
48
+ update
49
+ done
50
+ # one more update before we finish
51
+ update
52
+ # signal the main process that we are finished
53
+ echo > "$BASE_DIR/periodic_update_finished"
54
+ }
55
+
56
+ # signal the periodic_update process that the main process is finishing
57
+ periodic_update_finish() {
58
+ if [ -e "$BASE_DIR/periodic_update_control" ]; then
59
+ echo > "$BASE_DIR/periodic_update_control"
60
+ fi
61
+ }
62
+
63
+ ACTION=${1:-finish}
64
+
65
+ case "$ACTION" in
66
+ init-script-finish)
67
+ if ! [ -e "$BASE_DIR/manual_mode" ]; then
68
+ # make the exit code of initialization script the exit code of the whole job
69
+ cp init_exit_code exit_code
70
+ update
71
+ fi
72
+ ;;
73
+ finish)
74
+ # take exit code passed via the command line, with fallback
75
+ # to the exit code of the initialization script
76
+ exit_code=${2:-$(cat "$BASE_DIR/init_exit_code")}
77
+ echo $exit_code > "$BASE_DIR/exit_code"
78
+ update
79
+ if [ -e "$BASE_DIR/manual_mode" ]; then
80
+ rm "$BASE_DIR/manual_mode"
81
+ fi
82
+ ;;
83
+ update)
84
+ # read data from input when redirected though a pipe
85
+ if ! [ -t 0 ]; then
86
+ # couple of named pipes to coordinate the main process with the periodic_update
87
+ mkfifo "$BASE_DIR/periodic_update_control"
88
+ mkfifo "$BASE_DIR/periodic_update_finished"
89
+ trap "periodic_update_finish" EXIT
90
+ # run periodic update as separate process to keep sending updates in output to server
91
+ periodic_update &
92
+ # redirect the input into output
93
+ tee -a "$BASE_DIR/output"
94
+ periodic_update_finish
95
+ # ensure the periodic update finished before we return
96
+ wait_for_pipe "$BASE_DIR/periodic_update_finished"
97
+ else
98
+ update
99
+ fi
100
+ ;;
101
+ # mark the script to be in manual mode: this means the script author needs to use `update` and `finish`
102
+ # commands to send output to the remote execution job or mark it as finished.
103
+ manual-mode)
104
+ touch "$BASE_DIR/manual_mode"
105
+ ;;
106
+ *)
107
+ echo "Unknown action $ACTION"
108
+ exit 1
109
+ ;;
110
+ esac
@@ -0,0 +1,151 @@
1
+ #!/bin/sh
2
+
3
+ if ! pgrep --help 2>/dev/null >/dev/null; then
4
+ echo DONE 1
5
+ echo "pgrep is required" >&2
6
+ exit 1
7
+ fi
8
+
9
+ BASE_DIR="$(dirname "$(readlink -f "$0")")"
10
+
11
+ # load the data required for generating the callback
12
+ . "$BASE_DIR/env.sh"
13
+ URL_PREFIX="$CALLBACK_HOST/dynflow/tasks/$TASK_ID"
14
+ AUTH="$TASK_ID:$OTP"
15
+ CURL="curl --silent --show-error --fail --max-time 10"
16
+
17
+ MY_LOCK_FILE="$BASE_DIR/retrieve_lock.$$"
18
+ MY_PID=$$
19
+ echo $MY_PID >"$MY_LOCK_FILE"
20
+ LOCK_FILE="$BASE_DIR/retrieve_lock"
21
+ TMP_OUTPUT_FILE="$BASE_DIR/tmp_output"
22
+
23
+ RUN_TIMEOUT=30 # for how long can the script hold the lock
24
+ WAIT_TIMEOUT=60 # for how long the script is trying to acquire the lock
25
+ START_TIME=$(date +%s)
26
+
27
+ fail() {
28
+ echo RUNNING
29
+ echo "$1"
30
+ exit 1
31
+ }
32
+
33
+ acquire_lock() {
34
+ # try to acquire lock by creating the file (ln should be atomic an fail in case
35
+ # another process succeeded first). We also check the content of the lock file,
36
+ # in case our process won when competing over the lock while invalidating
37
+ # the lock on timeout.
38
+ ln "$MY_LOCK_FILE" "$LOCK_FILE" 2>/dev/null || [ "$(head -n1 "$LOCK_FILE")" = "$MY_PID" ]
39
+ return $?
40
+ }
41
+
42
+ # acquiring the lock before proceeding, to ensure only one instance of the script is running
43
+ while ! acquire_lock; do
44
+ # we failed to create retrieve_lock - assuming there is already another retrieve script running
45
+ current_pid=$(head -n1 "$LOCK_FILE")
46
+ if [ -z "$current_pid" ]; then
47
+ continue
48
+ fi
49
+ # check whether the lock is not too old (compared to $RUN_TIMEOUT) and try to kill
50
+ # if it is, so that we don't have a stalled processes here
51
+ lock_lines_count=$(wc -l < "$LOCK_FILE")
52
+ current_lock_time=$(stat --format "%Y" "$LOCK_FILE")
53
+ current_time=$(date +%s)
54
+
55
+ if [ "$(( current_time - START_TIME ))" -gt "$WAIT_TIMEOUT" ]; then
56
+ # We were waiting for the lock for too long - just give up
57
+ fail "Wait time exceeded $WAIT_TIMEOUT"
58
+ elif [ "$(( current_time - current_lock_time ))" -gt "$RUN_TIMEOUT" ]; then
59
+ # The previous lock it hold for too long - re-acquiring procedure
60
+ if [ "$lock_lines_count" -gt 1 ]; then
61
+ # there were multiple processes waiting for lock without resolution
62
+ # longer than the $RUN_TIMEOUT - we reset the lock file and let processes
63
+ # to compete
64
+ echo "RETRY" > "$LOCK_FILE"
65
+ fi
66
+ if [ "$current_pid" != "RETRY" ]; then
67
+ # try to kill the currently stalled process
68
+ kill -9 "$current_pid" 2>/dev/null
69
+ fi
70
+ # try to add our process as one candidate
71
+ echo $MY_PID >> "$LOCK_FILE"
72
+ if [ "$( head -n2 "$LOCK_FILE" | tail -n1 )" = "$MY_PID" ]; then
73
+ # our process won the competition for the new lock: it is the first pid
74
+ # after the original one in the lock file - take ownership of the lock
75
+ # next iteration only this process will get through
76
+ echo $MY_PID >"$LOCK_FILE"
77
+ fi
78
+ else
79
+ # still waiting for the original owner to finish
80
+ sleep 1
81
+ fi
82
+ done
83
+
84
+ release_lock() {
85
+ rm "$MY_LOCK_FILE"
86
+ rm "$LOCK_FILE"
87
+ }
88
+ # ensure the release the lock at exit
89
+ trap "release_lock" EXIT
90
+
91
+ # make sure we clear previous tmp output file
92
+ if [ -e "$TMP_OUTPUT_FILE" ]; then
93
+ rm "$TMP_OUTPUT_FILE"
94
+ fi
95
+
96
+ pid=$(cat "$BASE_DIR/pid")
97
+ [ -f "$BASE_DIR/position" ] || echo 1 > "$BASE_DIR/position"
98
+ position=$(cat "$BASE_DIR/position")
99
+
100
+ prepare_output() {
101
+ if [ -e "$BASE_DIR/manual_mode" ] || ([ -n "$pid" ] && pgrep -P "$pid" >/dev/null 2>&1); then
102
+ echo RUNNING
103
+ else
104
+ echo "DONE $(cat "$BASE_DIR/exit_code" 2>/dev/null)"
105
+ fi
106
+ [ -f "$BASE_DIR/output" ] || exit 0
107
+ tail --bytes "+${position}" "$BASE_DIR/output" > "$TMP_OUTPUT_FILE"
108
+ cat "$TMP_OUTPUT_FILE"
109
+ }
110
+
111
+ # prepare the callback payload
112
+ payload() {
113
+ if [ -n "$1" ]; then
114
+ exit_code="$1"
115
+ else
116
+ exit_code=null
117
+ fi
118
+
119
+ if [ -e "$BASE_DIR/manual_mode" ]; then
120
+ manual_mode=true
121
+ output=$(prepare_output | base64 -w0)
122
+ else
123
+ manual_mode=false
124
+ fi
125
+
126
+ echo "{ \"exit_code\": $exit_code,"\
127
+ " \"step_id\": \"$STEP_ID\","\
128
+ " \"manual_mode\": $manual_mode,"\
129
+ " \"output\": \"$output\" }"
130
+ }
131
+
132
+ if [ "$1" = "push_update" ]; then
133
+ if [ -e "$BASE_DIR/exit_code" ]; then
134
+ exit_code="$(cat "$BASE_DIR/exit_code")"
135
+ action="done"
136
+ else
137
+ exit_code=""
138
+ action="update"
139
+ fi
140
+ $CURL -X POST -d "$(payload $exit_code)" -u "$AUTH" "$URL_PREFIX"/$action 2>>"$BASE_DIR/curl_stderr"
141
+ success=$?
142
+ else
143
+ prepare_output
144
+ success=$?
145
+ fi
146
+
147
+ if [ "$success" = 0 ] && [ -e "$TMP_OUTPUT_FILE" ]; then
148
+ # in case the retrieval was successful, move the position of the cursor to be read next time
149
+ bytes=$(wc --bytes < "$TMP_OUTPUT_FILE")
150
+ expr "${position}" + "${bytes}" > "$BASE_DIR/position"
151
+ fi
@@ -24,8 +24,8 @@ module ForemanRemoteExecutionCore
24
24
  @data.freeze
25
25
  end
26
26
 
27
- def build(options)
28
- new(options)
27
+ def build(options, suspended_action:)
28
+ new(options, suspended_action: suspended_action)
29
29
  end
30
30
  end
31
31
 
@@ -4,10 +4,11 @@ module ForemanRemoteExecutionCore
4
4
  @base_logger = base_logger
5
5
  end
6
6
 
7
- def add(severity, *args)
7
+ def add(severity, *args, &block)
8
8
  severity ||= ::Logger::UNKNOWN
9
9
  return true if @base_logger.nil? || severity < @level
10
- @base_logger.add(severity, *args)
10
+
11
+ @base_logger.add(severity, *args, &block)
11
12
  end
12
13
  end
13
14
  end
@@ -1,10 +1,27 @@
1
+ require 'base64'
2
+
1
3
  module ForemanRemoteExecutionCore
2
4
  class PollingScriptRunner < ScriptRunner
3
5
 
4
6
  DEFAULT_REFRESH_INTERVAL = 60
5
7
 
6
- def initialize(options, user_method)
7
- super(options, user_method)
8
+ def self.load_script(name)
9
+ script_dir = File.expand_path('../async_scripts', __FILE__)
10
+ File.read(File.join(script_dir, name))
11
+ end
12
+
13
+ # The script that controls the flow of the job, able to initiate update or
14
+ # finish on the task, or take over the control over script lifecycle
15
+ CONTROL_SCRIPT = load_script('control.sh')
16
+
17
+ # The script always outputs at least one line
18
+ # First line of the output either has to begin with
19
+ # "RUNNING" or "DONE $EXITCODE"
20
+ # The following lines are treated as regular output
21
+ RETRIEVE_SCRIPT = load_script('retrieve.sh')
22
+
23
+ def initialize(options, user_method, suspended_action: nil)
24
+ super(options, user_method, suspended_action: suspended_action)
8
25
  @callback_host = options[:callback_host]
9
26
  @task_id = options[:uuid]
10
27
  @step_id = options[:step_id]
@@ -13,19 +30,19 @@ module ForemanRemoteExecutionCore
13
30
 
14
31
  def prepare_start
15
32
  super
16
- basedir = File.dirname @remote_script
17
- @pid_path = File.join(basedir, 'pid')
18
- @retrieval_script ||= File.join(basedir, 'retrieve')
19
- prepare_retrieval unless @prepared
33
+ @base_dir = File.dirname @remote_script
34
+ upload_control_scripts
20
35
  end
21
36
 
22
- def control_script
37
+ def initialization_script
23
38
  close_stdin = '</dev/null'
24
39
  close_fds = close_stdin + ' >/dev/null 2>/dev/null'
25
- # pipe the output to tee while capturing the exit code in a file, don't wait for it to finish, output PID of the main command
26
- <<-SCRIPT.gsub(/^\s+\| /, '')
27
- | sh -c '(#{@user_method.cli_command_prefix}#{@remote_script} #{close_stdin}; echo $?>#{@exit_code_path}) | /usr/bin/tee #{@output_path} >/dev/null; #{callback_scriptlet}' #{close_fds} &
28
- | echo $! > '#{@pid_path}'
40
+ main_script = "(#{@remote_script} #{close_stdin} 2>&1; echo $?>#{@base_dir}/init_exit_code) >#{@base_dir}/output"
41
+ control_script_finish = "#{@control_script_path} init-script-finish"
42
+ <<-SCRIPT.gsub(/^ +\| /, '')
43
+ | export CONTROL_SCRIPT="#{@control_script_path}"
44
+ | sh -c '#{main_script}; #{control_script_finish}' #{close_fds} &
45
+ | echo $! > '#{@base_dir}/pid'
29
46
  SCRIPT
30
47
  end
31
48
 
@@ -35,9 +52,13 @@ module ForemanRemoteExecutionCore
35
52
 
36
53
  def refresh
37
54
  err = output = nil
38
- with_retries do
55
+ begin
39
56
  _, output, err = run_sync("#{@user_method.cli_command_prefix} #{@retrieval_script}")
57
+ rescue => e
58
+ @logger.info("Error while connecting to the remote host on refresh: #{e.message}")
40
59
  end
60
+ return if output.nil? || output.empty?
61
+
41
62
  lines = output.lines
42
63
  result = lines.shift.match(/^DONE (\d+)?/)
43
64
  publish_data(lines.join, 'stdout') unless lines.empty?
@@ -45,8 +66,21 @@ module ForemanRemoteExecutionCore
45
66
  if result
46
67
  exitcode = result[1] || 0
47
68
  publish_exit_status(exitcode.to_i)
48
- run_sync("rm -rf \"#{remote_command_dir}\"") if @cleanup_working_dirs
69
+ cleanup
49
70
  end
71
+ ensure
72
+ destroy_session
73
+ end
74
+
75
+ def external_event(event)
76
+ data = event.data
77
+ if data['manual_mode']
78
+ load_event_updates(data)
79
+ else
80
+ # getting the update from automatic mode - reaching to the host to get the latest update
81
+ return run_refresh
82
+ end
83
+ ensure
50
84
  destroy_session
51
85
  end
52
86
 
@@ -55,66 +89,42 @@ module ForemanRemoteExecutionCore
55
89
  ForemanTasksCore::OtpManager.drop_otp(@task_id, @otp) if @otp
56
90
  end
57
91
 
58
- def prepare_retrieval
59
- # The script always outputs at least one line
60
- # First line of the output either has to begin with
61
- # "RUNNING" or "DONE $EXITCODE"
62
- # The following lines are treated as regular output
63
- base = File.dirname(@output_path)
64
- posfile = File.join(base, 'position')
65
- tmpfile = File.join(base, 'tmp')
66
- script = <<-SCRIPT.gsub(/^ +\| /, '')
67
- | #!/bin/sh
68
- | pid=$(cat "#{@pid_path}")
69
- | if ! pgrep --help 2>/dev/null >/dev/null; then
70
- | echo DONE 1
71
- | echo "pgrep is required" >&2
72
- | exit 1
73
- | fi
74
- | if pgrep -P "$pid" >/dev/null 2>/dev/null; then
75
- | echo RUNNING
76
- | else
77
- | echo "DONE $(cat "#{@exit_code_path}" 2>/dev/null)"
78
- | fi
79
- | [ -f "#{@output_path}" ] || exit 0
80
- | [ -f "#{posfile}" ] || echo 1 > "#{posfile}"
81
- | position=$(cat "#{posfile}")
82
- | tail --bytes "+${position}" "#{@output_path}" > "#{tmpfile}"
83
- | bytes=$(cat "#{tmpfile}" | wc --bytes)
84
- | expr "${position}" + "${bytes}" > "#{posfile}"
85
- | cat "#{tmpfile}"
86
- SCRIPT
87
- @logger.debug("copying script:\n#{script.lines.map { |line| " | #{line}" }.join}")
88
- cp_script_to_remote(script, 'retrieve')
89
- @prepared = true
90
- end
92
+ def upload_control_scripts
93
+ return if @control_scripts_uploaded
91
94
 
92
- def callback_scriptlet(callback_script_path = nil)
93
- if @otp
94
- callback_script_path = cp_script_to_remote(callback_script, 'callback') if callback_script_path.nil?
95
- "#{@user_method.cli_command_prefix}#{callback_script_path}"
96
- else
97
- ':' # Shell synonym for "do nothing"
98
- end
95
+ cp_script_to_remote(env_script, 'env.sh')
96
+ @control_script_path = cp_script_to_remote(CONTROL_SCRIPT, 'control.sh')
97
+ @retrieval_script = cp_script_to_remote(RETRIEVE_SCRIPT, 'retrieve.sh')
98
+ @control_scripts_uploaded = true
99
99
  end
100
100
 
101
- def callback_script
101
+ # Script setting the dynamic values to env variables: it's sourced from other control scripts
102
+ def env_script
102
103
  <<-SCRIPT.gsub(/^ +\| /, '')
103
- | #!/bin/sh
104
- | exit_code=$(cat "#{@exit_code_path}")
105
- | url="#{@callback_host}/dynflow/tasks/#{@task_id}/done"
106
- | json="{ \\\"step_id\\\": #{@step_id} }"
107
- | if which curl >/dev/null; then
108
- | curl -X POST -d "$json" -u "#{@task_id}:#{@otp}" "$url"
109
- | else
110
- | echo 'curl is required' >&2
111
- | exit 1
112
- | fi
104
+ | CALLBACK_HOST="#{@callback_host}"
105
+ | TASK_ID="#{@task_id}"
106
+ | STEP_ID="#{@step_id}"
107
+ | OTP="#{@otp}"
113
108
  SCRIPT
114
109
  end
115
110
 
116
111
  private
117
112
 
113
+ # Generates updates based on the callback data from the manual mode
114
+ def load_event_updates(event_data)
115
+ continuous_output = ForemanTasksCore::ContinuousOutput.new
116
+ if event_data.key?('output')
117
+ lines = Base64.decode64(event_data['output']).sub(/\A(RUNNING|DONE).*\n/, '')
118
+ continuous_output.add_output(lines, 'stdout')
119
+ end
120
+ cleanup if event_data['exit_code']
121
+ new_update(continuous_output, event_data['exit_code'])
122
+ end
123
+
124
+ def cleanup
125
+ run_sync("rm -rf \"#{remote_command_dir}\"") if @cleanup_working_dirs
126
+ end
127
+
118
128
  def destroy_session
119
129
  if @session
120
130
  @logger.debug("Closing session with #{@ssh_user}@#{@host}")
@@ -1,11 +1,11 @@
1
1
  require 'net/ssh'
2
2
  require 'fileutils'
3
3
 
4
- # rubocop:disable Lint/HandleExceptions
4
+ # rubocop:disable Lint/SuppressedException
5
5
  begin
6
6
  require 'net/ssh/krb'
7
7
  rescue LoadError; end
8
- # rubocop:enable Lint/HandleExceptions:
8
+ # rubocop:enable Lint/SuppressedException:
9
9
 
10
10
  module ForemanRemoteExecutionCore
11
11
  class SudoUserMethod
@@ -32,7 +32,7 @@ module ForemanRemoteExecutionCore
32
32
  end
33
33
 
34
34
  def filter_password?(received_data)
35
- !@effective_user_password.empty? && @password_sent && received_data.match(@effective_user_password)
35
+ !@effective_user_password.empty? && @password_sent && received_data.match(Regexp.escape(@effective_user_password))
36
36
  end
37
37
 
38
38
  def sent_all_data?
@@ -49,7 +49,7 @@ module ForemanRemoteExecutionCore
49
49
  end
50
50
 
51
51
  class DzdoUserMethod < SudoUserMethod
52
- LOGIN_PROMPT = /password/i
52
+ LOGIN_PROMPT = /password/i.freeze
53
53
 
54
54
  def login_prompt
55
55
  LOGIN_PROMPT
@@ -68,7 +68,8 @@ module ForemanRemoteExecutionCore
68
68
  @ssh_user = ssh_user
69
69
  end
70
70
 
71
- def on_data(_, _); end
71
+ def on_data(_, _)
72
+ end
72
73
 
73
74
  def filter_password?(received_data)
74
75
  false
@@ -82,11 +83,13 @@ module ForemanRemoteExecutionCore
82
83
  "su - #{effective_user} -c "
83
84
  end
84
85
 
85
- def reset; end
86
+ def reset
87
+ end
86
88
  end
87
89
 
88
90
  class NoopUserMethod
89
- def on_data(_, _); end
91
+ def on_data(_, _)
92
+ end
90
93
 
91
94
  def filter_password?(received_data)
92
95
  false
@@ -96,9 +99,11 @@ module ForemanRemoteExecutionCore
96
99
  true
97
100
  end
98
101
 
99
- def cli_command_prefix; end
102
+ def cli_command_prefix
103
+ end
100
104
 
101
- def reset; end
105
+ def reset
106
+ end
102
107
  end
103
108
 
104
109
  class ScriptRunner < ForemanTasksCore::Runner::Base
@@ -108,8 +113,8 @@ module ForemanRemoteExecutionCore
108
113
  DEFAULT_REFRESH_INTERVAL = 1
109
114
  MAX_PROCESS_RETRIES = 4
110
115
 
111
- def initialize(options, user_method)
112
- super()
116
+ def initialize(options, user_method, suspended_action: nil)
117
+ super suspended_action: suspended_action
113
118
  @host = options.fetch(:hostname)
114
119
  @script = options.fetch(:script)
115
120
  @ssh_user = options.fetch(:ssh_user, 'root')
@@ -127,7 +132,7 @@ module ForemanRemoteExecutionCore
127
132
  @user_method = user_method
128
133
  end
129
134
 
130
- def self.build(options)
135
+ def self.build(options, suspended_action:)
131
136
  effective_user = options.fetch(:effective_user, nil)
132
137
  ssh_user = options.fetch(:ssh_user, 'root')
133
138
  effective_user_method = options.fetch(:effective_user_method, 'sudo')
@@ -136,23 +141,23 @@ module ForemanRemoteExecutionCore
136
141
  NoopUserMethod.new
137
142
  elsif effective_user_method == 'sudo'
138
143
  SudoUserMethod.new(effective_user, ssh_user,
139
- options.fetch(:secrets, {}).fetch(:sudo_password, nil))
144
+ options.fetch(:secrets, {}).fetch(:sudo_password, nil))
140
145
  elsif effective_user_method == 'dzdo'
141
146
  DzdoUserMethod.new(effective_user, ssh_user,
142
- options.fetch(:secrets, {}).fetch(:sudo_password, nil))
147
+ options.fetch(:secrets, {}).fetch(:sudo_password, nil))
143
148
  elsif effective_user_method == 'su'
144
149
  SuUserMethod.new(effective_user, ssh_user)
145
150
  else
146
151
  raise "effective_user_method '#{effective_user_method}' not supported"
147
152
  end
148
153
 
149
- new(options, user_method)
154
+ new(options, user_method, suspended_action: suspended_action)
150
155
  end
151
156
 
152
157
  def start
153
158
  prepare_start
154
- script = control_script
155
- logger.debug("executing script:\n#{script.lines.map { |line| " | #{line}" }.join}")
159
+ script = initialization_script
160
+ logger.debug("executing script:\n#{indent_multiline(script)}")
156
161
  trigger(script)
157
162
  rescue => e
158
163
  logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
@@ -169,7 +174,8 @@ module ForemanRemoteExecutionCore
169
174
  @exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
170
175
  end
171
176
 
172
- def control_script
177
+ # the script that initiates the execution
178
+ def initialization_script
173
179
  # pipe the output to tee while capturing the exit code in a file
174
180
  <<-SCRIPT.gsub(/^\s+\| /, '')
175
181
  | sh <<WRAPPER
@@ -181,6 +187,7 @@ module ForemanRemoteExecutionCore
181
187
 
182
188
  def refresh
183
189
  return if @session.nil?
190
+
184
191
  with_retries do
185
192
  with_disconnect_handling do
186
193
  @session.process(0)
@@ -252,6 +259,10 @@ module ForemanRemoteExecutionCore
252
259
 
253
260
  private
254
261
 
262
+ def indent_multiline(string)
263
+ string.lines.map { |line| " | #{line}" }.join
264
+ end
265
+
255
266
  def should_cleanup?
256
267
  @session && !@session.closed? && @cleanup_working_dirs
257
268
  end
@@ -269,7 +280,6 @@ module ForemanRemoteExecutionCore
269
280
  ssh_options[:keys] = [@client_private_key_file] if @client_private_key_file
270
281
  ssh_options[:password] = @ssh_password if @ssh_password
271
282
  ssh_options[:passphrase] = @key_passphrase if @key_passphrase
272
- ssh_options[:user_known_hosts_file] = @known_hosts_file if @known_hosts_file
273
283
  ssh_options[:keys_only] = true
274
284
  # if the host public key is contained in the known_hosts_file,
275
285
  # verify it, otherwise, if missing, import it and continue
@@ -291,6 +301,7 @@ module ForemanRemoteExecutionCore
291
301
  # part of calling the `refresh` method.
292
302
  def run_async(command)
293
303
  raise 'Async command already in progress' if @started
304
+
294
305
  @started = false
295
306
  @user_method.reset
296
307
 
@@ -346,6 +357,7 @@ module ForemanRemoteExecutionCore
346
357
  end
347
358
  ch.exec command do |_, success|
348
359
  raise 'could not execute command' unless success
360
+
349
361
  started = true
350
362
  end
351
363
  end
@@ -390,7 +402,9 @@ module ForemanRemoteExecutionCore
390
402
  end
391
403
 
392
404
  def cp_script_to_remote(script = @script, name = 'script')
393
- upload_data(sanitize_script(script), remote_command_file(name), 555)
405
+ path = remote_command_file(name)
406
+ @logger.debug("copying script to #{path}:\n#{indent_multiline(script)}")
407
+ upload_data(sanitize_script(script), path, 555)
394
408
  end
395
409
 
396
410
  def upload_data(data, path, permissions = 555)
@@ -407,6 +421,7 @@ module ForemanRemoteExecutionCore
407
421
  if status != 0
408
422
  raise "Unable to upload file to #{path} on remote system: exit code: #{status}"
409
423
  end
424
+
410
425
  path
411
426
  end
412
427
 
@@ -440,6 +455,7 @@ module ForemanRemoteExecutionCore
440
455
  def check_expecting_disconnect
441
456
  last_output = @continuous_output.raw_outputs.find { |d| d['output_type'] == 'stdout' }
442
457
  return unless last_output
458
+
443
459
  if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output['output'] =~ /^#{message}/ }
444
460
  @expecting_disconnect = true
445
461
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecutionCore
2
- VERSION = '1.1.4'.freeze
2
+ VERSION = '1.3.1'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-09 00:00:00.000000000 Z
11
+ date: 2020-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: foreman-tasks-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.5
19
+ version: 0.3.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.1.5
26
+ version: 0.3.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: net-ssh
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -48,6 +48,8 @@ files:
48
48
  - LICENSE
49
49
  - lib/foreman_remote_execution_core.rb
50
50
  - lib/foreman_remote_execution_core/actions.rb
51
+ - lib/foreman_remote_execution_core/async_scripts/control.sh
52
+ - lib/foreman_remote_execution_core/async_scripts/retrieve.sh
51
53
  - lib/foreman_remote_execution_core/dispatcher.rb
52
54
  - lib/foreman_remote_execution_core/fake_script_runner.rb
53
55
  - lib/foreman_remote_execution_core/log_filter.rb
@@ -73,8 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
75
  - !ruby/object:Gem::Version
74
76
  version: '0'
75
77
  requirements: []
76
- rubyforge_project:
77
- rubygems_version: 2.7.3
78
+ rubygems_version: 3.0.3
78
79
  signing_key:
79
80
  specification_version: 4
80
81
  summary: Foreman remote execution - core bits