smart_proxy_remote_execution_ssh 0.7.0 → 0.8.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: e58a285a16b7d9a173ee4c29639b3367517c5d84f7979bdc84ab2c502a51edb1
4
- data.tar.gz: 57892ef63dc0a6bd156ae6fef562b8a4096a7a3cd6a2a03828b4598b1b1d37c7
3
+ metadata.gz: 1a599d3732d4f6b064a850ae8baced28354980096785a340828533f953f844c4
4
+ data.tar.gz: 160b80ab983f0eb7b987987191b48c19db42d3b76079b0ba6639ec831dccc6f3
5
5
  SHA512:
6
- metadata.gz: 89cc41ef35aa11210f322afc3b7ae35aa269cc4ed8fcef8faff11e32ac37fe765cc083d0bdef2b9c58a54f96be5c8224c5916b85cbb393040f6e715816bf51b7
7
- data.tar.gz: ec608d3b666443e6afbb97d28f1014f459d746d65adcf0e1f0cd43d168961e05884f8d3e393814f594527a294d6ac56d2af432509b8ecb99cfcf0df7101f501b
6
+ metadata.gz: 349961aa474809225d9781bda09687eb23f00db51cca9f758a57e075fa3d19fc07b729569b064887f91bb7f07190152d6ec9cb01e8ccf48df0841e9c3f85d10e
7
+ data.tar.gz: 8ad40859c92f7d1cd089adf0f37bb0aaa8add745cd461c2e1ed5b484d3ae6b856ac0ee53e61c62e89d564a4ba676b22bf5f01618da970986cd60e42591ff6346
@@ -1,5 +1,6 @@
1
1
  require 'smart_proxy_remote_execution_ssh/net_ssh_compat'
2
2
  require 'forwardable'
3
+ require 'securerandom'
3
4
 
4
5
  module Proxy::RemoteExecution
5
6
  module Cockpit
@@ -164,9 +165,8 @@ module Proxy::RemoteExecution
164
165
  out_read, out_write = IO.pipe
165
166
  err_read, err_write = IO.pipe
166
167
 
167
- # Force the script runner to initialize its logger
168
- script_runner.logger
169
- pid = spawn(*script_runner.send(:get_args, command), :in => in_read, :out => out_write, :err => err_write)
168
+ connection.establish!
169
+ pid = spawn(*connection.command(command), :in => in_read, :out => out_write, :err => err_write)
170
170
  [in_read, out_write, err_write].each(&:close)
171
171
 
172
172
  send_start
@@ -176,19 +176,22 @@ module Proxy::RemoteExecution
176
176
  in_buf = MiniSSLBufferedSocket.new(in_write)
177
177
 
178
178
  inner_system_ssh_loop out_buf, err_buf, in_buf, pid
179
+ ensure
180
+ connection.disconnect!
179
181
  end
180
182
 
181
183
  def inner_system_ssh_loop(out_buf, err_buf, in_buf, pid)
182
184
  err_buf_raw = ''
183
- readers = [buf_socket, out_buf, err_buf]
184
185
  loop do
186
+ readers = [buf_socket, out_buf, err_buf].reject { |io| io.closed? }
187
+ writers = [buf_socket, in_buf].select { |io| io.pending_writes? }
185
188
  # Prime the sockets for reading
186
- ready_readers, ready_writers = IO.select(readers, [buf_socket, in_buf], nil, 300)
189
+ ready_readers, ready_writers = IO.select(readers, writers)
187
190
  (ready_readers || []).each { |reader| reader.close if reader.fill.zero? }
188
191
 
189
192
  proxy_data(out_buf, in_buf)
190
193
  if buf_socket.closed?
191
- script_runner.close_session
194
+ connection.disconnect!
192
195
  end
193
196
 
194
197
  if out_buf.closed?
@@ -262,10 +265,10 @@ module Proxy::RemoteExecution
262
265
  params["hostname"]
263
266
  end
264
267
 
265
- def script_runner
266
- @script_runner ||= Proxy::RemoteExecution::Ssh::Runners::ScriptRunner.build(
268
+ def connection
269
+ @connection ||= Proxy::RemoteExecution::Ssh::Runners::MultiplexedSSHConnection.new(
267
270
  runner_params,
268
- suspended_action: nil
271
+ logger: logger
269
272
  )
270
273
  end
271
274
 
@@ -278,6 +281,7 @@ module Proxy::RemoteExecution
278
281
  # For compatibility only
279
282
  ret[:script] = nil
280
283
  ret[:hostname] = host
284
+ ret[:id] = SecureRandom.uuid
281
285
  ret
282
286
  end
283
287
  end
@@ -0,0 +1,23 @@
1
+ # lib/command_logging.rb
2
+
3
+ module Proxy::RemoteExecution::Ssh::Runners
4
+ module CommandLogging
5
+ def log_command(command, label: "Running")
6
+ command = command.join(' ')
7
+ label = "#{label}: " if label
8
+ logger.debug("#{label}#{command}")
9
+ end
10
+
11
+ def set_pm_debug_logging(pm, capture: false, user_method: nil)
12
+ callback = proc do |data|
13
+ data.each_line do |line|
14
+ logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
15
+ user_method.on_data(data, pm.stdin) if user_method
16
+ end
17
+ ''
18
+ end
19
+ pm.on_stdout(&callback)
20
+ pm.on_stderr(&callback)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,196 @@
1
+ require 'smart_proxy_remote_execution_ssh/command_logging'
2
+
3
+ module Proxy::RemoteExecution::Ssh::Runners
4
+ class SensitiveString
5
+ def initialize(value, mask: '*****')
6
+ @value = value
7
+ @mask = mask
8
+ end
9
+
10
+ def inspect
11
+ '"' + to_s + '"'
12
+ end
13
+
14
+ def to_s
15
+ @mask
16
+ end
17
+
18
+ def to_str
19
+ @value
20
+ end
21
+ end
22
+
23
+ class AuthenticationMethod
24
+ attr_reader :name
25
+ def initialize(name, prompt: nil, password: nil)
26
+ @name = name
27
+ @prompt = prompt
28
+ @password = password
29
+ end
30
+
31
+ def ssh_command_prefix
32
+ return [] unless @password
33
+
34
+ prompt = ['-P', @prompt] if @prompt
35
+ [{'SSHPASS' => SensitiveString.new(@password)}, '/usr/bin/sshpass', '-e', prompt].compact
36
+ end
37
+
38
+ def ssh_options
39
+ ["-o PreferredAuthentications=#{name}",
40
+ "-o NumberOfPasswordPrompts=#{@password ? 1 : 0}"]
41
+ end
42
+ end
43
+
44
+ class MultiplexedSSHConnection
45
+ include CommandLogging
46
+
47
+ attr_reader :logger
48
+ def initialize(options, logger:)
49
+ @logger = logger
50
+
51
+ @id = options.fetch(:id)
52
+ @host = options.fetch(:hostname)
53
+ @script = options.fetch(:script)
54
+ @ssh_user = options.fetch(:ssh_user, 'root')
55
+ @ssh_port = options.fetch(:ssh_port, 22)
56
+ @ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
57
+ @key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
58
+ @host_public_key = options.fetch(:host_public_key, nil)
59
+ @verify_host = options.fetch(:verify_host, nil)
60
+ @client_private_key_file = settings.ssh_identity_key_file
61
+
62
+ @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
63
+ @socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
64
+ @socket = nil
65
+ end
66
+
67
+ def establish!
68
+ @available_auth_methods ||= available_authentication_methods
69
+ method = @available_auth_methods.find do |method|
70
+ if try_auth_method(method)
71
+ @available_auth_methods.unshift(method).uniq!
72
+ true
73
+ end
74
+ end
75
+ method || raise("Could not establish connection to remote host using any available authentication method, tried #{@available_auth_methods.map(&:name).join(', ')}")
76
+ end
77
+
78
+ def disconnect!
79
+ return unless connected?
80
+
81
+ cmd = command(%w[-O exit])
82
+ log_command(cmd, label: "Closing shared connection")
83
+ pm = Proxy::Dynflow::ProcessManager.new(cmd)
84
+ set_pm_debug_logging(pm)
85
+ pm.run!
86
+ @socket = nil
87
+ end
88
+
89
+ def connected?
90
+ !@socket.nil?
91
+ end
92
+
93
+ def command(cmd)
94
+ raise "Cannot build command to run over multiplexed connection without having an established connection" unless connected?
95
+
96
+ ['/usr/bin/ssh', reuse_ssh_options, cmd].flatten
97
+ end
98
+
99
+ private
100
+
101
+ def try_auth_method(method)
102
+ # running "ssh -f -N" instead of "ssh true" would be cleaner, but ssh
103
+ # does not close its stderr which trips up the process manager which
104
+ # expects all FDs to be closed
105
+
106
+ full_command = [method.ssh_command_prefix, '/usr/bin/ssh', establish_ssh_options, method.ssh_options, @host, 'true'].flatten
107
+ log_command(full_command)
108
+ pm = Proxy::Dynflow::ProcessManager.new(full_command)
109
+ pm.start!
110
+ if pm.status
111
+ raise pm.stderr.to_s
112
+ else
113
+ set_pm_debug_logging(pm)
114
+ pm.stdin.io.close
115
+ pm.run!
116
+ end
117
+
118
+ if pm.status.zero?
119
+ logger.debug("Established connection using authentication method #{method.name}")
120
+ @socket = socket_file
121
+ true
122
+ else
123
+ logger.debug("Failed to establish connection using authentication method #{method.name}")
124
+ false
125
+ end
126
+ end
127
+
128
+ def settings
129
+ Proxy::RemoteExecution::Ssh::Plugin.settings
130
+ end
131
+
132
+ def available_authentication_methods
133
+ methods = []
134
+ methods << AuthenticationMethod.new('password', password: @ssh_password) if @ssh_password
135
+ if verify_key_passphrase
136
+ methods << AuthenticationMethod.new('publickey', password: @key_passphrase, prompt: 'passphrase')
137
+ end
138
+ methods << AuthenticationMethod.new('gssapi-with-mic') if settings[:kerberos_auth]
139
+ raise "There are no available authentication methods" if methods.empty?
140
+ methods
141
+ end
142
+
143
+ def establish_ssh_options
144
+ return @establish_ssh_options if @establish_ssh_options
145
+ ssh_options = []
146
+ ssh_options << "-o User=#{@ssh_user}"
147
+ ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
148
+ ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
149
+ ssh_options << "-o IdentitiesOnly=yes"
150
+ ssh_options << "-o StrictHostKeyChecking=no"
151
+ ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
152
+ ssh_options << "-o LogLevel=#{ssh_log_level(true)}"
153
+ ssh_options << "-o ControlMaster=auto"
154
+ ssh_options << "-o ControlPath=#{socket_file}"
155
+ ssh_options << "-o ControlPersist=yes"
156
+ ssh_options << "-o ProxyCommand=none"
157
+ @establish_ssh_options = ssh_options
158
+ end
159
+
160
+ def reuse_ssh_options
161
+ ["-o", "ControlPath=#{@socket}", "-o", "LogLevel=#{ssh_log_level(false)}", @host]
162
+ end
163
+
164
+ def socket_file
165
+ File.join(@socket_working_dir, @id)
166
+ end
167
+
168
+ def verify_key_passphrase
169
+ command = ['/usr/bin/ssh-keygen', '-y', '-f', File.expand_path(@client_private_key_file)]
170
+ log_command(command, label: "Checking if private key has passphrase")
171
+ pm = Proxy::Dynflow::ProcessManager.new(command)
172
+ pm.start!
173
+
174
+ raise pm.stderr.to_s if pm.status
175
+
176
+ pm.stdin.io.close
177
+ pm.run!
178
+
179
+ if pm.status.zero?
180
+ logger.debug("Private key is not protected with a passphrase")
181
+ @key_passphrase = nil
182
+ else
183
+ logger.debug("Private key is protected with a passphrase")
184
+ end
185
+
186
+ return true if pm.status.zero? || @key_passphrase
187
+
188
+ logger.debug("Private key is protected with a passphrase, but no passphrase was provided")
189
+ false
190
+ end
191
+
192
+ def ssh_log_level(new_connection)
193
+ new_connection ? settings[:ssh_log_level] : 'quiet'
194
+ end
195
+ end
196
+ end
@@ -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,7 @@
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
5
  # Unix domain socket path length is limited to 104 (on some platforms) characters
6
6
  # Socket path is composed of custom path (max 49 characters) + job id (37 characters)
7
7
  # + offset(17 characters) + null terminator
@@ -50,13 +50,13 @@ module Proxy::RemoteExecution::Ssh::Runners
50
50
  end
51
51
 
52
52
  def refresh
53
+ @connection.establish! unless @connection.connected?
53
54
  begin
54
55
  pm = run_sync("#{@user_method.cli_command_prefix} #{@retrieval_script}")
56
+ process_retrieved_data(pm.stdout.to_s.chomp, pm.stderr.to_s.chomp)
55
57
  rescue StandardError => e
56
58
  @logger.info("Error while connecting to the remote host on refresh: #{e.message}")
57
59
  end
58
-
59
- process_retrieved_data(pm.stdout.to_s.chomp, pm.stderr.to_s.chomp)
60
60
  ensure
61
61
  destroy_session
62
62
  end
@@ -139,7 +139,7 @@ module Proxy::RemoteExecution::Ssh::Runners
139
139
  end
140
140
 
141
141
  def destroy_session
142
- if @session
142
+ if @connection.connected?
143
143
  @logger.debug("Closing session with #{@ssh_user}@#{@host}")
144
144
  close_session
145
145
  end
@@ -1,6 +1,7 @@
1
1
  require 'fileutils'
2
2
  require 'smart_proxy_dynflow/runner/process_manager_command'
3
3
  require 'smart_proxy_dynflow/process_manager'
4
+ require 'smart_proxy_remote_execution_ssh/multiplexed_ssh_connection'
4
5
 
5
6
  module Proxy::RemoteExecution::Ssh::Runners
6
7
  class EffectiveUserMethod
@@ -92,6 +93,8 @@ module Proxy::RemoteExecution::Ssh::Runners
92
93
  # rubocop:disable Metrics/ClassLength
93
94
  class ScriptRunner < Proxy::Dynflow::Runner::Base
94
95
  include Proxy::Dynflow::Runner::ProcessManagerCommand
96
+ include CommandLogging
97
+
95
98
  attr_reader :execution_timeout_interval
96
99
 
97
100
  EXPECTED_POWER_ACTION_MESSAGES = ['restart host', 'shutdown host'].freeze
@@ -103,10 +106,7 @@ module Proxy::RemoteExecution::Ssh::Runners
103
106
  @script = options.fetch(:script)
104
107
  @ssh_user = options.fetch(:ssh_user, 'root')
105
108
  @ssh_port = options.fetch(:ssh_port, 22)
106
- @ssh_password = options.fetch(:secrets, {}).fetch(:ssh_password, nil)
107
- @key_passphrase = options.fetch(:secrets, {}).fetch(:key_passphrase, nil)
108
109
  @host_public_key = options.fetch(:host_public_key, nil)
109
- @verify_host = options.fetch(:verify_host, nil)
110
110
  @execution_timeout_interval = options.fetch(:execution_timeout_interval, nil)
111
111
 
112
112
  @client_private_key_file = settings.ssh_identity_key_file
@@ -116,6 +116,7 @@ module Proxy::RemoteExecution::Ssh::Runners
116
116
  @cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
117
117
  @first_execution = options.fetch(:first_execution, false)
118
118
  @user_method = user_method
119
+ @options = options
119
120
  end
120
121
 
121
122
  def self.build(options, suspended_action:)
@@ -143,7 +144,9 @@ module Proxy::RemoteExecution::Ssh::Runners
143
144
 
144
145
  def start
145
146
  Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
146
- establish_connection
147
+ ensure_local_directory(@socket_working_dir)
148
+ @connection = MultiplexedSSHConnection.new(@options.merge(:id => @id), logger: logger)
149
+ @connection.establish!
147
150
  preflight_checks
148
151
  prepare_start
149
152
  script = initialization_script
@@ -172,16 +175,6 @@ module Proxy::RemoteExecution::Ssh::Runners
172
175
  end
173
176
  end
174
177
 
175
- def establish_connection
176
- # run_sync ['-f', '-N'] would be cleaner, but ssh does not close its
177
- # stderr which trips up the process manager which expects all FDs to be
178
- # closed
179
- ensure_remote_command(
180
- 'true',
181
- error: 'Failed to establish connection to remote host, exit code: %{exit_code}'
182
- )
183
- end
184
-
185
178
  def prepare_start
186
179
  @remote_script = cp_script_to_remote
187
180
  @output_path = File.join(File.dirname(@remote_script), 'output')
@@ -232,11 +225,7 @@ module Proxy::RemoteExecution::Ssh::Runners
232
225
  def close_session
233
226
  raise 'Control socket file does not exist' unless File.exist?(socket_file)
234
227
  @logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
235
- args = ['/usr/bin/ssh', @host, "-o", "ControlPath=#{socket_file}", "-O", "exit"].flatten
236
- pm = Proxy::Dynflow::ProcessManager.new(args)
237
- pm.on_stdout { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
238
- pm.on_stderr { |data| @logger.debug "[close_session]: #{data.chomp}"; data }
239
- pm.run!
228
+ @connection.disconnect!
240
229
  end
241
230
 
242
231
  def close
@@ -264,34 +253,10 @@ module Proxy::RemoteExecution::Ssh::Runners
264
253
  @process_manager && @cleanup_working_dirs
265
254
  end
266
255
 
267
- def ssh_options(with_pty = false, quiet: false)
268
- ssh_options = []
269
- ssh_options << "-tt" if with_pty
270
- ssh_options << "-o User=#{@ssh_user}"
271
- ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
272
- ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
273
- ssh_options << "-o IdentitiesOnly=yes"
274
- ssh_options << "-o StrictHostKeyChecking=no"
275
- ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
276
- ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
277
- ssh_options << "-o NumberOfPasswordPrompts=1"
278
- ssh_options << "-o LogLevel=#{quiet ? 'quiet' : settings[:ssh_log_level]}"
279
- ssh_options << "-o ControlMaster=auto"
280
- ssh_options << "-o ControlPath=#{socket_file}"
281
- ssh_options << "-o ControlPersist=yes"
282
- end
283
-
284
256
  def settings
285
257
  Proxy::RemoteExecution::Ssh::Plugin.settings
286
258
  end
287
259
 
288
- def get_args(command, with_pty = false, quiet: false)
289
- args = []
290
- args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
291
- args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
292
- args += ['/usr/bin/ssh', @host, ssh_options(with_pty, quiet: quiet), command].flatten
293
- end
294
-
295
260
  # Initiates run of the remote command and yields the data when
296
261
  # available. The yielding doesn't happen automatically, but as
297
262
  # part of calling the `refresh` method.
@@ -299,7 +264,9 @@ module Proxy::RemoteExecution::Ssh::Runners
299
264
  raise 'Async command already in progress' if @process_manager&.started?
300
265
 
301
266
  @user_method.reset
302
- initialize_command(*get_args(command, true, quiet: true))
267
+ cmd = @connection.command([tty_flag(true), command].flatten.compact)
268
+ log_command(cmd)
269
+ initialize_command(*cmd)
303
270
 
304
271
  true
305
272
  end
@@ -308,17 +275,15 @@ module Proxy::RemoteExecution::Ssh::Runners
308
275
  @process_manager&.started? && @user_method.sent_all_data?
309
276
  end
310
277
 
278
+ def tty_flag(tty)
279
+ '-tt' if tty
280
+ end
281
+
311
282
  def run_sync(command, stdin: nil, close_stdin: true, tty: false, user_method: nil)
312
- pm = Proxy::Dynflow::ProcessManager.new(get_args(command, tty))
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
- ''
319
- end
320
- pm.on_stdout(&callback)
321
- pm.on_stderr(&callback)
283
+ cmd = @connection.command([tty_flag(tty), command].flatten.compact)
284
+ log_command(cmd)
285
+ pm = Proxy::Dynflow::ProcessManager.new(cmd)
286
+ set_pm_debug_logging(pm, user_method: user_method)
322
287
  pm.start!
323
288
  unless pm.status
324
289
  pm.stdin.io.puts(stdin) if stdin
@@ -428,13 +393,6 @@ module Proxy::RemoteExecution::Ssh::Runners
428
393
  @expecting_disconnect = true
429
394
  end
430
395
  end
431
-
432
- def available_authentication_methods
433
- methods = %w[publickey] # Always use pubkey auth as fallback
434
- methods << 'gssapi-with-mic' if settings[:kerberos_auth]
435
- methods.unshift('password') if @ssh_password
436
- methods
437
- end
438
396
  end
439
397
  # rubocop:enable Metrics/ClassLength
440
398
  end
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
6
6
  end
7
7
  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.7.0
4
+ version: 0.8.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-05-13 00:00:00.000000000 Z
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -170,10 +170,12 @@ files:
170
170
  - lib/smart_proxy_remote_execution_ssh/async_scripts/control.sh
171
171
  - lib/smart_proxy_remote_execution_ssh/async_scripts/retrieve.sh
172
172
  - lib/smart_proxy_remote_execution_ssh/cockpit.rb
173
+ - lib/smart_proxy_remote_execution_ssh/command_logging.rb
173
174
  - lib/smart_proxy_remote_execution_ssh/dispatcher.rb
174
175
  - lib/smart_proxy_remote_execution_ssh/http_config.ru
175
176
  - lib/smart_proxy_remote_execution_ssh/job_storage.rb
176
177
  - lib/smart_proxy_remote_execution_ssh/log_filter.rb
178
+ - lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb
177
179
  - lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb
178
180
  - lib/smart_proxy_remote_execution_ssh/plugin.rb
179
181
  - lib/smart_proxy_remote_execution_ssh/runners.rb
@@ -203,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
205
  - !ruby/object:Gem::Version
204
206
  version: '0'
205
207
  requirements: []
206
- rubygems_version: 3.1.4
208
+ rubygems_version: 3.2.26
207
209
  signing_key:
208
210
  specification_version: 4
209
211
  summary: Ssh remote execution provider for Foreman Smart-Proxy