smart_proxy_remote_execution_ssh 0.4.1 → 0.5.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: f90b757baa9c530c89d5c6c960ebf69601b173886c2f74560513592ab78697e4
4
- data.tar.gz: edf9bc88acd2f859f86dd15ddee26deb948f6b963ddcd6aee2c6d839e12198ed
3
+ metadata.gz: e2f91ec259ca105bcc18a5cecf4342019f4023d44cbff5695998b5e57c25e325
4
+ data.tar.gz: 51ccf299324d4adf10c07475dca2749d885c258913c8d9e7593d8b8149e6d18d
5
5
  SHA512:
6
- metadata.gz: 56cb17a0d7ac5eb456533e72e83aaffb07892a725609685f84c95571afa4fb0868dbd4017c4cf5ceadafa35aaf01beca27bdadb424b5bf0c0a5280c704f21e2c
7
- data.tar.gz: 04c1feec777172574e45369f48219a6d2ea80e7e704a695198ef56bb11b5d4c178c68650414a190f4e57751521a065ad5c0c649ad8842f99be03259f79f3c9f3
6
+ metadata.gz: 74313e91e51ff5d58fce0bc880131b12984a750110a2d76c88875a6b824eec40a2ba9d7336dfda34df7c11d465ded62c14d4d33ba10cf4bed1b2e0309aff9a2b
7
+ data.tar.gz: 92f85e971f10d9e45418e75d0325f3e548e65954160be9f86cb154adde6bc3a512b4e3a5429d162291704b49a50a5570fcdeb4b129f735b8f8f737938a0194d0
@@ -0,0 +1,110 @@
1
+ require 'mqtt'
2
+ require 'json'
3
+
4
+ module Proxy::RemoteExecution::Ssh::Actions
5
+ class PullScript < Proxy::Dynflow::Action::Runner
6
+ JobDelivered = Class.new
7
+
8
+ execution_plan_hooks.use :cleanup, :on => :stopped
9
+
10
+ def plan(action_input, mqtt: false)
11
+ super(action_input)
12
+ input[:with_mqtt] = mqtt
13
+ end
14
+
15
+ def run(event = nil)
16
+ if event == JobDelivered
17
+ output[:state] = :delivered
18
+ suspend
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def init_run
25
+ otp_password = if input[:with_mqtt]
26
+ ::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
27
+ end
28
+ input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script])
29
+ output[:state] = :ready_for_pickup
30
+ output[:result] = []
31
+ mqtt_start(otp_password) if input[:with_mqtt]
32
+ suspend
33
+ end
34
+
35
+ def cleanup(_plan = nil)
36
+ job_storage.drop_job(execution_plan_id, run_step_id)
37
+ Proxy::Dynflow::OtpManager.passwords.delete(execution_plan_id)
38
+ end
39
+
40
+ def process_external_event(event)
41
+ output[:state] = :running
42
+ data = event.data
43
+ 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']
46
+ process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
47
+ end
48
+
49
+ def kill_run
50
+ case output[:state]
51
+ when :ready_for_pickup
52
+ # If the job is not running yet on the client, wipe it from storage
53
+ cleanup
54
+ # TODO: Stop the action
55
+ when :notified, :running
56
+ # Client was notified or is already running, dealing with this situation
57
+ # is only supported if mqtt is available
58
+ # Otherwise we have to wait it out
59
+ # TODO
60
+ # if input[:with_mqtt]
61
+ end
62
+ suspend
63
+ end
64
+
65
+ def mqtt_start(otp_password)
66
+ payload = {
67
+ type: 'data',
68
+ message_id: SecureRandom.uuid,
69
+ version: 1,
70
+ sent: DateTime.now.iso8601,
71
+ directive: 'foreman',
72
+ metadata: {
73
+ 'job_uuid': input[:job_uuid],
74
+ 'username': execution_plan_id,
75
+ 'password': otp_password,
76
+ 'return_url': "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/update",
77
+ },
78
+ content: "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}",
79
+ }
80
+ mqtt_notify payload
81
+ output[:state] = :notified
82
+ end
83
+
84
+ def mqtt_notify(payload)
85
+ MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port) do |c|
86
+ c.publish(mqtt_topic, JSON.dump(payload), false, 1)
87
+ end
88
+ end
89
+
90
+ def host_name
91
+ alternative_names = input.fetch(:alternative_names, {})
92
+
93
+ alternative_names[:consumer_uuid] ||
94
+ alternative_names[:fqdn] ||
95
+ input[:hostname]
96
+ end
97
+
98
+ def mqtt_topic
99
+ "yggdrasil/#{host_name}/data/in"
100
+ end
101
+
102
+ def settings
103
+ Proxy::RemoteExecution::Ssh::Plugin.settings
104
+ end
105
+
106
+ def job_storage
107
+ Proxy::RemoteExecution::Ssh.job_storage
108
+ end
109
+ end
110
+ end
@@ -3,7 +3,20 @@ require 'smart_proxy_dynflow/action/runner'
3
3
 
4
4
  module Proxy::RemoteExecution::Ssh
5
5
  module Actions
6
- class RunScript < Proxy::Dynflow::Action::Runner
6
+ class RunScript < ::Dynflow::Action
7
+ def plan(*args)
8
+ mode = Proxy::RemoteExecution::Ssh::Plugin.settings.mode
9
+ case mode
10
+ when :ssh, :'ssh-async'
11
+ plan_action(ScriptRunner, *args)
12
+ when :pull, :'pull-mqtt'
13
+ plan_action(PullScript, *args,
14
+ mqtt: mode == :'pull-mqtt')
15
+ end
16
+ end
17
+ end
18
+
19
+ class ScriptRunner < Proxy::Dynflow::Action::Runner
7
20
  def initiate_runner
8
21
  additional_options = {
9
22
  :step_id => run_step_id,
@@ -0,0 +1,6 @@
1
+ module Proxy::RemoteExecution::Ssh
2
+ module Actions
3
+ require 'smart_proxy_remote_execution_ssh/actions/run_script'
4
+ require 'smart_proxy_remote_execution_ssh/actions/pull_script'
5
+ end
6
+ end
@@ -1,11 +1,13 @@
1
1
  require 'net/ssh'
2
2
  require 'base64'
3
+ require 'smart_proxy_dynflow/runner'
3
4
 
4
5
  module Proxy::RemoteExecution
5
6
  module Ssh
6
7
 
7
8
  class Api < ::Sinatra::Base
8
9
  include Sinatra::Authorization::Helpers
10
+ include Proxy::Dynflow::Helpers
9
11
 
10
12
  get "/pubkey" do
11
13
  File.read(Ssh.public_key_file)
@@ -37,6 +39,53 @@ module Proxy::RemoteExecution
37
39
  end
38
40
  204
39
41
  end
42
+
43
+ # Payload is a hash where
44
+ # exit_code: Integer | NilClass
45
+ # output: String
46
+ post '/jobs/:job_uuid/update' do |job_uuid|
47
+ do_authorize_with_ssl_client
48
+
49
+ with_authorized_job(job_uuid) do |job_record|
50
+ data = MultiJson.load(request.body.read)
51
+ notify_job(job_record, ::Proxy::Dynflow::Runner::ExternalEvent.new(data))
52
+ end
53
+ end
54
+
55
+ get '/jobs' do
56
+ do_authorize_with_ssl_client
57
+
58
+ MultiJson.dump(Proxy::RemoteExecution::Ssh.job_storage.job_uuids_for_host(https_cert_cn))
59
+ end
60
+
61
+ get "/jobs/:job_uuid" do |job_uuid|
62
+ do_authorize_with_ssl_client
63
+
64
+ with_authorized_job(job_uuid) do |job_record|
65
+ notify_job(job_record, Actions::PullScript::JobDelivered)
66
+ job_record[:job]
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def notify_job(job_record, event)
73
+ world.event(job_record[:execution_plan_uuid], job_record[:run_step_id], event)
74
+ end
75
+
76
+ def with_authorized_job(uuid)
77
+ if (job = authorized_job(uuid))
78
+ yield job
79
+ else
80
+ halt 404
81
+ end
82
+ end
83
+
84
+ def authorized_job(uuid)
85
+ job_record = Proxy::RemoteExecution::Ssh.job_storage.find_job(uuid) || {}
86
+ return job_record if authorize_with_token(clear: false, task_id: job_record[:execution_plan_uuid]) ||
87
+ job_record[:hostname] == https_cert_cn
88
+ end
40
89
  end
41
90
  end
42
91
  end
@@ -1,11 +1,11 @@
1
- require 'net/ssh'
1
+ require 'smart_proxy_remote_execution_ssh/net_ssh_compat'
2
2
  require 'forwardable'
3
3
 
4
4
  module Proxy::RemoteExecution
5
5
  module Cockpit
6
6
  # A wrapper class around different kind of sockets to comply with Net::SSH event loop
7
7
  class BufferedSocket
8
- include Net::SSH::BufferedIo
8
+ include Proxy::RemoteExecution::NetSSHCompat::BufferedIO
9
9
  extend Forwardable
10
10
 
11
11
  # The list of methods taken from OpenSSL::SSL::SocketForwarder for the object to act like a socket
@@ -52,14 +52,14 @@ module Proxy::RemoteExecution
52
52
  end
53
53
  def_delegators(:@socket, :read_nonblock, :write_nonblock, :close)
54
54
 
55
- def recv(n)
55
+ def recv(count)
56
56
  res = ""
57
57
  begin
58
58
  # To drain a SSLSocket before we can go back to the event
59
59
  # loop, we need to repeatedly call read_nonblock; a single
60
60
  # call is not enough.
61
61
  loop do
62
- res += @socket.read_nonblock(n)
62
+ res += @socket.read_nonblock(count)
63
63
  end
64
64
  rescue IO::WaitReadable
65
65
  # Sometimes there is no payload after reading everything
@@ -95,8 +95,8 @@ module Proxy::RemoteExecution
95
95
  end
96
96
  def_delegators(:@socket, :read_nonblock, :write_nonblock, :close)
97
97
 
98
- def recv(n)
99
- @socket.read_nonblock(n)
98
+ def recv(count)
99
+ @socket.read_nonblock(count)
100
100
  end
101
101
 
102
102
  def send(mesg, flags)
@@ -113,6 +113,7 @@ module Proxy::RemoteExecution
113
113
 
114
114
  def initialize(env)
115
115
  @env = env
116
+ @open_ios = []
116
117
  end
117
118
 
118
119
  def valid?
@@ -127,6 +128,7 @@ module Proxy::RemoteExecution
127
128
  begin
128
129
  @env['rack.hijack'].call
129
130
  rescue NotImplementedError
131
+ # This is fine
130
132
  end
131
133
  @socket = @env['rack.hijack_io']
132
134
  end
@@ -137,15 +139,11 @@ module Proxy::RemoteExecution
137
139
  private
138
140
 
139
141
  def ssh_on_socket
140
- with_error_handling { start_ssh_loop }
142
+ with_error_handling { system_ssh_loop }
141
143
  end
142
144
 
143
145
  def with_error_handling
144
146
  yield
145
- rescue Net::SSH::AuthenticationFailed => e
146
- send_error(401, e.message)
147
- rescue Errno::EHOSTUNREACH
148
- send_error(400, "No route to #{host}")
149
147
  rescue SystemCallError => e
150
148
  send_error(400, e.message)
151
149
  rescue SocketError => e
@@ -161,50 +159,62 @@ module Proxy::RemoteExecution
161
159
  end
162
160
  end
163
161
 
164
- def start_ssh_loop
165
- err_buf = ""
166
-
167
- Net::SSH.start(host, ssh_user, ssh_options) do |ssh|
168
- channel = ssh.open_channel do |ch|
169
- ch.exec(command) do |ch, success|
170
- raise "could not execute command" unless success
171
-
172
- ssh.listen_to(buf_socket)
173
-
174
- ch.on_process do
175
- if buf_socket.available.positive?
176
- ch.send_data(buf_socket.read_available)
177
- end
178
- if buf_socket.closed?
179
- ch.close
180
- end
181
- end
182
-
183
- ch.on_data do |ch2, data|
184
- send_start
185
- buf_socket.enqueue(data)
186
- end
187
-
188
- ch.on_request('exit-status') do |ch, data|
189
- code = data.read_long
190
- send_start if code.zero?
191
- err_buf += "Process exited with code #{code}.\r\n"
192
- ch.close
193
- end
194
-
195
- ch.on_request('exit-signal') do |ch, data|
196
- err_buf += "Process was terminated with signal #{data.read_string}.\r\n"
197
- ch.close
198
- end
199
-
200
- ch.on_extended_data do |ch2, type, data|
201
- err_buf += data
202
- end
203
- end
162
+ def system_ssh_loop
163
+ in_read, in_write = IO.pipe
164
+ out_read, out_write = IO.pipe
165
+ err_read, err_write = IO.pipe
166
+
167
+ pid = spawn(*script_runner.send(:get_args, command), :in => in_read, :out => out_write, :err => err_write)
168
+ [in_read, out_write, err_write].each(&:close)
169
+
170
+ send_start
171
+ # Not SSL buffer, but the interface kinda matches
172
+ out_buf = MiniSSLBufferedSocket.new(out_read)
173
+ err_buf = MiniSSLBufferedSocket.new(err_read)
174
+ in_buf = MiniSSLBufferedSocket.new(in_write)
175
+
176
+ inner_system_ssh_loop out_buf, err_buf, in_buf, pid
177
+ end
178
+
179
+ def inner_system_ssh_loop2(out_buf, err_buf, in_buf, pid)
180
+ err_buf_raw = ''
181
+ readers = [buf_socket, out_buf, err_buf]
182
+ loop do
183
+ # Prime the sockets for reading
184
+ ready_readers, ready_writers = IO.select(readers, [buf_socket, in_buf], nil, 300)
185
+ (ready_readers || []).each { |reader| reader.close if reader.fill.zero? }
186
+
187
+ proxy_data(out_buf, in_buf)
188
+
189
+ if out_buf.closed?
190
+ code = Process.wait2(pid).last.exitstatus
191
+ send_start if code.zero? # TODO: Why?
192
+ err_buf_raw += "Process exited with code #{code}.\r\n"
193
+ break
204
194
  end
205
195
 
206
- channel.wait
207
- send_error(400, err_buf) unless @started
196
+ if err_buf.available.positive?
197
+ err_buf_raw += err_buf.read_available
198
+ end
199
+
200
+ flush_pending_writes(ready_writers || [])
201
+ end
202
+ rescue # rubocop:disable Style/RescueStandardError
203
+ send_error(400, err_buf_raw) unless @started
204
+ ensure
205
+ [out_buff, err_buf, in_buf].each(&:close)
206
+ end
207
+
208
+ def proxy_data(out_buf, in_buf)
209
+ { out_buf => buf_socket, buf_socket => in_buf }.each do |src, dst|
210
+ dst.enqueue(src.read_available) if src.available.positive?
211
+ dst.close if src.closed?
212
+ end
213
+ end
214
+
215
+ def flush_pending_writes(writers)
216
+ writers.each do |writer|
217
+ writer.respond_to?(:send_pending) ? writer.send_pending : writer.flush
208
218
  end
209
219
  end
210
220
 
@@ -215,6 +225,7 @@ module Proxy::RemoteExecution
215
225
  buf_socket.enqueue("Connection: upgrade\r\n")
216
226
  buf_socket.enqueue("Upgrade: raw\r\n")
217
227
  buf_socket.enqueue("\r\n")
228
+ buf_socket.send_pending
218
229
  end
219
230
  end
220
231
 
@@ -223,6 +234,7 @@ module Proxy::RemoteExecution
223
234
  buf_socket.enqueue("Connection: close\r\n")
224
235
  buf_socket.enqueue("\r\n")
225
236
  buf_socket.enqueue(msg)
237
+ buf_socket.send_pending
226
238
  end
227
239
 
228
240
  def params
@@ -234,34 +246,33 @@ module Proxy::RemoteExecution
234
246
  end
235
247
 
236
248
  def buf_socket
237
- @buffered_socket ||= BufferedSocket.build(@socket)
249
+ @buf_socket ||= BufferedSocket.build(@socket)
238
250
  end
239
251
 
240
252
  def command
241
253
  params["command"]
242
254
  end
243
255
 
244
- def ssh_user
245
- params["ssh_user"]
246
- end
247
-
248
256
  def host
249
257
  params["hostname"]
250
258
  end
251
259
 
252
- def ssh_options
253
- auth_methods = %w[publickey]
254
- auth_methods.unshift('password') if params["ssh_password"]
255
-
256
- ret = {}
257
- ret[:port] = params["ssh_port"] if params["ssh_port"]
258
- ret[:keys] = [key_file] if key_file
259
- ret[:password] = params["ssh_password"] if params["ssh_password"]
260
- ret[:passphrase] = params["ssh_key_passphrase"] if params["ssh_key_passphrase"]
261
- ret[:keys_only] = true
262
- ret[:auth_methods] = auth_methods
263
- ret[:verify_host_key] = true
264
- ret[:number_of_password_prompts] = 1
260
+ def script_runner
261
+ @script_runner ||= Proxy::RemoteExecution::Ssh::Runners::ScriptRunner.build(
262
+ runner_params,
263
+ suspended_action: nil
264
+ )
265
+ end
266
+
267
+ def runner_params
268
+ ret = { secrets: {} }
269
+ ret[:secrets][:ssh_password] = params["ssh_password"] if params["ssh_password"]
270
+ ret[:secrets][:key_passphrase] = params["ssh_key_passphrase"] if params["ssh_key_passphrase"]
271
+ ret[:ssh_port] = params["ssh_port"] if params["ssh_port"]
272
+ ret[:ssh_user] = params["ssh_user"]
273
+ # For compatibility only
274
+ ret[:script] = nil
275
+ ret[:hostname] = host
265
276
  ret
266
277
  end
267
278
  end
@@ -0,0 +1,51 @@
1
+ # lib/job_storage.rb
2
+ require 'sequel'
3
+
4
+ module Proxy::RemoteExecution::Ssh
5
+ class JobStorage
6
+ def initialize
7
+ @db = Sequel.sqlite
8
+ @db.create_table :jobs do
9
+ DateTime :timestamp, null: false, default: Sequel::CURRENT_TIMESTAMP
10
+ String :uuid, fixed: true, size: 36, primary_key: true, null: false
11
+ String :hostname, null: false, index: true
12
+ String :execution_plan_uuid, fixed: true, size: 36, null: false, index: true
13
+ Integer :run_step_id, null: false
14
+ String :job, text: true
15
+ end
16
+ end
17
+
18
+ def find_job(uuid)
19
+ jobs.where(uuid: uuid).first
20
+ end
21
+
22
+ def job_uuids_for_host(hostname)
23
+ jobs_for_host(hostname).order(:timestamp)
24
+ .select_map(:uuid)
25
+ end
26
+
27
+ def store_job(hostname, execution_plan_uuid, run_step_id, job, uuid: SecureRandom.uuid, timestamp: Time.now.utc)
28
+ jobs.insert(timestamp: timestamp,
29
+ uuid: uuid,
30
+ hostname: hostname,
31
+ execution_plan_uuid: execution_plan_uuid,
32
+ run_step_id: run_step_id,
33
+ job: job)
34
+ uuid
35
+ end
36
+
37
+ def drop_job(execution_plan_uuid, run_step_id)
38
+ jobs.where(execution_plan_uuid: execution_plan_uuid, run_step_id: run_step_id).delete
39
+ end
40
+
41
+ private
42
+
43
+ def jobs_for_host(hostname)
44
+ jobs.where(hostname: hostname)
45
+ end
46
+
47
+ def jobs
48
+ @db[:jobs]
49
+ end
50
+ end
51
+ end
@@ -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
17
  :ssh_log_level => :fatal,
18
- :cleanup_working_dirs => true
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,10 +26,12 @@ 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'
33
+ require 'smart_proxy_remote_execution_ssh/utils'
34
+ require 'smart_proxy_remote_execution_ssh/job_storage'
30
35
 
31
36
  Proxy::RemoteExecution::Ssh.validate!
32
37
 
@@ -40,7 +45,7 @@ module Proxy::RemoteExecution::Ssh
40
45
  def self.runner_class
41
46
  @runner_class ||= if simulate?
42
47
  Runners::FakeScriptRunner
43
- elsif settings[:async_ssh]
48
+ elsif settings.mode == :'ssh-async'
44
49
  Runners::PollingScriptRunner
45
50
  else
46
51
  Runners::ScriptRunner
@@ -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
@@ -97,11 +90,11 @@ module Proxy::RemoteExecution::Ssh::Runners
97
90
 
98
91
  # rubocop:disable Metrics/ClassLength
99
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,13 @@ 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
+ *, err = session(args, in_stream: false, out_stream: false)
205
+ read_output_debug(err)
235
206
  end
236
207
 
237
208
  def close
@@ -239,12 +210,13 @@ module Proxy::RemoteExecution::Ssh::Runners
239
210
  rescue StandardError => e
240
211
  publish_exception('Error when removing remote working dir', e, false)
241
212
  ensure
242
- @session.close if @session && !@session.closed?
213
+ close_session if @session
243
214
  FileUtils.rm_rf(local_command_dir) if Dir.exist?(local_command_dir) && @cleanup_working_dirs
244
215
  end
245
216
 
246
217
  def publish_data(data, type)
247
- super(data.force_encoding('UTF-8'), type)
218
+ super(data.force_encoding('UTF-8'), type) unless @user_method.filter_password?(data)
219
+ @user_method.on_data(data, @command_in)
248
220
  end
249
221
 
250
222
  private
@@ -254,38 +226,54 @@ module Proxy::RemoteExecution::Ssh::Runners
254
226
  end
255
227
 
256
228
  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(Proxy::Dynflow::Log.instance)
282
- return ssh_options
229
+ @session && @cleanup_working_dirs
230
+ end
231
+
232
+ # Creates session with three pipes - one for reading and two for
233
+ # writing. Similar to `Open3.popen3` method but without creating
234
+ # a separate thread to monitor it.
235
+ def session(args, in_stream: true, out_stream: true, err_stream: true)
236
+ @session = true
237
+
238
+ in_read, in_write = in_stream ? IO.pipe : '/dev/null'
239
+ out_read, out_write = out_stream ? IO.pipe : [nil, '/dev/null']
240
+ err_read, err_write = err_stream ? IO.pipe : [nil, '/dev/null']
241
+ command_pid = spawn(*args, :in => in_read, :out => out_write, :err => err_write)
242
+ in_read.close if in_stream
243
+ out_write.close if out_stream
244
+ err_write.close if err_stream
245
+
246
+ return command_pid, in_write, out_read, err_read
247
+ end
248
+
249
+ def ssh_options(with_pty = false)
250
+ ssh_options = []
251
+ ssh_options << "-tt" if with_pty
252
+ ssh_options << "-o User=#{@ssh_user}"
253
+ ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
254
+ ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
255
+ ssh_options << "-o IdentitiesOnly=yes"
256
+ ssh_options << "-o StrictHostKeyChecking=no"
257
+ ssh_options << "-o PreferredAuthentications=#{available_authentication_methods.join(',')}"
258
+ ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
259
+ ssh_options << "-o NumberOfPasswordPrompts=1"
260
+ ssh_options << "-o LogLevel=#{settings[:ssh_log_level]}"
261
+ ssh_options << "-o ControlMaster=auto"
262
+ ssh_options << "-o ControlPath=#{local_command_file("socket")}"
263
+ ssh_options << "-o ControlPersist=yes"
283
264
  end
284
265
 
285
266
  def settings
286
267
  Proxy::RemoteExecution::Ssh::Plugin.settings
287
268
  end
288
269
 
270
+ def get_args(command, with_pty = false)
271
+ args = []
272
+ args = [{'SSHPASS' => @key_passphrase}, '/usr/bin/sshpass', '-P', 'passphrase', '-e'] if @key_passphrase
273
+ args = [{'SSHPASS' => @ssh_password}, '/usr/bin/sshpass', '-e'] if @ssh_password
274
+ args += ['/usr/bin/ssh', @host, ssh_options(with_pty), command].flatten
275
+ end
276
+
289
277
  # Initiates run of the remote command and yields the data when
290
278
  # available. The yielding doesn't happen automatically, but as
291
279
  # part of calling the `refresh` method.
@@ -294,30 +282,9 @@ module Proxy::RemoteExecution::Ssh::Runners
294
282
 
295
283
  @started = false
296
284
  @user_method.reset
285
+ @command_pid, @command_in, @command_out = session(get_args(command, with_pty: true), err_stream: false)
286
+ @started = true
297
287
 
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
288
  return true
322
289
  end
323
290
 
@@ -325,36 +292,27 @@ module Proxy::RemoteExecution::Ssh::Runners
325
292
  @started && @user_method.sent_all_data?
326
293
  end
327
294
 
328
- def run_sync(command, stdin = nil)
295
+ def read_output_debug(err_io, out_io = nil)
329
296
  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
297
+ debug_str = ''
298
+
299
+ if out_io
300
+ stdout += out_io.read until out_io.eof? rescue
301
+ out_io.close
353
302
  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
303
+ debug_str += err_io.read until err_io.eof? rescue
304
+ err_io.close
305
+ debug_str.lines.each { |line| @logger.debug(line.strip) }
306
+
307
+ return stdout, debug_str
308
+ end
309
+
310
+ def run_sync(command, stdin = nil)
311
+ pid, tx, rx, err = session(get_args(command))
312
+ tx.puts(stdin) unless stdin.nil?
313
+ tx.close
314
+ stdout, stderr = read_output_debug(err, rx)
315
+ exit_status = Process.wait2(pid)[1].exitstatus
358
316
  return exit_status, stdout, stderr
359
317
  end
360
318
 
@@ -371,7 +329,7 @@ module Proxy::RemoteExecution::Ssh::Runners
371
329
  end
372
330
 
373
331
  def local_command_file(filename)
374
- File.join(local_command_dir, filename)
332
+ File.join(ensure_local_directory(local_command_dir), filename)
375
333
  end
376
334
 
377
335
  def remote_command_dir
@@ -453,15 +411,8 @@ module Proxy::RemoteExecution::Ssh::Runners
453
411
 
454
412
  def available_authentication_methods
455
413
  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
414
+ methods << 'gssapi-with-mic' if settings[:kerberos_auth]
463
415
  methods.unshift('password') if @ssh_password
464
-
465
416
  methods
466
417
  end
467
418
  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
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
6
6
  end
7
7
  end
@@ -20,7 +20,9 @@ module Proxy::RemoteExecution
20
20
  raise "Ssh public key file #{public_key_file} doesn't exist"
21
21
  end
22
22
 
23
+ validate_mode!
23
24
  validate_ssh_log_level!
25
+ validate_mqtt_settings!
24
26
  end
25
27
 
26
28
  def private_key_file
@@ -31,6 +33,35 @@ module Proxy::RemoteExecution
31
33
  File.expand_path("#{private_key_file}.pub")
32
34
  end
33
35
 
36
+ def validate_mode!
37
+ Plugin.settings.mode = Plugin.settings.mode.to_sym
38
+
39
+ unless Plugin::MODES.include? Plugin.settings.mode
40
+ raise "Mode has to be one of #{Plugin::MODES.join(', ')}, given #{Plugin.settings.mode}"
41
+ end
42
+
43
+ if Plugin.settings.async_ssh
44
+ Plugin.logger.warn('Option async_ssh is deprecated, use ssh-async mode instead.')
45
+
46
+ case Plugin.settings.mode
47
+ when :ssh
48
+ Plugin.logger.warn('Deprecated option async_ssh used together with ssh mode, switching mode to ssh-async.')
49
+ Plugin.settings.mode = :'ssh-async'
50
+ when :'async-ssh'
51
+ # This is a noop
52
+ else
53
+ Plugin.logger.warn('Deprecated option async_ssh used together with incompatible mode, ignoring.')
54
+ end
55
+ end
56
+ end
57
+
58
+ def validate_mqtt_settings!
59
+ return unless Plugin.settings.mode == :'pull-mqtt'
60
+
61
+ raise 'mqtt_broker has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_broker.nil?
62
+ raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
63
+ end
64
+
34
65
  def validate_ssh_log_level!
35
66
  wanted_level = Plugin.settings.ssh_log_level.to_s
36
67
  levels = Plugin::SSH_LOG_LEVELS
@@ -51,6 +82,10 @@ module Proxy::RemoteExecution
51
82
 
52
83
  Plugin.settings.ssh_log_level = Plugin.settings.ssh_log_level.to_sym
53
84
  end
85
+
86
+ def job_storage
87
+ @job_storage ||= Proxy::RemoteExecution::Ssh::JobStorage.new
88
+ end
54
89
  end
55
90
  end
56
91
  end
@@ -4,17 +4,23 @@
4
4
  :local_working_dir: '/var/tmp'
5
5
  :remote_working_dir: '/var/tmp'
6
6
  # :kerberos_auth: false
7
- # :async_ssh: false
7
+
8
+ # Mode of operation, one of ssh, ssh-async, pull, pull-mqtt
9
+ :mode: ssh
8
10
 
9
11
  # Defines how often (in seconds) should the runner check
10
12
  # for new data leave empty to use the runner's default
11
13
  # (1 second for regular, 60 seconds with async_ssh enabled)
12
14
  # :runner_refresh_interval:
13
15
 
14
- # Defines the verbosity of logging coming from Net::SSH
15
- # one of :debug, :info, :warn, :error, :fatal
16
+ # Defines the verbosity of logging coming from ssh command
17
+ # one of :debug, :info, :error, :fatal
16
18
  # must be lower than general log level
17
19
  # :ssh_log_level: fatal
18
20
 
19
21
  # Remove working directories on job completion
20
22
  # :cleanup_working_dirs: true
23
+
24
+ # MQTT configuration, need to be set if mode is set to pull-mqtt
25
+ # :mqtt_broker: localhost
26
+ # :mqtt_port: 1883
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.4.1
4
+ version: 0.5.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: 2021-07-09 00:00:00.000000000 Z
11
+ date: 2021-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -124,6 +124,20 @@ dependencies:
124
124
  version: '0.5'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: net-ssh
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 4.2.0
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 4.2.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: mqtt
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - ">="
@@ -149,6 +163,8 @@ files:
149
163
  - README.md
150
164
  - bundler.d/remote_execution_ssh.rb
151
165
  - lib/smart_proxy_remote_execution_ssh.rb
166
+ - lib/smart_proxy_remote_execution_ssh/actions.rb
167
+ - lib/smart_proxy_remote_execution_ssh/actions/pull_script.rb
152
168
  - lib/smart_proxy_remote_execution_ssh/actions/run_script.rb
153
169
  - lib/smart_proxy_remote_execution_ssh/api.rb
154
170
  - lib/smart_proxy_remote_execution_ssh/async_scripts/control.sh
@@ -156,12 +172,15 @@ files:
156
172
  - lib/smart_proxy_remote_execution_ssh/cockpit.rb
157
173
  - lib/smart_proxy_remote_execution_ssh/dispatcher.rb
158
174
  - lib/smart_proxy_remote_execution_ssh/http_config.ru
175
+ - lib/smart_proxy_remote_execution_ssh/job_storage.rb
159
176
  - lib/smart_proxy_remote_execution_ssh/log_filter.rb
177
+ - lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb
160
178
  - lib/smart_proxy_remote_execution_ssh/plugin.rb
161
179
  - lib/smart_proxy_remote_execution_ssh/runners.rb
162
180
  - lib/smart_proxy_remote_execution_ssh/runners/fake_script_runner.rb
163
181
  - lib/smart_proxy_remote_execution_ssh/runners/polling_script_runner.rb
164
182
  - lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb
183
+ - lib/smart_proxy_remote_execution_ssh/utils.rb
165
184
  - lib/smart_proxy_remote_execution_ssh/version.rb
166
185
  - lib/smart_proxy_remote_execution_ssh/webrick_ext.rb
167
186
  - settings.d/remote_execution_ssh.yml.example