smart_proxy_remote_execution_ssh 0.3.2 → 0.5.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: a04bba60b9857717faf87e96b17f1734feb66dab9ab009517843d843868791ec
4
- data.tar.gz: 27a2bc2d81c77709a4a414f04c7ee1dbcbdbe532727c2083b2734384e4b80c3e
3
+ metadata.gz: 2515f08b67e5b567e50182d278d73d2db8466e7a12cb6e8000ad8e4f8ce6bf65
4
+ data.tar.gz: 686ba55bf862fc81f3154ebadbaafa3702416109e81641bdcb2442873c5a451b
5
5
  SHA512:
6
- metadata.gz: 3b9bd1e86b27273b27de65083052611fc4de5eb012705f2f0b31a5e77843b647d81fd67c10bd28abcb22d928e1db65bb6c18131fb872cb4bdaac4fd319061dde
7
- data.tar.gz: 20596fd3597915a897eac49051c45228c12fb91753164c23d64b8f7cccefbe1b6567f4e2b5620edebe7108bf7063b61ea037c55ecdc7d87c4177862ca56413fc
6
+ metadata.gz: 9f9e84c78cab2997b711ee559440ad9323c6c3605726f6360a94415ccda3f7a08db0450fba8fe5479e06b649a8e6e8e545be72f35751762c50bf8ef5bc32696e
7
+ data.tar.gz: 0705716e01985cd8b5a3aa2ffd4104f6d4ce0938dc8a0e9b105f65f149d62c1c030916250cf0f1c35ed51bfaceb36964d0b959e5e13f42c70fb0a42389a01a2e
@@ -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