smart_proxy_remote_execution_ssh 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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