smart_proxy_remote_execution_ssh 0.10.0 → 0.10.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/smart_proxy_remote_execution_ssh/actions/pull_script.rb +56 -18
- data/lib/smart_proxy_remote_execution_ssh/api.rb +8 -0
- data/lib/smart_proxy_remote_execution_ssh/mqtt/dispatcher.rb +28 -10
- data/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb +4 -1
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +9 -4
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +4 -1
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e155b0459ac25c633c9c38de0d288b22779f5ea7f6d82146e5505b83cfd7e12e
|
4
|
+
data.tar.gz: 700810a05b8fac1b57ebcc4ca52c5c2de45575dac1b81fe9b84f5c066c6f1dad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11d92de711f06ee33deabf98442e5eb998a24cbd78f8b548a977cd2189a19e7ee998106d8935dbbfe7f1d7b74cc0fc7eb924430fb6f6a98b9565bcde09f917fd
|
7
|
+
data.tar.gz: f23cce7baef47c6651c1ef894b7f39d27b6b043153ed04e388940bab4898633689acd5aa66587be5f01651037d754357c8dd79f162e3d8a72a3430d6f7294efd
|
@@ -4,9 +4,20 @@ require 'time'
|
|
4
4
|
|
5
5
|
module Proxy::RemoteExecution::Ssh::Actions
|
6
6
|
class PullScript < Proxy::Dynflow::Action::Runner
|
7
|
+
include ::Dynflow::Action::Timeouts
|
8
|
+
|
7
9
|
JobDelivered = Class.new
|
8
10
|
PickupTimeout = Class.new
|
9
11
|
|
12
|
+
# The proxy has the job stored in its job storage
|
13
|
+
READY_FOR_PICKUP = 'ready_for_pickup'
|
14
|
+
# The host was notified over MQTT at least once
|
15
|
+
NOTIFIED = 'notified'
|
16
|
+
# The host has picked up the job
|
17
|
+
DELIVERED = 'delivered'
|
18
|
+
# We received at least one output from the host
|
19
|
+
RUNNING = 'running'
|
20
|
+
|
10
21
|
execution_plan_hooks.use :cleanup, :on => :stopped
|
11
22
|
|
12
23
|
def plan(action_input, mqtt: false)
|
@@ -16,14 +27,22 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
16
27
|
|
17
28
|
def run(event = nil)
|
18
29
|
if event == JobDelivered
|
19
|
-
output[:state] =
|
30
|
+
output[:state] = DELIVERED
|
20
31
|
suspend
|
21
32
|
elsif event == PickupTimeout
|
22
33
|
process_pickup_timeout
|
34
|
+
elsif event == ::Dynflow::Action::Timeouts::Timeout
|
35
|
+
process_timeout
|
36
|
+
elsif event.nil?
|
37
|
+
if (timeout = input['execution_timeout_interval'])
|
38
|
+
schedule_timeout(timeout, optional: true)
|
39
|
+
end
|
40
|
+
super
|
23
41
|
else
|
24
42
|
super
|
25
43
|
end
|
26
44
|
rescue => e
|
45
|
+
cleanup
|
27
46
|
action_logger.error(e)
|
28
47
|
process_update(Proxy::Dynflow::Runner::Update.encode_exception('Proxy error', e))
|
29
48
|
end
|
@@ -35,8 +54,10 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
35
54
|
|
36
55
|
plan_event(PickupTimeout, input[:time_to_pickup], optional: true) if input[:time_to_pickup]
|
37
56
|
|
38
|
-
input[:job_uuid] =
|
39
|
-
|
57
|
+
input[:job_uuid] =
|
58
|
+
job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''),
|
59
|
+
effective_user: input[:effective_user])
|
60
|
+
output[:state] = READY_FOR_PICKUP
|
40
61
|
output[:result] = []
|
41
62
|
|
42
63
|
mqtt_start(otp_password) if input[:with_mqtt]
|
@@ -50,7 +71,7 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
50
71
|
end
|
51
72
|
|
52
73
|
def process_external_event(event)
|
53
|
-
output[:state] =
|
74
|
+
output[:state] = RUNNING
|
54
75
|
data = event.data
|
55
76
|
case data['version']
|
56
77
|
when nil
|
@@ -64,7 +85,11 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
64
85
|
|
65
86
|
def process_external_unversioned(payload)
|
66
87
|
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
67
|
-
|
88
|
+
if payload.key?('output')
|
89
|
+
Array(payload['output']).each do |line|
|
90
|
+
continuous_output.add_output(line, payload['type'])
|
91
|
+
end
|
92
|
+
end
|
68
93
|
exit_code = payload['exit_code'].to_i if payload['exit_code']
|
69
94
|
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
70
95
|
end
|
@@ -89,19 +114,34 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
89
114
|
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
90
115
|
end
|
91
116
|
|
92
|
-
def
|
117
|
+
def process_timeout
|
118
|
+
kill_run "Execution timeout exceeded"
|
119
|
+
end
|
120
|
+
|
121
|
+
def kill_run(fail_msg = 'The job was cancelled by the user')
|
122
|
+
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
123
|
+
exit_code = nil
|
124
|
+
|
93
125
|
case output[:state]
|
94
|
-
when
|
126
|
+
when READY_FOR_PICKUP, NOTIFIED
|
95
127
|
# If the job is not running yet on the client, wipe it from storage
|
96
128
|
cleanup
|
97
|
-
|
98
|
-
when
|
129
|
+
exit_code = 'EXCEPTION'
|
130
|
+
when DELIVERED, RUNNING
|
99
131
|
# Client was notified or is already running, dealing with this situation
|
100
132
|
# is only supported if mqtt is available
|
101
133
|
# Otherwise we have to wait it out
|
102
|
-
|
134
|
+
if input[:with_mqtt]
|
135
|
+
mqtt_cancel
|
136
|
+
fail_msg += ', notifying the host over MQTT'
|
137
|
+
else
|
138
|
+
fail_msg += ', however the job was triggered without MQTT and cannot be stopped'
|
139
|
+
end
|
103
140
|
end
|
104
|
-
|
141
|
+
continuous_output.add_output(fail_msg + "\n")
|
142
|
+
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
143
|
+
|
144
|
+
suspend unless exit_code
|
105
145
|
end
|
106
146
|
|
107
147
|
def mqtt_start(otp_password)
|
@@ -118,12 +158,12 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
118
158
|
},
|
119
159
|
)
|
120
160
|
Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.new(input[:job_uuid], mqtt_topic, payload)
|
121
|
-
output[:state] =
|
161
|
+
output[:state] = NOTIFIED
|
122
162
|
end
|
123
163
|
|
124
164
|
def mqtt_cancel
|
125
|
-
cleanup
|
126
165
|
payload = mqtt_payload_base.merge(
|
166
|
+
content: "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/cancel",
|
127
167
|
metadata: {
|
128
168
|
'event': 'cancel',
|
129
169
|
'job_uuid': input[:job_uuid]
|
@@ -163,11 +203,9 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
163
203
|
end
|
164
204
|
|
165
205
|
def process_pickup_timeout
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
suspend
|
170
|
-
end
|
206
|
+
suspend unless [READY_FOR_PICKUP, NOTIFIED].include? output[:state]
|
207
|
+
|
208
|
+
kill_run 'The job was not picked up in time'
|
171
209
|
end
|
172
210
|
end
|
173
211
|
end
|
@@ -71,6 +71,14 @@ module Proxy::RemoteExecution
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
+
get "/jobs/:job_uuid/cancel" do |job_uuid|
|
75
|
+
do_authorize_with_ssl_client
|
76
|
+
|
77
|
+
with_authorized_job(job_uuid) do |job_record|
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
74
82
|
private
|
75
83
|
|
76
84
|
def notify_job(job_record, event)
|
@@ -2,15 +2,39 @@ require 'concurrent'
|
|
2
2
|
require 'mqtt'
|
3
3
|
|
4
4
|
class Proxy::RemoteExecution::Ssh::MQTT
|
5
|
+
class DispatcherSupervisor < Concurrent::Actor::RestartingContext
|
6
|
+
def initialize
|
7
|
+
limit = Proxy::RemoteExecution::Ssh::Plugin.settings[:mqtt_rate_limit]
|
8
|
+
@dispatcher = DispatcherActor.spawn('MQTT dispatcher',
|
9
|
+
Proxy::Dynflow::Core.world.clock,
|
10
|
+
limit)
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_message(message)
|
14
|
+
case message
|
15
|
+
when :dispatcher_reference
|
16
|
+
@dispatcher
|
17
|
+
when :resumed
|
18
|
+
# Carry on
|
19
|
+
else
|
20
|
+
pass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# In case an exception is raised during processing, instruct concurrent-ruby
|
25
|
+
# to keep going without losing state
|
26
|
+
def behaviour_definition
|
27
|
+
Concurrent::Actor::Behaviour.restarting_behaviour_definition(:resume!)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
5
31
|
class Dispatcher
|
6
32
|
include Singleton
|
7
33
|
|
8
34
|
attr_reader :reference
|
9
35
|
def initialize
|
10
|
-
|
11
|
-
@reference =
|
12
|
-
Proxy::Dynflow::Core.world.clock,
|
13
|
-
limit)
|
36
|
+
@supervisor = DispatcherSupervisor.spawn(name: 'RestartingSupervisor', args: [])
|
37
|
+
@reference = @supervisor.ask!(:dispatcher_reference)
|
14
38
|
end
|
15
39
|
|
16
40
|
def new(uuid, topic, payload)
|
@@ -159,11 +183,5 @@ class Proxy::RemoteExecution::Ssh::MQTT
|
|
159
183
|
def timer_off
|
160
184
|
@timer.shutdown
|
161
185
|
end
|
162
|
-
|
163
|
-
# In case an exception is raised during processing, instruct concurrent-ruby
|
164
|
-
# to keep going without losing state
|
165
|
-
def behaviour_definition
|
166
|
-
Concurrent::Actor::Behaviour.restarting_behaviour_definition(:resume!)
|
167
|
-
end
|
168
186
|
end
|
169
187
|
end
|
@@ -103,7 +103,8 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
103
103
|
# does not close its stderr which trips up the process manager which
|
104
104
|
# expects all FDs to be closed
|
105
105
|
|
106
|
-
full_command = [method.ssh_command_prefix, 'ssh', establish_ssh_options, method.ssh_options, @host,
|
106
|
+
full_command = [method.ssh_command_prefix, 'ssh', establish_ssh_options, method.ssh_options, @host,
|
107
|
+
'true'].flatten
|
107
108
|
log_command(full_command)
|
108
109
|
pm = Proxy::Dynflow::ProcessManager.new(full_command)
|
109
110
|
pm.start!
|
@@ -154,6 +155,8 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
154
155
|
ssh_options << "-o ControlPath=#{socket_file}"
|
155
156
|
ssh_options << "-o ControlPersist=yes"
|
156
157
|
ssh_options << "-o ProxyCommand=none"
|
158
|
+
ssh_options << "-o ServerAliveInterval=15"
|
159
|
+
ssh_options << "-o ServerAliveCountMax=3" # This is the default, but let's be explicit
|
157
160
|
@establish_ssh_options = ssh_options
|
158
161
|
end
|
159
162
|
|
@@ -90,7 +90,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
90
90
|
def reset; end
|
91
91
|
end
|
92
92
|
|
93
|
-
# rubocop:disable Metrics/ClassLength
|
94
93
|
class ScriptRunner < Proxy::Dynflow::Runner::Base
|
95
94
|
include Proxy::Dynflow::Runner::ProcessManagerCommand
|
96
95
|
include CommandLogging
|
@@ -180,7 +179,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
180
179
|
@output_path = File.join(File.dirname(@remote_script), 'output')
|
181
180
|
@exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
|
182
181
|
@pid_path = File.join(File.dirname(@remote_script), 'pid')
|
183
|
-
@remote_script_wrapper = upload_data(
|
182
|
+
@remote_script_wrapper = upload_data(
|
183
|
+
"echo $$ > #{@pid_path}; exec \"$@\";",
|
184
|
+
File.join(File.dirname(@remote_script), 'script-wrapper'),
|
185
|
+
555)
|
184
186
|
end
|
185
187
|
|
186
188
|
# the script that initiates the execution
|
@@ -192,7 +194,11 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
192
194
|
#{@remote_script_wrapper} #{@user_method.cli_command_prefix}#{su_method ? "'#{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}
|
193
195
|
echo \\$?>#{@exit_code_path}
|
194
196
|
EOF
|
195
|
-
|
197
|
+
if [ -f #{@exit_code_path} ] && [ $(wc -l < #{@exit_code_path}) -gt 0 ]; then
|
198
|
+
exit $(cat #{@exit_code_path})
|
199
|
+
else
|
200
|
+
exit 1
|
201
|
+
fi
|
196
202
|
SCRIPT
|
197
203
|
end
|
198
204
|
|
@@ -394,5 +400,4 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
394
400
|
end
|
395
401
|
end
|
396
402
|
end
|
397
|
-
# rubocop:enable Metrics/ClassLength
|
398
403
|
end
|
@@ -50,7 +50,10 @@ module Proxy::RemoteExecution
|
|
50
50
|
raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
|
51
51
|
|
52
52
|
if Plugin.settings.mqtt_tls.nil?
|
53
|
-
Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key],
|
53
|
+
Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key],
|
54
|
+
[:foreman_ssl_ca, :ssl_ca_file]].all? do |(client, server)|
|
55
|
+
::Proxy::SETTINGS[client] || ::Proxy::SETTINGS[server]
|
56
|
+
end
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_proxy_remote_execution_ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.10.
|
4
|
+
version: 0.10.2
|
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: 2023-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '2.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '2.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: webmock
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,7 +192,7 @@ homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
|
|
192
192
|
licenses:
|
193
193
|
- GPL-3.0
|
194
194
|
metadata: {}
|
195
|
-
post_install_message:
|
195
|
+
post_install_message:
|
196
196
|
rdoc_options: []
|
197
197
|
require_paths:
|
198
198
|
- lib
|
@@ -207,8 +207,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
207
207
|
- !ruby/object:Gem::Version
|
208
208
|
version: '0'
|
209
209
|
requirements: []
|
210
|
-
rubygems_version: 3.
|
211
|
-
signing_key:
|
210
|
+
rubygems_version: 3.1.6
|
211
|
+
signing_key:
|
212
212
|
specification_version: 4
|
213
213
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|
214
214
|
test_files: []
|