foreman_remote_execution_core 1.1.6 → 1.4.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: ba1645f191d187cfd5564f4498c5ccd30be30997de48e6c0238f8d9894201874
4
- data.tar.gz: 10990da0475e8df75da0a3e17e5fc08f6e04d9320a71c640aab920d44c3f1105
3
+ metadata.gz: 8359d215620a71b5042433c21cb5ee8d2e4606e69f964841dbc48472885a1625
4
+ data.tar.gz: b48c35f3828723142f308da5d4a91c5fe42e1be71d3f448b441f4a4c7bd4b3e9
5
5
  SHA512:
6
- metadata.gz: 9e001b849e2c57aab3233ca917bc671bcd347b88edd410457b417804407c85687234a37ed73517f628fc5a417dec57a133d2e22aca8e3b6d832fe7871c4c386e
7
- data.tar.gz: 6e09ecf3e607fbfbfb0c5f7321f4a2af280165303f18323230977a3561f80dfabb921555cfb2ecc502b9948b3c1d24ade068e2e0910b7b651d4dfb6c609890da
6
+ metadata.gz: 38b65d0f37c2c574c02757b7f84f17be6fdcff8bbc5dcd8a7257a309d5bbf137f7ec8d4c3f0e1de6a1baaa7e5e964b8f5c1eddc553f42672b5ff932cae8aabc1
7
+ data.tar.gz: b273070d6d36d6154e08ae261a44dca40c03a2b924b11cba250243e2e2fc6327222e21f5cae04ddc48cdd53d4cc67b9cb02c982501ef61fe9be06d9834e34b6e
@@ -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
@@ -71,6 +73,13 @@ module ForemanRemoteExecutionCore
71
73
  require 'foreman_remote_execution_core/dispatcher'
72
74
  require 'foreman_remote_execution_core/actions'
73
75
 
76
+ # rubocop:disable Lint/SuppressedException
77
+ begin
78
+ require 'smart_proxy_dynflow_core/task_launcher_registry'
79
+ rescue LoadError
80
+ end
81
+ # rubocop:enable Lint/SuppressedException
82
+
74
83
  if defined?(::SmartProxyDynflowCore)
75
84
  SmartProxyDynflowCore::TaskLauncherRegistry.register('ssh', ForemanTasksCore::TaskLauncher::Batch)
76
85
  end
@@ -6,10 +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
11
  ForemanRemoteExecutionCore.runner_class.build(input.merge(additional_options),
12
- suspended_action: suspended_action)
12
+ suspended_action: suspended_action)
13
13
  end
14
14
 
15
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
@@ -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,8 +1,25 @@
1
+ require 'base64'
2
+
1
3
  module ForemanRemoteExecutionCore
2
4
  class PollingScriptRunner < ScriptRunner
3
5
 
4
6
  DEFAULT_REFRESH_INTERVAL = 60
5
7
 
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
+
6
23
  def initialize(options, user_method, suspended_action: nil)
7
24
  super(options, user_method, suspended_action: suspended_action)
8
25
  @callback_host = options[:callback_host]
@@ -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,16 +1,14 @@
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
- class SudoUserMethod
12
- LOGIN_PROMPT = 'rex login: '.freeze
13
-
11
+ class EffectiveUserMethod
14
12
  attr_reader :effective_user, :ssh_user, :effective_user_password, :password_sent
15
13
 
16
14
  def initialize(effective_user, ssh_user, effective_user_password)
@@ -27,66 +25,64 @@ module ForemanRemoteExecutionCore
27
25
  end
28
26
  end
29
27
 
30
- def login_prompt
31
- LOGIN_PROMPT
32
- end
33
-
34
28
  def filter_password?(received_data)
35
- !@effective_user_password.empty? && @password_sent && received_data.match(@effective_user_password)
29
+ !@effective_user_password.empty? && @password_sent && received_data.match(Regexp.escape(@effective_user_password))
36
30
  end
37
31
 
38
32
  def sent_all_data?
39
33
  effective_user_password.empty? || password_sent
40
34
  end
41
35
 
36
+ def reset
37
+ @password_sent = false
38
+ end
39
+
42
40
  def cli_command_prefix
43
- "sudo -p '#{LOGIN_PROMPT}' -u #{effective_user} "
44
41
  end
45
42
 
46
- def reset
47
- @password_sent = false
43
+ def login_prompt
48
44
  end
49
45
  end
50
46
 
51
- class DzdoUserMethod < SudoUserMethod
52
- LOGIN_PROMPT = /password/i.freeze
47
+ class SudoUserMethod < EffectiveUserMethod
48
+ LOGIN_PROMPT = 'rex login: '.freeze
53
49
 
54
50
  def login_prompt
55
51
  LOGIN_PROMPT
56
52
  end
57
53
 
58
54
  def cli_command_prefix
59
- "dzdo -u #{effective_user} "
55
+ "sudo -p '#{LOGIN_PROMPT}' -u #{effective_user} "
60
56
  end
61
57
  end
62
58
 
63
- class SuUserMethod
64
- attr_accessor :effective_user, :ssh_user
59
+ class DzdoUserMethod < EffectiveUserMethod
60
+ LOGIN_PROMPT = /password/i.freeze
65
61
 
66
- def initialize(effective_user, ssh_user)
67
- @effective_user = effective_user
68
- @ssh_user = ssh_user
62
+ def login_prompt
63
+ LOGIN_PROMPT
69
64
  end
70
65
 
71
- def on_data(_, _); end
72
-
73
- def filter_password?(received_data)
74
- false
66
+ def cli_command_prefix
67
+ "dzdo -u #{effective_user} "
75
68
  end
69
+ end
76
70
 
77
- def sent_all_data?
78
- true
71
+ class SuUserMethod < EffectiveUserMethod
72
+ LOGIN_PROMPT = /Password: /i.freeze
73
+
74
+ def login_prompt
75
+ LOGIN_PROMPT
79
76
  end
80
77
 
81
78
  def cli_command_prefix
82
79
  "su - #{effective_user} -c "
83
80
  end
84
-
85
- def reset; end
86
81
  end
87
82
 
88
83
  class NoopUserMethod
89
- def on_data(_, _); end
84
+ def on_data(_, _)
85
+ end
90
86
 
91
87
  def filter_password?(received_data)
92
88
  false
@@ -96,9 +92,11 @@ module ForemanRemoteExecutionCore
96
92
  true
97
93
  end
98
94
 
99
- def cli_command_prefix; end
95
+ def cli_command_prefix
96
+ end
100
97
 
101
- def reset; end
98
+ def reset
99
+ end
102
100
  end
103
101
 
104
102
  class ScriptRunner < ForemanTasksCore::Runner::Base
@@ -136,12 +134,13 @@ module ForemanRemoteExecutionCore
136
134
  NoopUserMethod.new
137
135
  elsif effective_user_method == 'sudo'
138
136
  SudoUserMethod.new(effective_user, ssh_user,
139
- options.fetch(:secrets, {}).fetch(:sudo_password, nil))
137
+ options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
140
138
  elsif effective_user_method == 'dzdo'
141
139
  DzdoUserMethod.new(effective_user, ssh_user,
142
- options.fetch(:secrets, {}).fetch(:sudo_password, nil))
140
+ options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
143
141
  elsif effective_user_method == 'su'
144
- SuUserMethod.new(effective_user, ssh_user)
142
+ SuUserMethod.new(effective_user, ssh_user,
143
+ options.fetch(:secrets, {}).fetch(:effective_user_password, nil))
145
144
  else
146
145
  raise "effective_user_method '#{effective_user_method}' not supported"
147
146
  end
@@ -151,8 +150,8 @@ module ForemanRemoteExecutionCore
151
150
 
152
151
  def start
153
152
  prepare_start
154
- script = control_script
155
- logger.debug("executing script:\n#{script.lines.map { |line| " | #{line}" }.join}")
153
+ script = initialization_script
154
+ logger.debug("executing script:\n#{indent_multiline(script)}")
156
155
  trigger(script)
157
156
  rescue => e
158
157
  logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
@@ -169,18 +168,19 @@ module ForemanRemoteExecutionCore
169
168
  @exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
170
169
  end
171
170
 
172
- def control_script
171
+ # the script that initiates the execution
172
+ def initialization_script
173
+ su_method = @user_method.instance_of?(ForemanRemoteExecutionCore::SuUserMethod)
173
174
  # pipe the output to tee while capturing the exit code in a file
174
175
  <<-SCRIPT.gsub(/^\s+\| /, '')
175
- | sh <<WRAPPER
176
- | (#{@user_method.cli_command_prefix}#{@remote_script} < /dev/null; echo \\$?>#{@exit_code_path}) | /usr/bin/tee #{@output_path}
177
- | exit \\$(cat #{@exit_code_path})
178
- | WRAPPER
176
+ | sh -c "(#{@user_method.cli_command_prefix}#{su_method ? "'#{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}; echo \\$?>#{@exit_code_path}) | /usr/bin/tee #{@output_path}
177
+ | exit \\$(cat #{@exit_code_path})"
179
178
  SCRIPT
180
179
  end
181
180
 
182
181
  def refresh
183
182
  return if @session.nil?
183
+
184
184
  with_retries do
185
185
  with_disconnect_handling do
186
186
  @session.process(0)
@@ -252,6 +252,10 @@ module ForemanRemoteExecutionCore
252
252
 
253
253
  private
254
254
 
255
+ def indent_multiline(string)
256
+ string.lines.map { |line| " | #{line}" }.join
257
+ end
258
+
255
259
  def should_cleanup?
256
260
  @session && !@session.closed? && @cleanup_working_dirs
257
261
  end
@@ -290,6 +294,7 @@ module ForemanRemoteExecutionCore
290
294
  # part of calling the `refresh` method.
291
295
  def run_async(command)
292
296
  raise 'Async command already in progress' if @started
297
+
293
298
  @started = false
294
299
  @user_method.reset
295
300
 
@@ -345,6 +350,7 @@ module ForemanRemoteExecutionCore
345
350
  end
346
351
  ch.exec command do |_, success|
347
352
  raise 'could not execute command' unless success
353
+
348
354
  started = true
349
355
  end
350
356
  end
@@ -389,7 +395,9 @@ module ForemanRemoteExecutionCore
389
395
  end
390
396
 
391
397
  def cp_script_to_remote(script = @script, name = 'script')
392
- upload_data(sanitize_script(script), remote_command_file(name), 555)
398
+ path = remote_command_file(name)
399
+ @logger.debug("copying script to #{path}:\n#{indent_multiline(script)}")
400
+ upload_data(sanitize_script(script), path, 555)
393
401
  end
394
402
 
395
403
  def upload_data(data, path, permissions = 555)
@@ -406,6 +414,7 @@ module ForemanRemoteExecutionCore
406
414
  if status != 0
407
415
  raise "Unable to upload file to #{path} on remote system: exit code: #{status}"
408
416
  end
417
+
409
418
  path
410
419
  end
411
420
 
@@ -439,6 +448,7 @@ module ForemanRemoteExecutionCore
439
448
  def check_expecting_disconnect
440
449
  last_output = @continuous_output.raw_outputs.find { |d| d['output_type'] == 'stdout' }
441
450
  return unless last_output
451
+
442
452
  if EXPECTED_POWER_ACTION_MESSAGES.any? { |message| last_output['output'] =~ /^#{message}/ }
443
453
  @expecting_disconnect = true
444
454
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecutionCore
2
- VERSION = '1.1.6'.freeze
2
+ VERSION = '1.4.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.6
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-11 00:00:00.000000000 Z
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: foreman-tasks-core
@@ -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
@@ -58,7 +60,7 @@ homepage: https://github.com/theforeman/foreman_remote_execution
58
60
  licenses:
59
61
  - GPL-3.0
60
62
  metadata: {}
61
- post_install_message:
63
+ post_install_message:
62
64
  rdoc_options: []
63
65
  require_paths:
64
66
  - lib
@@ -73,9 +75,8 @@ 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
- signing_key:
78
+ rubygems_version: 3.1.2
79
+ signing_key:
79
80
  specification_version: 4
80
81
  summary: Foreman remote execution - core bits
81
82
  test_files: []