foreman_remote_execution_core 1.1.6 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|