smart_proxy_remote_execution_ssh 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9532552fe00ed7bf26c3755d710ad2ea87b4ac9255a12851a2a2f20cd6e3afbf
4
- data.tar.gz: 5b2d309cc8bdec3ed2c20d2b0a944c6bc1863a3fd728e7febc5e020dc3f88239
3
+ metadata.gz: e2f91ec259ca105bcc18a5cecf4342019f4023d44cbff5695998b5e57c25e325
4
+ data.tar.gz: 51ccf299324d4adf10c07475dca2749d885c258913c8d9e7593d8b8149e6d18d
5
5
  SHA512:
6
- metadata.gz: a2359e2706bb4d465fbb98285e48099d0ff3c6f58ab7aae0e4972d101ec2b5663bea62e965be074c784dea19cda7de551d1f27f26aba6c4502d4d96c9151bd7d
7
- data.tar.gz: f7bce64d724947712ee9c2d394802b07f64df2c581d92d36cb3ac4732b9a8609b2c1935e4189a39a0712692c2946111e426eb2ea5452f379906eb3ff379142ee
6
+ metadata.gz: 74313e91e51ff5d58fce0bc880131b12984a750110a2d76c88875a6b824eec40a2ba9d7336dfda34df7c11d465ded62c14d4d33ba10cf4bed1b2e0309aff9a2b
7
+ data.tar.gz: 92f85e971f10d9e45418e75d0325f3e548e65954160be9f86cb154adde6bc3a512b4e3a5429d162291704b49a50a5570fcdeb4b129f735b8f8f737938a0194d0
@@ -0,0 +1,110 @@
1
+ require 'mqtt'
2
+ require 'json'
3
+
4
+ module Proxy::RemoteExecution::Ssh::Actions
5
+ class PullScript < Proxy::Dynflow::Action::Runner
6
+ JobDelivered = Class.new
7
+
8
+ execution_plan_hooks.use :cleanup, :on => :stopped
9
+
10
+ def plan(action_input, mqtt: false)
11
+ super(action_input)
12
+ input[:with_mqtt] = mqtt
13
+ end
14
+
15
+ def run(event = nil)
16
+ if event == JobDelivered
17
+ output[:state] = :delivered
18
+ suspend
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def init_run
25
+ otp_password = if input[:with_mqtt]
26
+ ::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
27
+ end
28
+ input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script])
29
+ output[:state] = :ready_for_pickup
30
+ output[:result] = []
31
+ mqtt_start(otp_password) if input[:with_mqtt]
32
+ suspend
33
+ end
34
+
35
+ def cleanup(_plan = nil)
36
+ job_storage.drop_job(execution_plan_id, run_step_id)
37
+ Proxy::Dynflow::OtpManager.passwords.delete(execution_plan_id)
38
+ end
39
+
40
+ def process_external_event(event)
41
+ output[:state] = :running
42
+ data = event.data
43
+ continuous_output = Proxy::Dynflow::ContinuousOutput.new
44
+ Array(data['output']).each { |line| continuous_output.add_output(line, 'stdout') } if data.key?('output')
45
+ exit_code = data['exit_code'].to_i if data['exit_code']
46
+ process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
47
+ end
48
+
49
+ def kill_run
50
+ case output[:state]
51
+ when :ready_for_pickup
52
+ # If the job is not running yet on the client, wipe it from storage
53
+ cleanup
54
+ # TODO: Stop the action
55
+ when :notified, :running
56
+ # Client was notified or is already running, dealing with this situation
57
+ # is only supported if mqtt is available
58
+ # Otherwise we have to wait it out
59
+ # TODO
60
+ # if input[:with_mqtt]
61
+ end
62
+ suspend
63
+ end
64
+
65
+ def mqtt_start(otp_password)
66
+ payload = {
67
+ type: 'data',
68
+ message_id: SecureRandom.uuid,
69
+ version: 1,
70
+ sent: DateTime.now.iso8601,
71
+ directive: 'foreman',
72
+ metadata: {
73
+ 'job_uuid': input[:job_uuid],
74
+ 'username': execution_plan_id,
75
+ 'password': otp_password,
76
+ 'return_url': "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/update",
77
+ },
78
+ content: "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}",
79
+ }
80
+ mqtt_notify payload
81
+ output[:state] = :notified
82
+ end
83
+
84
+ def mqtt_notify(payload)
85
+ MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port) do |c|
86
+ c.publish(mqtt_topic, JSON.dump(payload), false, 1)
87
+ end
88
+ end
89
+
90
+ def host_name
91
+ alternative_names = input.fetch(:alternative_names, {})
92
+
93
+ alternative_names[:consumer_uuid] ||
94
+ alternative_names[:fqdn] ||
95
+ input[:hostname]
96
+ end
97
+
98
+ def mqtt_topic
99
+ "yggdrasil/#{host_name}/data/in"
100
+ end
101
+
102
+ def settings
103
+ Proxy::RemoteExecution::Ssh::Plugin.settings
104
+ end
105
+
106
+ def job_storage
107
+ Proxy::RemoteExecution::Ssh.job_storage
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,34 @@
1
+ require 'smart_proxy_dynflow/action/shareable'
2
+ require 'smart_proxy_dynflow/action/runner'
3
+
4
+ module Proxy::RemoteExecution::Ssh
5
+ module Actions
6
+ class RunScript < ::Dynflow::Action
7
+ def plan(*args)
8
+ mode = Proxy::RemoteExecution::Ssh::Plugin.settings.mode
9
+ case mode
10
+ when :ssh, :'ssh-async'
11
+ plan_action(ScriptRunner, *args)
12
+ when :pull, :'pull-mqtt'
13
+ plan_action(PullScript, *args,
14
+ mqtt: mode == :'pull-mqtt')
15
+ end
16
+ end
17
+ end
18
+
19
+ class ScriptRunner < Proxy::Dynflow::Action::Runner
20
+ def initiate_runner
21
+ additional_options = {
22
+ :step_id => run_step_id,
23
+ :uuid => execution_plan_id,
24
+ }
25
+ Proxy::RemoteExecution::Ssh::Plugin.runner_class.build(input.merge(additional_options),
26
+ suspended_action: suspended_action)
27
+ end
28
+
29
+ def runner_dispatcher
30
+ Dispatcher.instance
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ module Proxy::RemoteExecution::Ssh
2
+ module Actions
3
+ require 'smart_proxy_remote_execution_ssh/actions/run_script'
4
+ require 'smart_proxy_remote_execution_ssh/actions/pull_script'
5
+ end
6
+ end
@@ -1,11 +1,13 @@
1
1
  require 'net/ssh'
2
2
  require 'base64'
3
+ require 'smart_proxy_dynflow/runner'
3
4
 
4
5
  module Proxy::RemoteExecution
5
6
  module Ssh
6
7
 
7
8
  class Api < ::Sinatra::Base
8
9
  include Sinatra::Authorization::Helpers
10
+ include Proxy::Dynflow::Helpers
9
11
 
10
12
  get "/pubkey" do
11
13
  File.read(Ssh.public_key_file)
@@ -37,6 +39,53 @@ module Proxy::RemoteExecution
37
39
  end
38
40
  204
39
41
  end
42
+
43
+ # Payload is a hash where
44
+ # exit_code: Integer | NilClass
45
+ # output: String
46
+ post '/jobs/:job_uuid/update' do |job_uuid|
47
+ do_authorize_with_ssl_client
48
+
49
+ with_authorized_job(job_uuid) do |job_record|
50
+ data = MultiJson.load(request.body.read)
51
+ notify_job(job_record, ::Proxy::Dynflow::Runner::ExternalEvent.new(data))
52
+ end
53
+ end
54
+
55
+ get '/jobs' do
56
+ do_authorize_with_ssl_client
57
+
58
+ MultiJson.dump(Proxy::RemoteExecution::Ssh.job_storage.job_uuids_for_host(https_cert_cn))
59
+ end
60
+
61
+ get "/jobs/:job_uuid" do |job_uuid|
62
+ do_authorize_with_ssl_client
63
+
64
+ with_authorized_job(job_uuid) do |job_record|
65
+ notify_job(job_record, Actions::PullScript::JobDelivered)
66
+ job_record[:job]
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def notify_job(job_record, event)
73
+ world.event(job_record[:execution_plan_uuid], job_record[:run_step_id], event)
74
+ end
75
+
76
+ def with_authorized_job(uuid)
77
+ if (job = authorized_job(uuid))
78
+ yield job
79
+ else
80
+ halt 404
81
+ end
82
+ end
83
+
84
+ def authorized_job(uuid)
85
+ job_record = Proxy::RemoteExecution::Ssh.job_storage.find_job(uuid) || {}
86
+ return job_record if authorize_with_token(clear: false, task_id: job_record[:execution_plan_uuid]) ||
87
+ job_record[:hostname] == https_cert_cn
88
+ end
40
89
  end
41
90
  end
42
91
  end
@@ -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