smart_proxy_remote_execution_ssh 0.6.0 → 0.7.2

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: 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