smart_proxy_remote_execution_ssh 0.6.0 → 0.7.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 +4 -4
- data/lib/smart_proxy_remote_execution_ssh/actions/pull_script.rb +39 -6
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +5 -0
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +23 -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 +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e58a285a16b7d9a173ee4c29639b3367517c5d84f7979bdc84ab2c502a51edb1
|
4
|
+
data.tar.gz: 57892ef63dc0a6bd156ae6fef562b8a4096a7a3cd6a2a03828b4598b1b1d37c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89cc41ef35aa11210f322afc3b7ae35aa269cc4ed8fcef8faff11e32ac37fe765cc083d0bdef2b9c58a54f96be5c8224c5916b85cbb393040f6e715816bf51b7
|
7
|
+
data.tar.gz: ec608d3b666443e6afbb97d28f1014f459d746d65adcf0e1f0cd43d168961e05884f8d3e393814f594527a294d6ac56d2af432509b8ecb99cfcf0df7101f501b
|
@@ -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
|
|
@@ -2,6 +2,10 @@ module Proxy::RemoteExecution::Ssh
|
|
2
2
|
class Plugin < Proxy::Plugin
|
3
3
|
SSH_LOG_LEVELS = %w[debug info error fatal].freeze
|
4
4
|
MODES = %i[ssh async-ssh 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,9 +275,9 @@ 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"
|
283
282
|
end
|
284
283
|
|
@@ -286,11 +285,11 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
286
285
|
Proxy::RemoteExecution::Ssh::Plugin.settings
|
287
286
|
end
|
288
287
|
|
289
|
-
def get_args(command, with_pty = false)
|
288
|
+
def get_args(command, with_pty = false, quiet: false)
|
290
289
|
args = []
|
291
290
|
args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
|
292
291
|
args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
|
293
|
-
args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
|
292
|
+
args += ['/usr/bin/ssh', @host, ssh_options(with_pty, quiet: quiet), command].flatten
|
294
293
|
end
|
295
294
|
|
296
295
|
# Initiates run of the remote command and yields the data when
|
@@ -300,7 +299,7 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
300
299
|
raise 'Async command already in progress' if @process_manager&.started?
|
301
300
|
|
302
301
|
@user_method.reset
|
303
|
-
initialize_command(*get_args(command, true))
|
302
|
+
initialize_command(*get_args(command, true, quiet: true))
|
304
303
|
|
305
304
|
true
|
306
305
|
end
|
@@ -309,12 +308,17 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
309
308
|
@process_manager&.started? && @user_method.sent_all_data?
|
310
309
|
end
|
311
310
|
|
312
|
-
def run_sync(command, stdin: nil,
|
311
|
+
def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
|
313
312
|
pm = Proxy::Dynflow::ProcessManager.new(get_args(command, tty))
|
314
|
-
|
315
|
-
|
316
|
-
|
313
|
+
callback = proc do |data|
|
314
|
+
data.each_line do |line|
|
315
|
+
logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
|
316
|
+
user_method.on_data(data, pm.stdin) if user_method
|
317
|
+
end
|
318
|
+
''
|
317
319
|
end
|
320
|
+
pm.on_stdout(&callback)
|
321
|
+
pm.on_stderr(&callback)
|
318
322
|
pm.start!
|
319
323
|
unless pm.status
|
320
324
|
pm.stdin.io.puts(stdin) if stdin
|
@@ -340,6 +344,10 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
340
344
|
File.join(ensure_local_directory(local_command_dir), filename)
|
341
345
|
end
|
342
346
|
|
347
|
+
def socket_file
|
348
|
+
File.join(ensure_local_directory(@socket_working_dir), @id)
|
349
|
+
end
|
350
|
+
|
343
351
|
def remote_command_dir
|
344
352
|
File.join(@remote_working_dir, "foreman-ssh-cmd-#{id}")
|
345
353
|
end
|
@@ -372,7 +380,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
372
380
|
|
373
381
|
@logger.debug("Sending data to #{path} on remote host:\n#{data}")
|
374
382
|
ensure_remote_command(command,
|
375
|
-
publish: true,
|
376
383
|
stdin: data,
|
377
384
|
error: "Unable to upload file to #{path} on remote system, exit code: %{exit_code}"
|
378
385
|
)
|
@@ -388,7 +395,6 @@ module Proxy::RemoteExecution::Ssh::Runners
|
|
388
395
|
|
389
396
|
def ensure_remote_directory(path)
|
390
397
|
ensure_remote_command("mkdir -p #{path}",
|
391
|
-
publish: true,
|
392
398
|
error: "Unable to create directory #{path} on remote system, exit code: %{exit_code}"
|
393
399
|
)
|
394
400
|
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.0
|
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-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|