smart_proxy_remote_execution_ssh 0.10.0 → 0.10.2
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/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: []
|