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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3e83401d99929917142c15969fce19bf17e6fffc82000da554c457567016f8d
4
- data.tar.gz: 9dbdec23d2203557204fb60f45cadc884d640d49b436eb291ad0cc4769182e2c
3
+ metadata.gz: f032dc665671e9aa070cc127201147fc2577d5252bf8b9afc6bd9e04c77e96e5
4
+ data.tar.gz: 9f8c886f1c8b18646e5ef0e85a967e398cf25fd13b96883eaa9e2275882531a2
5
5
  SHA512:
6
- metadata.gz: 3cc070d29a185dc80cee33c3b69bf06772c226721ed9f42fba167a702a917ac04b50729ee3354d430b2a7c840581e98b75236cb4298e77aeee749fd9af6f56f2
7
- data.tar.gz: a040bf6621ea2e35b421bc9098a5ec2569bc24e51ea6ae5346eec8c9e05d91e2560b96843d553589f8bbab10f009725947e76b306c5bb785c16ae0f3006baed8
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
- 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
 
@@ -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, [buf_socket, in_buf], nil, 300)
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 output.length.positive?
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 output.length.positive?
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-ssh pull pull-mqtt].freeze
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?(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,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=#{local_command_file("socket")}"
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, publish: false, close_stdin: true, tty: false)
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
- if publish
315
- pm.on_stdout { |data| publish_data(data, 'stdout', pm); '' }
316
- pm.on_stderr { |data| publish_data(data, 'stderr', pm); '' }
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
@@ -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.2'
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.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-04-19 00:00:00.000000000 Z
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.1.4
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