foreman_remote_execution_core 1.1.5 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bfdb9a519170fb2a183ffc6de0b287567c6055696e67e7f0d2cba198d5080df
4
- data.tar.gz: ff28e0f95331c7a4089e24349b064b6d7a56d398e480f86b8e3022200b2d5535
3
+ metadata.gz: dffd5715e2f9f015bb0797db3ac51bbbd8398032459ba8108bb7b7b015edefaa
4
+ data.tar.gz: 5c6092674e23f651612964b94b3ba33e07f3be381d634767e94cfaa204f6bebb
5
5
  SHA512:
6
- metadata.gz: b835a6a82dd30ae99560b9c43a190f375d3a59a52db886e4e99b0280c31b0df73e0b85d9a03e22f80564f294893d88798b68572ebb62914d3f1263248d9c14be
7
- data.tar.gz: 01c800450ae2c8432ed0482ac0058897640f7a7b0f906cda179cf056520286cd8861f24dea4e6bb429b5226498ac5f8601fa5bf6acef5632b4335c5d01a2871a
6
+ metadata.gz: c8bb7142e790e7724fd2e81bc80c95c3d542552003055c9ef4f1c71149ed9c8c64efa9fb701f47eccd62a7f54c31290f984ad50cf1eb2e5612c32ba14a2e08b7
7
+ data.tar.gz: c4ee503e2ca8fdced721e5b802a4b0e7fad7824dbeb12bbedbc959704701e3716cd195048e764745012f68fbf429f1c4e55ee4725d705fb830ea302f40f6b60f
@@ -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,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
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
@@ -108,8 +106,8 @@ module ForemanRemoteExecutionCore
108
106
  DEFAULT_REFRESH_INTERVAL = 1
109
107
  MAX_PROCESS_RETRIES = 4
110
108
 
111
- def initialize(options, user_method)
112
- super()
109
+ def initialize(options, user_method, suspended_action: nil)
110
+ super suspended_action: suspended_action
113
111
  @host = options.fetch(:hostname)
114
112
  @script = options.fetch(:script)
115
113
  @ssh_user = options.fetch(:ssh_user, 'root')
@@ -127,7 +125,7 @@ module ForemanRemoteExecutionCore
127
125
  @user_method = user_method
128
126
  end
129
127
 
130
- def self.build(options)
128
+ def self.build(options, suspended_action:)
131
129
  effective_user = options.fetch(:effective_user, nil)
132
130
  ssh_user = options.fetch(:ssh_user, 'root')
133
131
  effective_user_method = options.fetch(:effective_user_method, 'sudo')
@@ -136,23 +134,24 @@ 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
148
147
 
149
- new(options, user_method)
148
+ new(options, user_method, suspended_action: suspended_action)
150
149
  end
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.5'.freeze
2
+ VERSION = '1.4.0'.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.5
4
+ version: 1.4.0
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-01-14 00:00:00.000000000 Z
11
+ date: 1980-01-01 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
@@ -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.6
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: []