smart_proxy_remote_execution_ssh 0.4.0 → 0.5.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.
@@ -0,0 +1,228 @@
1
+ module Proxy::RemoteExecution
2
+ module NetSSHCompat
3
+ class Buffer
4
+ # exposes the raw content of the buffer
5
+ attr_reader :content
6
+
7
+ # the current position of the pointer in the buffer
8
+ attr_accessor :position
9
+
10
+ # Creates a new buffer, initialized to the given content. The position
11
+ # is initialized to the beginning of the buffer.
12
+ def initialize(content = +'')
13
+ @content = content.to_s
14
+ @position = 0
15
+ end
16
+
17
+ # Returns the length of the buffer's content.
18
+ def length
19
+ @content.length
20
+ end
21
+
22
+ # Returns the number of bytes available to be read (e.g., how many bytes
23
+ # remain between the current position and the end of the buffer).
24
+ def available
25
+ length - position
26
+ end
27
+
28
+ # Returns a copy of the buffer's content.
29
+ def to_s
30
+ (@content || "").dup
31
+ end
32
+
33
+ # Returns +true+ if the buffer contains no data (e.g., it is of zero length).
34
+ def empty?
35
+ @content.empty?
36
+ end
37
+
38
+ # Resets the pointer to the start of the buffer. Subsequent reads will
39
+ # begin at position 0.
40
+ def reset!
41
+ @position = 0
42
+ end
43
+
44
+ # Returns true if the pointer is at the end of the buffer. Subsequent
45
+ # reads will return nil, in this case.
46
+ def eof?
47
+ @position >= length
48
+ end
49
+
50
+ # Resets the buffer, making it empty. Also, resets the read position to
51
+ # 0.
52
+ def clear!
53
+ @content = +''
54
+ @position = 0
55
+ end
56
+
57
+ # Consumes n bytes from the buffer, where n is the current position
58
+ # unless otherwise specified. This is useful for removing data from the
59
+ # buffer that has previously been read, when you are expecting more data
60
+ # to be appended. It helps to keep the size of buffers down when they
61
+ # would otherwise tend to grow without bound.
62
+ #
63
+ # Returns the buffer object itself.
64
+ def consume!(count = position)
65
+ if count >= length
66
+ # OPTIMIZE: a fairly common case
67
+ clear!
68
+ elsif count.positive?
69
+ @content = @content[count..-1] || +''
70
+ @position -= count
71
+ @position = 0 if @position.negative?
72
+ end
73
+ self
74
+ end
75
+
76
+ # Appends the given text to the end of the buffer. Does not alter the
77
+ # read position. Returns the buffer object itself.
78
+ def append(text)
79
+ @content << text
80
+ self
81
+ end
82
+
83
+ # Reads and returns the next +count+ bytes from the buffer, starting from
84
+ # the read position. If +count+ is +nil+, this will return all remaining
85
+ # text in the buffer. This method will increment the pointer.
86
+ def read(count = nil)
87
+ count ||= length
88
+ count = length - @position if @position + count > length
89
+ @position += count
90
+ @content[@position - count, count]
91
+ end
92
+
93
+ # Writes the given data literally into the string. Does not alter the
94
+ # read position. Returns the buffer object.
95
+ def write(*data)
96
+ data.each { |datum| @content << datum.dup.force_encoding('BINARY') }
97
+ self
98
+ end
99
+ end
100
+
101
+ module BufferedIO
102
+ # This module is used to extend sockets and other IO objects, to allow
103
+ # them to be buffered for both read and write. This abstraction makes it
104
+ # quite easy to write a select-based event loop
105
+ # (see Net::SSH::Connection::Session#listen_to).
106
+ #
107
+ # The general idea is that instead of calling #read directly on an IO that
108
+ # has been extended with this module, you call #fill (to add pending input
109
+ # to the internal read buffer), and then #read_available (to read from that
110
+ # buffer). Likewise, you don't call #write directly, you call #enqueue to
111
+ # add data to the write buffer, and then #send_pending or #wait_for_pending_sends
112
+ # to actually send the data across the wire.
113
+ #
114
+ # In this way you can easily use the object as an argument to IO.select,
115
+ # calling #fill when it is available for read, or #send_pending when it is
116
+ # available for write, and then call #enqueue and #read_available during
117
+ # the idle times.
118
+ #
119
+ # socket = TCPSocket.new(address, port)
120
+ # socket.extend(Net::SSH::BufferedIo)
121
+ #
122
+ # ssh.listen_to(socket)
123
+ #
124
+ # ssh.loop do
125
+ # if socket.available > 0
126
+ # puts socket.read_available
127
+ # socket.enqueue("response\n")
128
+ # end
129
+ # end
130
+ #
131
+ # Note that this module must be used to extend an instance, and should not
132
+ # be included in a class. If you do want to use it via an include, then you
133
+ # must make sure to invoke the private #initialize_buffered_io method in
134
+ # your class' #initialize method:
135
+ #
136
+ # class Foo < IO
137
+ # include Net::SSH::BufferedIo
138
+ #
139
+ # def initialize
140
+ # initialize_buffered_io
141
+ # # ...
142
+ # end
143
+ # end
144
+
145
+ # Tries to read up to +n+ bytes of data from the remote end, and appends
146
+ # the data to the input buffer. It returns the number of bytes read, or 0
147
+ # if no data was available to be read.
148
+ def fill(count = 8192)
149
+ input.consume!
150
+ data = recv(count)
151
+ input.append(data)
152
+ return data.length
153
+ rescue EOFError => e
154
+ @input_errors << e
155
+ return 0
156
+ end
157
+
158
+ # Read up to +length+ bytes from the input buffer. If +length+ is nil,
159
+ # all available data is read from the buffer. (See #available.)
160
+ def read_available(length = nil)
161
+ input.read(length || available)
162
+ end
163
+
164
+ # Returns the number of bytes available to be read from the input buffer.
165
+ # (See #read_available.)
166
+ def available
167
+ input.available
168
+ end
169
+
170
+ # Enqueues data in the output buffer, to be written when #send_pending
171
+ # is called. Note that the data is _not_ sent immediately by this method!
172
+ def enqueue(data)
173
+ output.append(data)
174
+ end
175
+
176
+ # Sends as much of the pending output as possible. Returns +true+ if any
177
+ # data was sent, and +false+ otherwise.
178
+ def send_pending
179
+ if output.length.positive?
180
+ sent = send(output.to_s, 0)
181
+ output.consume!(sent)
182
+ return sent.positive?
183
+ else
184
+ return false
185
+ end
186
+ end
187
+
188
+ # Calls #send_pending repeatedly, if necessary, blocking until the output
189
+ # buffer is empty.
190
+ def wait_for_pending_sends
191
+ send_pending
192
+ while output.length.positive?
193
+ result = IO.select(nil, [self]) || next
194
+ next unless result[1].any?
195
+
196
+ send_pending
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ #--
203
+ # Can't use attr_reader here (after +private+) without incurring the
204
+ # wrath of "ruby -w". We hates it.
205
+ #++
206
+
207
+ def input
208
+ @input
209
+ end
210
+
211
+ def output
212
+ @output
213
+ end
214
+
215
+ # Initializes the intput and output buffers for this object. This method
216
+ # is called automatically when the module is mixed into an object via
217
+ # Object#extend (see Net::SSH::BufferedIo.extended), but must be called
218
+ # explicitly in the +initialize+ method of any class that uses
219
+ # Module#include to add this module.
220
+ def initialize_buffered_io
221
+ @input = Buffer.new
222
+ @input_errors = []
223
+ @output = Buffer.new
224
+ @output_errors = []
225
+ end
226
+ end
227
+ end
228
+ end
@@ -1,6 +1,7 @@
1
1
  module Proxy::RemoteExecution::Ssh
2
2
  class Plugin < Proxy::Plugin
3
- SSH_LOG_LEVELS = %w[debug info warn error fatal].freeze
3
+ SSH_LOG_LEVELS = %w[debug info error fatal].freeze
4
+ MODES = %i[ssh async-ssh pull pull-mqtt].freeze
4
5
 
5
6
  http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
6
7
  https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
@@ -11,11 +12,13 @@ module Proxy::RemoteExecution::Ssh
11
12
  :remote_working_dir => '/var/tmp',
12
13
  :local_working_dir => '/var/tmp',
13
14
  :kerberos_auth => false,
14
- :async_ssh => false,
15
15
  # When set to nil, makes REX use the runner's default interval
16
16
  # :runner_refresh_interval => nil,
17
- :ssh_log_level => :fatal,
18
- :cleanup_working_dirs => true
17
+ :ssh_log_level => :error,
18
+ :cleanup_working_dirs => true,
19
+ # :mqtt_broker => nil,
20
+ # :mqtt_port => nil,
21
+ :mode => :ssh
19
22
 
20
23
  plugin :ssh, Proxy::RemoteExecution::Ssh::VERSION
21
24
  after_activation do
@@ -23,13 +26,16 @@ module Proxy::RemoteExecution::Ssh
23
26
  require 'smart_proxy_remote_execution_ssh/version'
24
27
  require 'smart_proxy_remote_execution_ssh/cockpit'
25
28
  require 'smart_proxy_remote_execution_ssh/api'
26
- require 'smart_proxy_remote_execution_ssh/actions/run_script'
29
+ require 'smart_proxy_remote_execution_ssh/actions'
27
30
  require 'smart_proxy_remote_execution_ssh/dispatcher'
28
31
  require 'smart_proxy_remote_execution_ssh/log_filter'
29
32
  require 'smart_proxy_remote_execution_ssh/runners'
30
- require 'smart_proxy_dynflow_core'
33
+ require 'smart_proxy_remote_execution_ssh/utils'
34
+ require 'smart_proxy_remote_execution_ssh/job_storage'
31
35
 
32
36
  Proxy::RemoteExecution::Ssh.validate!
37
+
38
+ Proxy::Dynflow::TaskLauncherRegistry.register('ssh', Proxy::Dynflow::TaskLauncher::Batch)
33
39
  end
34
40
 
35
41
  def self.simulate?
@@ -39,7 +45,7 @@ module Proxy::RemoteExecution::Ssh
39
45
  def self.runner_class
40
46
  @runner_class ||= if simulate?
41
47
  Runners::FakeScriptRunner
42
- elsif settings[:async_ssh]
48
+ elsif settings.mode == :'ssh-async'
43
49
  Runners::PollingScriptRunner
44
50
  else
45
51
  Runners::ScriptRunner
@@ -1,5 +1,5 @@
1
1
  module Proxy::RemoteExecution::Ssh::Runners
2
- class FakeScriptRunner < ForemanTasksCore::Runner::Base
2
+ class FakeScriptRunner < ::Proxy::Dynflow::Runner::Base
3
3
  DEFAULT_REFRESH_INTERVAL = 1
4
4
 
5
5
  @data = []
@@ -24,7 +24,7 @@ module Proxy::RemoteExecution::Ssh::Runners
24
24
  @callback_host = options[:callback_host]
25
25
  @task_id = options[:uuid]
26
26
  @step_id = options[:step_id]
27
- @otp = ForemanTasksCore::OtpManager.generate_otp(@task_id)
27
+ @otp = Proxy::Dynflow::OtpManager.generate_otp(@task_id)
28
28
  end
29
29
 
30
30
  def prepare_start
@@ -40,7 +40,7 @@ module Proxy::RemoteExecution::Ssh::Runners
40
40
  control_script_finish = "#{@control_script_path} init-script-finish"
41
41
  <<-SCRIPT.gsub(/^ +\| /, '')
42
42
  | export CONTROL_SCRIPT="#{@control_script_path}"
43
- | sh -c '#{main_script}; #{control_script_finish}' #{close_fds} &
43
+ | #{@user_method.cli_command_prefix} sh -c '#{main_script}; #{control_script_finish}' #{close_fds} &
44
44
  | echo $! > '#{@base_dir}/pid'
45
45
  SCRIPT
46
46
  end
@@ -90,7 +90,7 @@ module Proxy::RemoteExecution::Ssh::Runners
90
90
 
91
91
  def close
92
92
  super
93
- ForemanTasksCore::OtpManager.drop_otp(@task_id, @otp) if @otp
93
+ Proxy::Dynflow::OtpManager.drop_otp(@task_id, @otp) if @otp
94
94
  end
95
95
 
96
96
  def upload_control_scripts
@@ -116,7 +116,7 @@ module Proxy::RemoteExecution::Ssh::Runners
116
116
 
117
117
  # Generates updates based on the callback data from the manual mode
118
118
  def load_event_updates(event_data)
119
- continuous_output = ForemanTasksCore::ContinuousOutput.new
119
+ continuous_output = Proxy::Dynflow::ContinuousOutput.new
120
120
  if event_data.key?('output')
121
121
  lines = Base64.decode64(event_data['output']).sub(/\A(RUNNING|DONE).*\n/, '')
122
122
  continuous_output.add_output(lines, 'stdout')
@@ -132,8 +132,7 @@ module Proxy::RemoteExecution::Ssh::Runners
132
132
  def destroy_session
133
133
  if @session
134
134
  @logger.debug("Closing session with #{@ssh_user}@#{@host}")
135
- @session.close
136
- @session = nil
135
+ close_session
137
136
  end
138
137
  end
139
138
  end
@@ -1,12 +1,5 @@
1
- require 'net/ssh'
2
1
  require 'fileutils'
3
-
4
- # Rubocop can't make up its mind what it wants
5
- # rubocop:disable Lint/SuppressedException, Lint/RedundantCopDisableDirective
6
- begin
7
- require 'net/ssh/krb'
8
- rescue LoadError; end
9
- # rubocop:enable Lint/SuppressedException, Lint/RedundantCopDisableDirective
2
+ require 'smart_proxy_dynflow/runner/command'
10
3
 
11
4
  module Proxy::RemoteExecution::Ssh::Runners
12
5
  class EffectiveUserMethod
@@ -21,7 +14,7 @@ module Proxy::RemoteExecution::Ssh::Runners
21
14
 
22
15
  def on_data(received_data, ssh_channel)
23
16
  if received_data.match(login_prompt)
24
- ssh_channel.send_data(effective_user_password + "\n")
17
+ ssh_channel.puts(effective_user_password)
25
18
  @password_sent = true
26
19
  end
27
20
  end
@@ -96,12 +89,12 @@ module Proxy::RemoteExecution::Ssh::Runners
96
89
  end
97
90
 
98
91
  # rubocop:disable Metrics/ClassLength
99
- class ScriptRunner < ForemanTasksCore::Runner::Base
92
+ class ScriptRunner < Proxy::Dynflow::Runner::Base
93
+ include Proxy::Dynflow::Runner::Command
100
94
  attr_reader :execution_timeout_interval
101
95
 
102
96
  EXPECTED_POWER_ACTION_MESSAGES = ['restart host', 'shutdown host'].freeze
103
97
  DEFAULT_REFRESH_INTERVAL = 1
104
- MAX_PROCESS_RETRIES = 4
105
98
 
106
99
  def initialize(options, user_method, suspended_action: nil)
107
100
  super suspended_action: suspended_action
@@ -119,6 +112,7 @@ module Proxy::RemoteExecution::Ssh::Runners
119
112
  @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
120
113
  @remote_working_dir = options.fetch(:remote_working_dir, settings.remote_working_dir)
121
114
  @cleanup_working_dirs = options.fetch(:cleanup_working_dirs, settings.cleanup_working_dirs)
115
+ @first_execution = options.fetch(:first_execution, false)
122
116
  @user_method = user_method
123
117
  end
124
118
 
@@ -146,12 +140,13 @@ module Proxy::RemoteExecution::Ssh::Runners
146
140
  end
147
141
 
148
142
  def start
143
+ Proxy::RemoteExecution::Utils.prune_known_hosts!(@host, @ssh_port, logger) if @first_execution
149
144
  prepare_start
150
145
  script = initialization_script
151
146
  logger.debug("executing script:\n#{indent_multiline(script)}")
152
147
  trigger(script)
153
- rescue StandardError => e
154
- logger.error("error while initalizing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
148
+ rescue StandardError, NotImplementedError => e
149
+ logger.error("error while initializing command #{e.class} #{e.message}:\n #{e.backtrace.join("\n")}")
155
150
  publish_exception('Error initializing command', e)
156
151
  end
157
152
 
@@ -177,12 +172,7 @@ module Proxy::RemoteExecution::Ssh::Runners
177
172
 
178
173
  def refresh
179
174
  return if @session.nil?
180
-
181
- with_retries do
182
- with_disconnect_handling do
183
- @session.process(0)
184
- end
185
- end
175
+ super
186
176
  ensure
187
177
  check_expecting_disconnect
188
178
  end
@@ -206,32 +196,15 @@ module Proxy::RemoteExecution::Ssh::Runners
206
196
  execution_timeout_interval
207
197
  end
208
198
 
209
- def with_retries
210
- tries = 0
211
- begin
212
- yield
213
- rescue StandardError => e
214
- logger.error("Unexpected error: #{e.class} #{e.message}\n #{e.backtrace.join("\n")}")
215
- tries += 1
216
- if tries <= MAX_PROCESS_RETRIES
217
- logger.error('Retrying')
218
- retry
219
- else
220
- publish_exception('Unexpected error', e)
221
- end
222
- end
223
- end
224
-
225
- def with_disconnect_handling
226
- yield
227
- rescue IOError, Net::SSH::Disconnect => e
228
- @session.shutdown!
229
- check_expecting_disconnect
230
- if @expecting_disconnect
231
- publish_exit_status(0)
232
- else
233
- publish_exception('Unexpected disconnect', e)
234
- end
199
+ def close_session
200
+ @session = nil
201
+ raise 'Control socket file does not exist' unless File.exist?(local_command_file("socket"))
202
+ @logger.debug("Sending exit request for session #{@ssh_user}@#{@host}")
203
+ args = ['/usr/bin/ssh', @host, "-o", "User=#{@ssh_user}", "-o", "ControlPath=#{local_command_file("socket")}", "-O", "exit"].flatten
204
+ pid, *, err = session(args, in_stream: false, out_stream: false)
205
+ result = read_output_debug(err)
206
+ Process.wait(pid)
207
+ result
235
208
  end
236
209
 
237
210
  def close
@@ -239,12 +212,13 @@ module Proxy::RemoteExecution::Ssh::Runners
239
212
  rescue StandardError => e
240
213
  publish_exception('Error when removing remote working dir', e, false)
241
214
  ensure
242
- @session.close if @session && !@session.closed?
215
+ close_session if @session
243
216
  FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs
244
217
  end
245
218
 
246
219
  def publish_data(data, type)
247
- super(data.force_encoding('UTF-8'), type)
220
+ super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data)
221
+ @user_method.on_data(data, @command_in)
248
222
  end
249
223
 
250
224
  private
@@ -254,38 +228,54 @@ module Proxy::RemoteExecution::Ssh::Runners
254
228
  end
255
229
 
256
230
  def should_cleanup?
257
- @session && !@session.closed? && @cleanup_working_dirs
258
- end
259
-
260
- def session
261
- @session ||= begin
262
- @logger.debug("opening session to #{@ssh_user}@#{@host}")
263
- Net::SSH.start(@host, @ssh_user, ssh_options)
264
- end
265
- end
266
-
267
- def ssh_options
268
- ssh_options = {}
269
- ssh_options[:port] = @ssh_port if @ssh_port
270
- ssh_options[:keys] = [@client_private_key_file] if @client_private_key_file
271
- ssh_options[:password] = @ssh_password if @ssh_password
272
- ssh_options[:passphrase] = @key_passphrase if @key_passphrase
273
- ssh_options[:keys_only] = true
274
- # if the host public key is contained in the known_hosts_file,
275
- # verify it, otherwise, if missing, import it and continue
276
- ssh_options[:paranoid] = true
277
- ssh_options[:auth_methods] = available_authentication_methods
278
- ssh_options[:user_known_hosts_file] = prepare_known_hosts if @host_public_key
279
- ssh_options[:number_of_password_prompts] = 1
280
- ssh_options[:verbose] = settings[:ssh_log_level]
281
- ssh_options[:logger] = Proxy::RemoteExecution::Ssh::LogFilter.new(SmartProxyDynflowCore::Log.instance)
282
- return ssh_options
231
+ @session && @cleanup_working_dirs
232
+ end
233
+
234
+ # Creates session with three pipes - one for reading and two for
235
+ # writing. Similar to `Open3.popen3` method but without creating
236
+ # a separate thread to monitor it.
237
+ def session(args, in_stream: true, out_stream: true, err_stream: true)
238
+ @session = true
239
+
240
+ in_read, in_write = in_stream ? IO.pipe : '/dev/null'
241
+ out_read, out_write = out_stream ? IO.pipe : [nil, '/dev/null']
242
+ err_read, err_write = err_stream ? IO.pipe : [nil, '/dev/null']
243
+ command_pid = spawn(*args, :in => in_read, :out => out_write, :err => err_write)
244
+ in_read.close if in_stream
245
+ out_write.close if out_stream
246
+ err_write.close if err_stream
247
+
248
+ return command_pid, in_write, out_read, err_read
249
+ end
250
+
251
+ def ssh_options(with_pty = false)
252
+ ssh_options = []
253
+ ssh_options << "-tt" if with_pty
254
+ ssh_options << "-o User=#{@ssh_user}"
255
+ ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
256
+ ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
257
+ ssh_options << "-o IdentitiesOnly=yes"
258
+ ssh_options << "-o StrictHostKeyChecking=no"
259
+ ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
260
+ ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
261
+ ssh_options << "-o NumberOfPasswordPrompts=1"
262
+ ssh_options << "-o LogLevel=#{settings[:ssh_log_level]}"
263
+ ssh_options << "-o ControlMaster=auto"
264
+ ssh_options << "-o ControlPath=#{local_command_file("socket")}"
265
+ ssh_options << "-o ControlPersist=yes"
283
266
  end
284
267
 
285
268
  def settings
286
269
  Proxy::RemoteExecution::Ssh::Plugin.settings
287
270
  end
288
271
 
272
+ def get_args(command, with_pty = false)
273
+ args = []
274
+ args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
275
+ args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
276
+ args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
277
+ end
278
+
289
279
  # Initiates run of the remote command and yields the data when
290
280
  # available. The yielding doesn't happen automatically, but as
291
281
  # part of calling the `refresh` method.
@@ -294,30 +284,9 @@ module Proxy::RemoteExecution::Ssh::Runners
294
284
 
295
285
  @started = false
296
286
  @user_method.reset
287
+ @command_pid, @command_in, @command_out = session(get_args(command, with_pty: true), err_stream: false)
288
+ @started = true
297
289
 
298
- session.open_channel do |channel|
299
- channel.request_pty
300
- channel.on_data do |ch, data|
301
- publish_data(data, 'stdout') unless @user_method.filter_password?(data)
302
- @user_method.on_data(data, ch)
303
- end
304
- channel.on_extended_data { |ch, type, data| publish_data(data, 'stderr') }
305
- # standard exit of the command
306
- channel.on_request('exit-status') { |ch, data| publish_exit_status(data.read_long) }
307
- # on signal: sending the signal value (such as 'TERM')
308
- channel.on_request('exit-signal') do |ch, data|
309
- publish_exit_status(data.read_string)
310
- ch.close
311
- # wait for the channel to finish so that we know at the end
312
- # that the session is inactive
313
- ch.wait
314
- end
315
- channel.exec(command) do |_, success|
316
- @started = true
317
- raise('Error initializing command') unless success
318
- end
319
- end
320
- session.process(0) { !run_started? }
321
290
  return true
322
291
  end
323
292
 
@@ -325,36 +294,27 @@ module Proxy::RemoteExecution::Ssh::Runners
325
294
  @started && @user_method.sent_all_data?
326
295
  end
327
296
 
328
- def run_sync(command, stdin = nil)
297
+ def read_output_debug(err_io, out_io = nil)
329
298
  stdout = ''
330
- stderr = ''
331
- exit_status = nil
332
- started = false
333
-
334
- channel = session.open_channel do |ch|
335
- ch.on_data do |c, data|
336
- stdout.concat(data)
337
- end
338
- ch.on_extended_data { |_, _, data| stderr.concat(data) }
339
- ch.on_request('exit-status') { |_, data| exit_status = data.read_long }
340
- # Send data to stdin if we have some
341
- ch.send_data(stdin) unless stdin.nil?
342
- # on signal: sending the signal value (such as 'TERM')
343
- ch.on_request('exit-signal') do |_, data|
344
- exit_status = data.read_string
345
- ch.close
346
- ch.wait
347
- end
348
- ch.exec command do |_, success|
349
- raise 'could not execute command' unless success
350
-
351
- started = true
352
- end
299
+ debug_str = ''
300
+
301
+ if out_io
302
+ stdout += out_io.read until out_io.eof? rescue
303
+ out_io.close
353
304
  end
354
- session.process(0) { !started }
355
- # Closing the channel without sending any data gives us SIGPIPE
356
- channel.close unless stdin.nil?
357
- channel.wait
305
+ debug_str += err_io.read until err_io.eof? rescue
306
+ err_io.close
307
+ debug_str.lines.each { |line| @logger.debug(line.strip) }
308
+
309
+ return stdout, debug_str
310
+ end
311
+
312
+ def run_sync(command, stdin = nil)
313
+ pid, tx, rx, err = session(get_args(command))
314
+ tx.puts(stdin) unless stdin.nil?
315
+ tx.close
316
+ stdout, stderr = read_output_debug(err, rx)
317
+ exit_status = Process.wait2(pid)[1].exitstatus
358
318
  return exit_status, stdout, stderr
359
319
  end
360
320
 
@@ -371,7 +331,7 @@ module Proxy::RemoteExecution::Ssh::Runners
371
331
  end
372
332
 
373
333
  def local_command_file(filename)
374
- File.join(local_command_dir, filename)
334
+ File.join(ensure_local_directory(local_command_dir), filename)
375
335
  end
376
336
 
377
337
  def remote_command_dir
@@ -453,15 +413,8 @@ module Proxy::RemoteExecution::Ssh::Runners
453
413
 
454
414
  def available_authentication_methods
455
415
  methods = %w[publickey] # Always use pubkey auth as fallback
456
- if settings[:kerberos_auth]
457
- if defined? Net::SSH::Kerberos
458
- methods << 'gssapi-with-mic'
459
- else
460
- @logger.warn('Kerberos authentication requested but not available')
461
- end
462
- end
416
+ methods << 'gssapi-with-mic' if settings[:kerberos_auth]
463
417
  methods.unshift('password') if @ssh_password
464
-
465
418
  methods
466
419
  end
467
420
  end
@@ -0,0 +1,24 @@
1
+ require 'open3'
2
+
3
+ module Proxy::RemoteExecution
4
+ module Utils
5
+ class << self
6
+ def prune_known_hosts!(hostname, port, logger = Logger.new($stdout))
7
+ return if Net::SSH::KnownHosts.search_for(hostname).empty?
8
+
9
+ target = if port == 22
10
+ hostname
11
+ else
12
+ "[#{hostname}]:#{port}"
13
+ end
14
+
15
+ Open3.popen3('ssh-keygen', '-R', target) do |_stdin, stdout, _stderr, wait_thr|
16
+ wait_thr.join
17
+ stdout.read
18
+ end
19
+ rescue Errno::ENOENT => e
20
+ logger.warn("Could not remove #{hostname} from know_hosts: #{e}")
21
+ end
22
+ end
23
+ end
24
+ end