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.
- checksums.yaml +4 -4
- data/{bundler.plugins.d → bundler.d}/remote_execution_ssh.rb +0 -0
- data/lib/smart_proxy_remote_execution_ssh/actions/pull_script.rb +110 -0
- data/lib/smart_proxy_remote_execution_ssh/actions/run_script.rb +16 -2
- data/lib/smart_proxy_remote_execution_ssh/actions.rb +6 -0
- data/lib/smart_proxy_remote_execution_ssh/api.rb +49 -0
- data/lib/smart_proxy_remote_execution_ssh/cockpit.rb +87 -71
- data/lib/smart_proxy_remote_execution_ssh/dispatcher.rb +2 -2
- data/lib/smart_proxy_remote_execution_ssh/job_storage.rb +51 -0
- data/lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb +228 -0
- data/lib/smart_proxy_remote_execution_ssh/plugin.rb +13 -7
- data/lib/smart_proxy_remote_execution_ssh/runners/fake_script_runner.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh/runners/polling_script_runner.rb +5 -6
- data/lib/smart_proxy_remote_execution_ssh/runners/script_runner.rb +86 -133
- data/lib/smart_proxy_remote_execution_ssh/utils.rb +24 -0
- data/lib/smart_proxy_remote_execution_ssh/version.rb +1 -1
- data/lib/smart_proxy_remote_execution_ssh.rb +37 -6
- data/settings.d/remote_execution_ssh.yml.example +10 -4
- metadata +19 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ba2217281e0dfb1199d345fd81e2e135c3b3f83e590177ecc8826dfb8176262
|
4
|
+
data.tar.gz: db341f9a3cbed3045938d16cc0b772887098ee1db4adc2d4b515449785afbf39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4564886d08716efa9f7d40b887027d67dcd2bb9b02688033cd6f9f7f5a633a2e215ff5e567e144db687f7bb51f624b19aa240159c0f2a15ccaccbe983161c28a
|
7
|
+
data.tar.gz: fb3e8fd7bdba9193731c1399f2809e5625c6313b9cf1a1d1a6daad1e2bfc012e7057ebc7a3766e9d4bf9e1938b898dabfeea042f87db2d31b20dd07964a6744e
|
File without changes
|
@@ -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
|
@@ -1,8 +1,22 @@
|
|
1
|
-
require '
|
1
|
+
require 'smart_proxy_dynflow/action/shareable'
|
2
|
+
require 'smart_proxy_dynflow/action/runner'
|
2
3
|
|
3
4
|
module Proxy::RemoteExecution::Ssh
|
4
5
|
module Actions
|
5
|
-
class RunScript <
|
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
|
6
20
|
def initiate_runner
|
7
21
|
additional_options = {
|
8
22
|
:step_id => run_step_id,
|
@@ -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 '
|
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
|
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(
|
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(
|
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(
|
99
|
-
@socket.read_nonblock(
|
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 {
|
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,67 @@ module Proxy::RemoteExecution
|
|
161
159
|
end
|
162
160
|
end
|
163
161
|
|
164
|
-
def
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
+
# Force the script runner to initialize its logger
|
168
|
+
script_runner.logger
|
169
|
+
pid = spawn(*script_runner.send(:get_args, command), :in => in_read, :out => out_write, :err => err_write)
|
170
|
+
[in_read, out_write, err_write].each(&:close)
|
171
|
+
|
172
|
+
send_start
|
173
|
+
# Not SSL buffer, but the interface kinda matches
|
174
|
+
out_buf = MiniSSLBufferedSocket.new(out_read)
|
175
|
+
err_buf = MiniSSLBufferedSocket.new(err_read)
|
176
|
+
in_buf = MiniSSLBufferedSocket.new(in_write)
|
177
|
+
|
178
|
+
inner_system_ssh_loop out_buf, err_buf, in_buf, pid
|
179
|
+
end
|
180
|
+
|
181
|
+
def inner_system_ssh_loop(out_buf, err_buf, in_buf, pid)
|
182
|
+
err_buf_raw = ''
|
183
|
+
readers = [buf_socket, out_buf, err_buf]
|
184
|
+
loop do
|
185
|
+
# Prime the sockets for reading
|
186
|
+
ready_readers, ready_writers = IO.select(readers, [buf_socket, in_buf], nil, 300)
|
187
|
+
(ready_readers || []).each { |reader| reader.close if reader.fill.zero? }
|
188
|
+
|
189
|
+
proxy_data(out_buf, in_buf)
|
190
|
+
if buf_socket.closed?
|
191
|
+
script_runner.close_session
|
204
192
|
end
|
205
193
|
|
206
|
-
|
207
|
-
|
194
|
+
if out_buf.closed?
|
195
|
+
code = Process.wait2(pid).last.exitstatus
|
196
|
+
send_start if code.zero? # TODO: Why?
|
197
|
+
err_buf_raw += "Process exited with code #{code}.\r\n"
|
198
|
+
break
|
199
|
+
end
|
200
|
+
|
201
|
+
if err_buf.available.positive?
|
202
|
+
err_buf_raw += err_buf.read_available
|
203
|
+
end
|
204
|
+
|
205
|
+
flush_pending_writes(ready_writers || [])
|
206
|
+
end
|
207
|
+
rescue # rubocop:disable Style/RescueStandardError
|
208
|
+
send_error(400, err_buf_raw) unless @started
|
209
|
+
ensure
|
210
|
+
[out_buf, err_buf, in_buf].each(&:close)
|
211
|
+
end
|
212
|
+
|
213
|
+
def proxy_data(out_buf, in_buf)
|
214
|
+
{ out_buf => buf_socket, buf_socket => in_buf }.each do |src, dst|
|
215
|
+
dst.enqueue(src.read_available) if src.available.positive?
|
216
|
+
dst.close if src.closed?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def flush_pending_writes(writers)
|
221
|
+
writers.each do |writer|
|
222
|
+
writer.respond_to?(:send_pending) ? writer.send_pending : writer.flush
|
208
223
|
end
|
209
224
|
end
|
210
225
|
|
@@ -215,6 +230,7 @@ module Proxy::RemoteExecution
|
|
215
230
|
buf_socket.enqueue("Connection: upgrade\r\n")
|
216
231
|
buf_socket.enqueue("Upgrade: raw\r\n")
|
217
232
|
buf_socket.enqueue("\r\n")
|
233
|
+
buf_socket.send_pending
|
218
234
|
end
|
219
235
|
end
|
220
236
|
|
@@ -223,6 +239,7 @@ module Proxy::RemoteExecution
|
|
223
239
|
buf_socket.enqueue("Connection: close\r\n")
|
224
240
|
buf_socket.enqueue("\r\n")
|
225
241
|
buf_socket.enqueue(msg)
|
242
|
+
buf_socket.send_pending
|
226
243
|
end
|
227
244
|
|
228
245
|
def params
|
@@ -234,34 +251,33 @@ module Proxy::RemoteExecution
|
|
234
251
|
end
|
235
252
|
|
236
253
|
def buf_socket
|
237
|
-
@
|
254
|
+
@buf_socket ||= BufferedSocket.build(@socket)
|
238
255
|
end
|
239
256
|
|
240
257
|
def command
|
241
258
|
params["command"]
|
242
259
|
end
|
243
260
|
|
244
|
-
def ssh_user
|
245
|
-
params["ssh_user"]
|
246
|
-
end
|
247
|
-
|
248
261
|
def host
|
249
262
|
params["hostname"]
|
250
263
|
end
|
251
264
|
|
252
|
-
def
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
ret
|
261
|
-
ret[:
|
262
|
-
ret[:
|
263
|
-
ret[:
|
264
|
-
ret[:
|
265
|
+
def script_runner
|
266
|
+
@script_runner ||= Proxy::RemoteExecution::Ssh::Runners::ScriptRunner.build(
|
267
|
+
runner_params,
|
268
|
+
suspended_action: nil
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
def runner_params
|
273
|
+
ret = { secrets: {} }
|
274
|
+
ret[:secrets][:ssh_password] = params["ssh_password"] if params["ssh_password"]
|
275
|
+
ret[:secrets][:key_passphrase] = params["ssh_key_passphrase"] if params["ssh_key_passphrase"]
|
276
|
+
ret[:ssh_port] = params["ssh_port"] if params["ssh_port"]
|
277
|
+
ret[:ssh_user] = params["ssh_user"]
|
278
|
+
# For compatibility only
|
279
|
+
ret[:script] = nil
|
280
|
+
ret[:hostname] = host
|
265
281
|
ret
|
266
282
|
end
|
267
283
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'smart_proxy_dynflow/runner/dispatcher'
|
2
2
|
|
3
3
|
module Proxy::RemoteExecution::Ssh
|
4
|
-
class Dispatcher < ::
|
4
|
+
class Dispatcher < ::Proxy::Dynflow::Runner::Dispatcher
|
5
5
|
def refresh_interval
|
6
6
|
@refresh_interval ||= Plugin.settings[:runner_refresh_interval] ||
|
7
7
|
Plugin.runner_class::DEFAULT_REFRESH_INTERVAL
|
@@ -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
|