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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3e83401d99929917142c15969fce19bf17e6fffc82000da554c457567016f8d
4
- data.tar.gz: 9dbdec23d2203557204fb60f45cadc884d640d49b436eb291ad0cc4769182e2c
3
+ metadata.gz: e58a285a16b7d9a173ee4c29639b3367517c5d84f7979bdc84ab2c502a51edb1
4
+ data.tar.gz: 57892ef63dc0a6bd156ae6fef562b8a4096a7a3cd6a2a03828b4598b1b1d37c7
5
5
  SHA512:
6
- metadata.gz: 3cc070d29a185dc80cee33c3b69bf06772c226721ed9f42fba167a702a917ac04b50729ee3354d430b2a7c840581e98b75236cb4298e77aeee749fd9af6f56f2
7
- data.tar.gz: a040bf6621ea2e35b421bc9098a5ec2569bc24e51ea6ae5346eec8c9e05d91e2560b96843d553589f8bbab10f009725947e76b306c5bb785c16ae0f3006baed8
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
- input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script])
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(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']
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?(local_command_file("socket"))
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=#{local_command_file("socket")}", "-O", "exit"].flatten
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=#{local_command_file("socket")}"
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, publish: false, close_stdin: true, tty: false)
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
- if publish
315
- pm.on_stdout { |data| publish_data(data, 'stdout', pm); '' }
316
- pm.on_stderr { |data| publish_data(data, 'stderr', pm); '' }
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
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
6
6
  end
7
7
  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? { |key| ::Proxy::SETTINGS[key] }
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
@@ -3,6 +3,7 @@
3
3
  :ssh_identity_key_file: '~/.ssh/id_rsa_foreman_proxy'
4
4
  :local_working_dir: '/var/tmp'
5
5
  :remote_working_dir: '/var/tmp'
6
+ :socket_working_dir: '/var/tmp'
6
7
  # :kerberos_auth: false
7
8
 
8
9
  # :cockpit_integration: true
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.6.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-04-19 00:00:00.000000000 Z
11
+ date: 2022-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler