foreman_remote_execution_core 1.1.4 → 1.3.1

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: 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