smart_proxy_remote_execution_ssh 0.4.0 → 0.5.2

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