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 +4 -4
- data/lib/foreman_remote_execution_core.rb +20 -11
- data/lib/foreman_remote_execution_core/actions.rb +2 -2
- data/lib/foreman_remote_execution_core/async_scripts/control.sh +110 -0
- data/lib/foreman_remote_execution_core/async_scripts/retrieve.sh +151 -0
- data/lib/foreman_remote_execution_core/log_filter.rb +3 -2
- data/lib/foreman_remote_execution_core/polling_script_runner.rb +72 -62
- data/lib/foreman_remote_execution_core/script_runner.rb +53 -43
- data/lib/foreman_remote_execution_core/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8359d215620a71b5042433c21cb5ee8d2e4606e69f964841dbc48472885a1625
|
4
|
+
data.tar.gz: b48c35f3828723142f308da5d4a91c5fe42e1be71d3f448b441f4a4c7bd4b3e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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?(
|
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
|
-
|
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
|
-
|
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
|
-
|
17
|
-
|
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
|
37
|
+
def initialization_script
|
23
38
|
close_stdin = '</dev/null'
|
24
39
|
close_fds = close_stdin + ' >/dev/null 2>/dev/null'
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
|
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
|
-
|
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
|
-
|
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
|
59
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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/
|
4
|
+
# rubocop:disable Lint/SuppressedException
|
5
5
|
begin
|
6
6
|
require 'net/ssh/krb'
|
7
7
|
rescue LoadError; end
|
8
|
-
# rubocop:enable Lint/
|
8
|
+
# rubocop:enable Lint/SuppressedException:
|
9
9
|
|
10
10
|
module ForemanRemoteExecutionCore
|
11
|
-
class
|
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
|
47
|
-
@password_sent = false
|
43
|
+
def login_prompt
|
48
44
|
end
|
49
45
|
end
|
50
46
|
|
51
|
-
class
|
52
|
-
LOGIN_PROMPT =
|
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
|
-
"
|
55
|
+
"sudo -p '#{LOGIN_PROMPT}' -u #{effective_user} "
|
60
56
|
end
|
61
57
|
end
|
62
58
|
|
63
|
-
class
|
64
|
-
|
59
|
+
class DzdoUserMethod < EffectiveUserMethod
|
60
|
+
LOGIN_PROMPT = /password/i.freeze
|
65
61
|
|
66
|
-
def
|
67
|
-
|
68
|
-
@ssh_user = ssh_user
|
62
|
+
def login_prompt
|
63
|
+
LOGIN_PROMPT
|
69
64
|
end
|
70
65
|
|
71
|
-
def
|
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
|
-
|
78
|
-
|
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(_, _)
|
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
|
95
|
+
def cli_command_prefix
|
96
|
+
end
|
100
97
|
|
101
|
-
def reset
|
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
|
-
|
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
|
-
|
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 =
|
155
|
-
logger.debug("executing script:\n#{script
|
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
|
-
|
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
|
176
|
-
| (#{@
|
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
|
-
|
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
|
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
|
+
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:
|
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
|
-
|
77
|
-
|
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: []
|