smart_proxy_remote_execution_ssh 0.6.0 → 0.7.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 +39 -6
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +3 -2
- data/lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb +6 -2
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +6 -1
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +24 -17
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +9 -1
- data/settings.d/remote_execution_ssh.yml.example +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f032dc665671e9aa070cc127201147fc2577d5252bf8b9afc6bd9e04c77e96e5
|
|
4
|
+
data.tar.gz: 9f8c886f1c8b18646e5ef0e85a967e398cf25fd13b96883eaa9e2275882531a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c8635ee16b4928f3e82bd5701a2492e434ea08b200719d5232874ee63b5b89f2d86a51913cd959eecba9c6ecc292be2070cd4473c8ced818aa7ede45e2d53eb
|
|
7
|
+
data.tar.gz: e6bbc045c00a4857aac5421953c6915bb0ba95238bad2a0ce0a2eed1269b538a704f474306107ca23dbf25b416ae19d98605fd4b93c7b4566941079e047cdc19
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'mqtt'
|
|
2
2
|
require 'json'
|
|
3
|
+
require 'time'
|
|
3
4
|
|
|
4
5
|
module Proxy::RemoteExecution::Ssh::Actions
|
|
5
6
|
class PullScript < Proxy::Dynflow::Action::Runner
|
|
@@ -25,7 +26,8 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
25
26
|
otp_password = if input[:with_mqtt]
|
|
26
27
|
::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
|
|
27
28
|
end
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''))
|
|
29
31
|
output[:state] = :ready_for_pickup
|
|
30
32
|
output[:result] = []
|
|
31
33
|
mqtt_start(otp_password) if input[:with_mqtt]
|
|
@@ -40,9 +42,40 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
40
42
|
def process_external_event(event)
|
|
41
43
|
output[:state] = :running
|
|
42
44
|
data = event.data
|
|
45
|
+
case data['version']
|
|
46
|
+
when nil
|
|
47
|
+
process_external_unversioned(data)
|
|
48
|
+
when '1'
|
|
49
|
+
process_external_v1(data)
|
|
50
|
+
else
|
|
51
|
+
raise "Unexpected update message version '#{data['version']}'"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def process_external_unversioned(payload)
|
|
43
56
|
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
|
44
|
-
Array(
|
|
45
|
-
exit_code =
|
|
57
|
+
Array(payload['output']).each { |line| continuous_output.add_output(line, payload['type']) } if payload.key?('output')
|
|
58
|
+
exit_code = payload['exit_code'].to_i if payload['exit_code']
|
|
59
|
+
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_external_v1(payload)
|
|
63
|
+
continuous_output = Proxy::Dynflow::ContinuousOutput.new
|
|
64
|
+
exit_code = nil
|
|
65
|
+
|
|
66
|
+
payload['updates'].each do |update|
|
|
67
|
+
time = Time.parse update['timestamp']
|
|
68
|
+
type = update['type']
|
|
69
|
+
case type
|
|
70
|
+
when 'output'
|
|
71
|
+
continuous_output.add_output(update['content'], update['stream'], time)
|
|
72
|
+
when 'exit'
|
|
73
|
+
exit_code = update['exit_code'].to_i
|
|
74
|
+
else
|
|
75
|
+
raise "Unexpected update type '#{update['type']}'"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
46
79
|
process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
|
|
47
80
|
end
|
|
48
81
|
|
|
@@ -96,9 +129,9 @@ module Proxy::RemoteExecution::Ssh::Actions
|
|
|
96
129
|
def with_mqtt_client(&block)
|
|
97
130
|
MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port,
|
|
98
131
|
:ssl => settings.mqtt_tls,
|
|
99
|
-
:cert_file => ::Proxy::SETTINGS.foreman_ssl_cert,
|
|
100
|
-
:key_file => ::Proxy::SETTINGS.foreman_ssl_key,
|
|
101
|
-
:ca_file => ::Proxy::SETTINGS.foreman_ssl_ca,
|
|
132
|
+
:cert_file => ::Proxy::SETTINGS.foreman_ssl_cert || ::Proxy::SETTINGS.ssl_certificate,
|
|
133
|
+
:key_file => ::Proxy::SETTINGS.foreman_ssl_key || ::Proxy::SETTINGS.ssl_private_key,
|
|
134
|
+
:ca_file => ::Proxy::SETTINGS.foreman_ssl_ca || ::Proxy::SETTINGS.ssl_ca_file,
|
|
102
135
|
&block)
|
|
103
136
|
end
|
|
104
137
|
|
|
@@ -180,10 +180,11 @@ module Proxy::RemoteExecution
|
|
|
180
180
|
|
|
181
181
|
def inner_system_ssh_loop(out_buf, err_buf, in_buf, pid)
|
|
182
182
|
err_buf_raw = ''
|
|
183
|
-
readers = [buf_socket, out_buf, err_buf]
|
|
184
183
|
loop do
|
|
184
|
+
readers = [buf_socket, out_buf, err_buf].reject { |io| io.closed? }
|
|
185
|
+
writers = [buf_socket, in_buf].select { |io| io.pending_writes? }
|
|
185
186
|
# Prime the sockets for reading
|
|
186
|
-
ready_readers, ready_writers = IO.select(readers,
|
|
187
|
+
ready_readers, ready_writers = IO.select(readers, writers)
|
|
187
188
|
(ready_readers || []).each { |reader| reader.close if reader.fill.zero? }
|
|
188
189
|
|
|
189
190
|
proxy_data(out_buf, in_buf)
|
|
@@ -173,10 +173,14 @@ module Proxy::RemoteExecution
|
|
|
173
173
|
output.append(data)
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def pending_writes?
|
|
177
|
+
output.length.positive?
|
|
178
|
+
end
|
|
179
|
+
|
|
176
180
|
# Sends as much of the pending output as possible. Returns +true+ if any
|
|
177
181
|
# data was sent, and +false+ otherwise.
|
|
178
182
|
def send_pending
|
|
179
|
-
if
|
|
183
|
+
if pending_writes?
|
|
180
184
|
sent = send(output.to_s, 0)
|
|
181
185
|
output.consume!(sent)
|
|
182
186
|
return sent.positive?
|
|
@@ -189,7 +193,7 @@ module Proxy::RemoteExecution
|
|
|
189
193
|
# buffer is empty.
|
|
190
194
|
def wait_for_pending_sends
|
|
191
195
|
send_pending
|
|
192
|
-
while
|
|
196
|
+
while pending_writes?
|
|
193
197
|
result = IO.select(nil, [self]) || next
|
|
194
198
|
next unless result[1].any?
|
|
195
199
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Proxy::RemoteExecution::Ssh
|
|
2
2
|
class Plugin < Proxy::Plugin
|
|
3
3
|
SSH_LOG_LEVELS = %w[debug info error fatal].freeze
|
|
4
|
-
MODES = %i[ssh async
|
|
4
|
+
MODES = %i[ssh ssh-async pull pull-mqtt].freeze
|
|
5
|
+
# Unix domain socket path length is limited to 104 (on some platforms) characters
|
|
6
|
+
# Socket path is composed of custom path (max 49 characters) + job id (37 characters)
|
|
7
|
+
# + offset(17 characters) + null terminator
|
|
8
|
+
SOCKET_PATH_MAX_LENGTH = 49
|
|
5
9
|
|
|
6
10
|
http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
|
7
11
|
https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
|
@@ -11,6 +15,7 @@ module Proxy::RemoteExecution::Ssh
|
|
|
11
15
|
:ssh_user => 'root',
|
|
12
16
|
:remote_working_dir => '/var/tmp',
|
|
13
17
|
:local_working_dir => '/var/tmp',
|
|
18
|
+
:socket_working_dir => '/var/tmp',
|
|
14
19
|
:kerberos_auth => false,
|
|
15
20
|
:cockpit_integration => true,
|
|
16
21
|
# When set to nil, makes REX use the runner's default interval
|
|
@@ -112,6 +112,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
112
112
|
@client_private_key_file = settings.ssh_identity_key_file
|
|
113
113
|
@local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
|
|
114
114
|
@remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir.shellescape)
|
|
115
|
+
@socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
|
|
115
116
|
@cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
|
|
116
117
|
@first_execution = options.fetch(:first_execution, false)
|
|
117
118
|
@user_method = user_method
|
|
@@ -159,15 +160,14 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
159
160
|
|
|
160
161
|
def preflight_checks
|
|
161
162
|
ensure_remote_command(cp_script_to_remote("#!/bin/sh\nexec true", 'test'),
|
|
162
|
-
publish: true,
|
|
163
163
|
error: 'Failed to execute script on remote machine, exit code: %{exit_code}.'
|
|
164
164
|
)
|
|
165
165
|
unless @user_method.is_a? NoopUserMethod
|
|
166
166
|
path = cp_script_to_remote("#!/bin/sh\nexec #{@user_method.cli_command_prefix} true", 'effective-user-test')
|
|
167
167
|
ensure_remote_command(path,
|
|
168
168
|
error: 'Failed to change to effective user, exit code: %{exit_code}',
|
|
169
|
-
publish: true,
|
|
170
169
|
tty: true,
|
|
170
|
+
user_method: @user_method,
|
|
171
171
|
close_stdin: false)
|
|
172
172
|
end
|
|
173
173
|
end
|
|
@@ -178,7 +178,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
178
178
|
# closed
|
|
179
179
|
ensure_remote_command(
|
|
180
180
|
'true',
|
|
181
|
-
publish: true,
|
|
182
181
|
error: 'Failed to establish connection to remote host, exit code: %{exit_code}'
|
|
183
182
|
)
|
|
184
183
|
end
|
|
@@ -231,9 +230,9 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
231
230
|
end
|
|
232
231
|
|
|
233
232
|
def close_session
|
|
234
|
-
raise 'Control socket file does not exist' unless File.exist?(
|
|
233
|
+
raise 'Control socket file does not exist' unless File.exist?(socket_file)
|
|
235
234
|
@logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
|
|
236
|
-
args = ['/usr/bin/ssh', @host, "-o", "ControlPath=#{
|
|
235
|
+
args = ['/usr/bin/ssh', @host, "-o", "ControlPath=#{socket_file}", "-O", "exit"].flatten
|
|
237
236
|
pm = Proxy::Dynflow::ProcessManager.new(args)
|
|
238
237
|
pm.on_stdout { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
|
|
239
238
|
pm.on_stderr { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
|
|
@@ -265,7 +264,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
265
264
|
@process_manager && @cleanup_working_dirs
|
|
266
265
|
end
|
|
267
266
|
|
|
268
|
-
def ssh_options(with_pty = false)
|
|
267
|
+
def ssh_options(with_pty = false, quiet: false)
|
|
269
268
|
ssh_options = []
|
|
270
269
|
ssh_options << "-tt" if with_pty
|
|
271
270
|
ssh_options << "-o User=#{@ssh_user}"
|
|
@@ -276,21 +275,22 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
276
275
|
ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
|
|
277
276
|
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
|
|
278
277
|
ssh_options << "-o NumberOfPasswordPrompts=1"
|
|
279
|
-
ssh_options << "-o LogLevel=#{settings[:ssh_log_level]}"
|
|
278
|
+
ssh_options << "-o LogLevel=#{quiet ? 'quiet' : settings[:ssh_log_level]}"
|
|
280
279
|
ssh_options << "-o ControlMaster=auto"
|
|
281
|
-
ssh_options << "-o ControlPath=#{
|
|
280
|
+
ssh_options << "-o ControlPath=#{socket_file}"
|
|
282
281
|
ssh_options << "-o ControlPersist=yes"
|
|
282
|
+
ssh_options << "-o ProxyCommand=none"
|
|
283
283
|
end
|
|
284
284
|
|
|
285
285
|
def settings
|
|
286
286
|
Proxy::RemoteExecution::Ssh::Plugin.settings
|
|
287
287
|
end
|
|
288
288
|
|
|
289
|
-
def get_args(command, with_pty = false)
|
|
289
|
+
def get_args(command, with_pty = false, quiet: false)
|
|
290
290
|
args = []
|
|
291
291
|
args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
|
|
292
292
|
args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
|
|
293
|
-
args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
|
|
293
|
+
args += ['/usr/bin/ssh', @host, ssh_options(with_pty, quiet: quiet), command].flatten
|
|
294
294
|
end
|
|
295
295
|
|
|
296
296
|
# Initiates run of the remote command and yields the data when
|
|
@@ -300,7 +300,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
300
300
|
raise 'Async command already in progress' if @process_manager&.started?
|
|
301
301
|
|
|
302
302
|
@user_method.reset
|
|
303
|
-
initialize_command(*get_args(command, true))
|
|
303
|
+
initialize_command(*get_args(command, true, quiet: true))
|
|
304
304
|
|
|
305
305
|
true
|
|
306
306
|
end
|
|
@@ -309,12 +309,17 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
309
309
|
@process_manager&.started? && @user_method.sent_all_data?
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
-
def run_sync(command, stdin: nil,
|
|
312
|
+
def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
|
|
313
313
|
pm = Proxy::Dynflow::ProcessManager.new(get_args(command, tty))
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
callback = proc do |data|
|
|
315
|
+
data.each_line do |line|
|
|
316
|
+
logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
|
|
317
|
+
user_method.on_data(data, pm.stdin) if user_method
|
|
318
|
+
end
|
|
319
|
+
''
|
|
317
320
|
end
|
|
321
|
+
pm.on_stdout(&callback)
|
|
322
|
+
pm.on_stderr(&callback)
|
|
318
323
|
pm.start!
|
|
319
324
|
unless pm.status
|
|
320
325
|
pm.stdin.io.puts(stdin) if stdin
|
|
@@ -340,6 +345,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
340
345
|
File.join(ensure_local_directory(local_command_dir), filename)
|
|
341
346
|
end
|
|
342
347
|
|
|
348
|
+
def socket_file
|
|
349
|
+
File.join(ensure_local_directory(@socket_working_dir), @id)
|
|
350
|
+
end
|
|
351
|
+
|
|
343
352
|
def remote_command_dir
|
|
344
353
|
File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
|
|
345
354
|
end
|
|
@@ -372,7 +381,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
372
381
|
|
|
373
382
|
@logger.debug("Sending data to #{path} on remote host:\n#{data}")
|
|
374
383
|
ensure_remote_command(command,
|
|
375
|
-
publish: true,
|
|
376
384
|
stdin: data,
|
|
377
385
|
error: "Unable to upload file to #{path} on remote system, exit code: %{exit_code}"
|
|
378
386
|
)
|
|
@@ -388,7 +396,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
|
388
396
|
|
|
389
397
|
def ensure_remote_directory(path)
|
|
390
398
|
ensure_remote_command("mkdir -p #{path}",
|
|
391
|
-
publish: true,
|
|
392
399
|
error: "Unable to create directory #{path} on remote system, exit code: %{exit_code}"
|
|
393
400
|
)
|
|
394
401
|
end
|
|
@@ -10,6 +10,7 @@ module Proxy::RemoteExecution
|
|
|
10
10
|
validate_mode!
|
|
11
11
|
validate_ssh_settings!
|
|
12
12
|
validate_mqtt_settings!
|
|
13
|
+
validate_socket_path!
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def private_key_file
|
|
@@ -49,7 +50,7 @@ module Proxy::RemoteExecution
|
|
|
49
50
|
raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
|
|
50
51
|
|
|
51
52
|
if Plugin.settings.mqtt_tls.nil?
|
|
52
|
-
Plugin.settings.mqtt_tls = [:foreman_ssl_cert, :foreman_ssl_key, :foreman_ssl_ca].all? { |
|
|
53
|
+
Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key], [:foreman_ssl_ca, :ssl_ca_file]].all? { |(client, server)| ::Proxy::SETTINGS[client] || ::Proxy::SETTINGS[server] }
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -96,6 +97,13 @@ module Proxy::RemoteExecution
|
|
|
96
97
|
%i[ssh ssh-async].include?(Plugin.settings.mode) || Plugin.settings.cockpit_integration
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
def validate_socket_path!
|
|
101
|
+
return unless Plugin.settings.mode == :'ssh' || Plugin.settings.mode == :'ssh-async'
|
|
102
|
+
|
|
103
|
+
socket_path = File.expand_path(Plugin.settings.socket_working_dir)
|
|
104
|
+
raise "Socket path #{socket_path} is too long" if socket_path.length > Plugin::SOCKET_PATH_MAX_LENGTH
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
def job_storage
|
|
100
108
|
@job_storage ||= Proxy::RemoteExecution::Ssh::JobStorage.new
|
|
101
109
|
end
|
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.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Nečas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -203,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
203
203
|
- !ruby/object:Gem::Version
|
|
204
204
|
version: '0'
|
|
205
205
|
requirements: []
|
|
206
|
-
rubygems_version: 3.
|
|
206
|
+
rubygems_version: 3.3.20
|
|
207
207
|
signing_key:
|
|
208
208
|
specification_version: 4
|
|
209
209
|
summary: Ssh remote execution provider for Foreman Smart-Proxy
|