smart_proxy_remote_execution_ssh 0.9.0 → 0.10.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: f28e2eb48888626dad82eb56e7a5309da15d584e6d4c53eaf648e5ddfc6f3d1e
4
- data.tar.gz: a5b604d80264dc78b461e1792100ecd6fc9a4a71791b8df5b064bd80a32f6920
3
+ metadata.gz: '048f8694430967060fae6bd7790fe56930b5c2bac37f3bc26e7b13db69081da6'
4
+ data.tar.gz: 5a3b1e344a47aa4c97dcce97f43dc9e424d398f79c67a6f1e066d670758c99f0
5
5
  SHA512:
6
- metadata.gz: 3231a3d465b180d1b8b325fd9fc341c009b566452946ee56837f9f37e6502d7095d1a213bc5b4b13078b9d54e5bd592f5430aea502ecdee93edab15901440815
7
- data.tar.gz: 62e77d3cf6d37c90b2e6e8e48ad8b2080186a50fd1d3633b38743a099e0897b9b1e4d4dafaf7b02b1855dcd51036d418333c156438c3b20e5fa42feac93ded17
6
+ metadata.gz: a4bc90b3dda6d190c8f94aeb6731a3a6679788ea4f950e1bb4196c1d5da4630b170fe152e9c96a5e4cb7dcd25ba1d6371292e52b4059773759c1fbe8461bd347
7
+ data.tar.gz: f4849b32f7529cbb29d127ae1c4c9fd9642b0700c834b1e3eebf59b5d9cfca282fca674188a734a37b36d45ff3e423096aa3512ceae118aef7ff831cb1600a38
@@ -6,7 +6,6 @@ module Proxy::RemoteExecution::Ssh::Actions
6
6
  class PullScript < Proxy::Dynflow::Action::Runner
7
7
  JobDelivered = Class.new
8
8
  PickupTimeout = Class.new
9
- ResendNotification = Class.new
10
9
 
11
10
  execution_plan_hooks.use :cleanup, :on => :stopped
12
11
 
@@ -21,12 +20,6 @@ module Proxy::RemoteExecution::Ssh::Actions
21
20
  suspend
22
21
  elsif event == PickupTimeout
23
22
  process_pickup_timeout
24
- elsif event == ResendNotification
25
- if input[:with_mqtt] && %w(ready_for_pickup notified).include?(output[:state])
26
- schedule_mqtt_resend
27
- mqtt_start(::Proxy::Dynflow::OtpManager.passwords[execution_plan_id])
28
- end
29
- suspend
30
23
  else
31
24
  super
32
25
  end
@@ -45,16 +38,15 @@ module Proxy::RemoteExecution::Ssh::Actions
45
38
  input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''), effective_user: input[:effective_user])
46
39
  output[:state] = :ready_for_pickup
47
40
  output[:result] = []
48
- if input[:with_mqtt]
49
- schedule_mqtt_resend
50
- mqtt_start(otp_password)
51
- end
41
+
42
+ mqtt_start(otp_password) if input[:with_mqtt]
52
43
  suspend
53
44
  end
54
45
 
55
46
  def cleanup(_plan = nil)
56
47
  job_storage.drop_job(execution_plan_id, run_step_id)
57
48
  Proxy::Dynflow::OtpManager.passwords.delete(execution_plan_id)
49
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.done(input[:job_uuid])
58
50
  end
59
51
 
60
52
  def process_external_event(event)
@@ -125,7 +117,7 @@ module Proxy::RemoteExecution::Ssh::Actions
125
117
  'effective_user': input[:effective_user]
126
118
  },
127
119
  )
128
- mqtt_notify payload
120
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.new(input[:job_uuid], mqtt_topic, payload)
129
121
  output[:state] = :notified
130
122
  end
131
123
 
@@ -141,18 +133,7 @@ module Proxy::RemoteExecution::Ssh::Actions
141
133
  end
142
134
 
143
135
  def mqtt_notify(payload)
144
- with_mqtt_client do |c|
145
- c.publish(mqtt_topic, JSON.dump(payload), false, 1)
146
- end
147
- end
148
-
149
- def with_mqtt_client(&block)
150
- MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port,
151
- :ssl => settings.mqtt_tls,
152
- :cert_file => ::Proxy::SETTINGS.foreman_ssl_cert || ::Proxy::SETTINGS.ssl_certificate,
153
- :key_file => ::Proxy::SETTINGS.foreman_ssl_key || ::Proxy::SETTINGS.ssl_private_key,
154
- :ca_file => ::Proxy::SETTINGS.foreman_ssl_ca || ::Proxy::SETTINGS.ssl_ca_file,
155
- &block)
136
+ Proxy::RemoteExecution::Ssh::MQTT.publish(mqtt_topic, JSON.dump(payload))
156
137
  end
157
138
 
158
139
  def host_name
@@ -167,10 +148,6 @@ module Proxy::RemoteExecution::Ssh::Actions
167
148
  "yggdrasil/#{host_name}/data/in"
168
149
  end
169
150
 
170
- def settings
171
- Proxy::RemoteExecution::Ssh::Plugin.settings
172
- end
173
-
174
151
  def job_storage
175
152
  Proxy::RemoteExecution::Ssh.job_storage
176
153
  end
@@ -192,9 +169,5 @@ module Proxy::RemoteExecution::Ssh::Actions
192
169
  suspend
193
170
  end
194
171
  end
195
-
196
- def schedule_mqtt_resend
197
- plan_event(ResendNotification, settings[:mqtt_resend_interval], optional: true)
198
- end
199
172
  end
200
173
  end
@@ -64,6 +64,7 @@ module Proxy::RemoteExecution
64
64
  do_authorize_with_ssl_client
65
65
 
66
66
  with_authorized_job(job_uuid) do |job_record|
67
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.running(job_record[:uuid])
67
68
  notify_job(job_record, Actions::PullScript::JobDelivered)
68
69
  response.headers['X-Foreman-Effective-User'] = job_record[:effective_user]
69
70
  job_record[:job]
@@ -0,0 +1,169 @@
1
+ require 'concurrent'
2
+ require 'mqtt'
3
+
4
+ class Proxy::RemoteExecution::Ssh::MQTT
5
+ class Dispatcher
6
+ include Singleton
7
+
8
+ attr_reader :reference
9
+ def initialize
10
+ limit = Proxy::RemoteExecution::Ssh::Plugin.settings[:mqtt_rate_limit]
11
+ @reference = DispatcherActor.spawn('MQTT dispatcher',
12
+ Proxy::Dynflow::Core.world.clock,
13
+ limit)
14
+ end
15
+
16
+ def new(uuid, topic, payload)
17
+ reference.tell([:new, uuid, topic, payload])
18
+ end
19
+
20
+ def running(uuid)
21
+ reference.tell([:running, uuid])
22
+ end
23
+
24
+ def resend(uuid)
25
+ reference.tell([:resend, uuid])
26
+ end
27
+
28
+ def done(uuid)
29
+ reference.tell([:done, uuid])
30
+ end
31
+ end
32
+
33
+ class DispatcherActor < Concurrent::Actor::RestartingContext
34
+ JobDefinition = Struct.new :uuid, :topic, :payload
35
+
36
+ class Tracker
37
+ def initialize(limit, clock)
38
+ @clock = clock
39
+ @limit = limit
40
+ @jobs = {}
41
+ @pending = []
42
+ @running = Set.new
43
+ @hot = Set.new
44
+ @cold = Set.new
45
+ end
46
+
47
+ def new(uuid, topic, payload)
48
+ @jobs[uuid] = JobDefinition.new(uuid, topic, payload)
49
+ @pending << uuid
50
+ dispatch_pending
51
+ end
52
+
53
+ def running(uuid)
54
+ [@pending, @hot, @cold].each { |source| source.delete(uuid) }
55
+ @running << uuid
56
+ end
57
+
58
+ def resend(uuid)
59
+ return unless @jobs[uuid]
60
+
61
+ @pending << uuid
62
+ dispatch_pending
63
+ end
64
+
65
+ def done(uuid)
66
+ @jobs.delete(uuid)
67
+ [@pending, @running, @hot, @cold].each do |source|
68
+ source.delete(uuid)
69
+ end
70
+ dispatch_pending
71
+ end
72
+
73
+ def needs_processing?
74
+ pending_count.positive? || @hot.any? || @cold.any?
75
+ end
76
+
77
+ def pending_count
78
+ pending = @pending.count
79
+ return pending if @limit.nil?
80
+
81
+ running = [@running, @hot, @cold].map(&:count).sum
82
+ capacity = @limit - running
83
+ pending > capacity ? capacity : pending
84
+ end
85
+
86
+ def dispatch_pending
87
+ pending_count.times do
88
+ mqtt_notify(@pending.first)
89
+ @hot << @pending.shift
90
+ end
91
+ end
92
+
93
+ def process
94
+ @cold.each { |uuid| schedule_resend(uuid) }
95
+ @cold = @hot
96
+ @hot = Set.new
97
+
98
+ dispatch_pending
99
+ end
100
+
101
+ def mqtt_notify(uuid)
102
+ job = @jobs[uuid]
103
+ return if job.nil?
104
+
105
+ Proxy::RemoteExecution::Ssh::MQTT.publish(job.topic, JSON.dump(job.payload))
106
+ end
107
+
108
+ def settings
109
+ Proxy::RemoteExecution::Ssh::Plugin.settings
110
+ end
111
+
112
+ def schedule_resend(uuid)
113
+ @clock.ping(Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance, resend_interval, uuid, :resend)
114
+ end
115
+
116
+ def resend_interval
117
+ settings[:mqtt_resend_interval]
118
+ end
119
+ end
120
+
121
+ def initialize(clock, limit = nil)
122
+ @tracker = Tracker.new(limit, clock)
123
+
124
+ interval = Proxy::RemoteExecution::Ssh::Plugin.settings[:mqtt_ttl]
125
+ @timer = Concurrent::TimerTask.new(execution_interval: interval) do
126
+ reference.tell(:tick)
127
+ end
128
+ end
129
+
130
+ def on_message(message)
131
+ action, arg = message
132
+ # Enable the timer just in case anything in tracker raises an exception so
133
+ # we can continue
134
+ timer_on
135
+ case action
136
+ when :new
137
+ _, uuid, topic, payload = message
138
+ @tracker.new(uuid, topic, payload)
139
+ when :resend
140
+ @tracker.resend(arg)
141
+ when :running
142
+ @tracker.running(arg)
143
+ when :done
144
+ @tracker.done(arg)
145
+ when :tick
146
+ @tracker.process
147
+ end
148
+ timer_set(@tracker.needs_processing?)
149
+ end
150
+
151
+ def timer_set(on)
152
+ on ? timer_on : timer_off
153
+ end
154
+
155
+ def timer_on
156
+ @timer.execute
157
+ end
158
+
159
+ def timer_off
160
+ @timer.shutdown
161
+ end
162
+
163
+ # In case an exception is raised during processing, instruct concurrent-ruby
164
+ # to keep going without losing state
165
+ def behaviour_definition
166
+ Concurrent::Actor::Behaviour.restarting_behaviour_definition(:resume!)
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,23 @@
1
+ module Proxy::RemoteExecution::Ssh
2
+ class MQTT
3
+ require 'smart_proxy_remote_execution_ssh/mqtt/dispatcher'
4
+
5
+ class << self
6
+ def publish(topic, payload, retain: false, qos: 1)
7
+ with_mqtt_client do |c|
8
+ c.publish(topic, payload, retain, qos)
9
+ end
10
+ end
11
+
12
+ def with_mqtt_client(&block)
13
+ ::MQTT::Client.connect(Plugin.settings.mqtt_broker,
14
+ Plugin.settings.mqtt_port,
15
+ :ssl => Plugin.settings.mqtt_tls,
16
+ :cert_file => ::Proxy::SETTINGS.foreman_ssl_cert || ::Proxy::SETTINGS.ssl_certificate,
17
+ :key_file => ::Proxy::SETTINGS.foreman_ssl_key || ::Proxy::SETTINGS.ssl_private_key,
18
+ :ca_file => ::Proxy::SETTINGS.foreman_ssl_ca || ::Proxy::SETTINGS.ssl_ca_file,
19
+ &block)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -25,8 +25,10 @@ module Proxy::RemoteExecution::Ssh
25
25
  # :mqtt_broker => nil,
26
26
  # :mqtt_port => nil,
27
27
  # :mqtt_tls => nil,
28
+ # :mqtt_rate_limit => nil
28
29
  :mode => :ssh,
29
- :mqtt_resend_interval => 900
30
+ :mqtt_resend_interval => 900,
31
+ :mqtt_ttl => 5
30
32
 
31
33
  capability(proc { 'cockpit' if settings.cockpit_integration })
32
34
 
@@ -46,6 +48,11 @@ module Proxy::RemoteExecution::Ssh
46
48
  Proxy::RemoteExecution::Ssh.validate!
47
49
 
48
50
  Proxy::Dynflow::TaskLauncherRegistry.register('ssh', Proxy::Dynflow::TaskLauncher::Batch)
51
+ if settings.mode == :'pull-mqtt'
52
+ require 'smart_proxy_remote_execution_ssh/mqtt'
53
+ # Force initialization
54
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance
55
+ end
49
56
  end
50
57
 
51
58
  def self.simulate?
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_remote_execution_ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-14 00:00:00.000000000 Z
11
+ date: 2022-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -175,6 +175,8 @@ files:
175
175
  - lib/smart_proxy_remote_execution_ssh/http_config.ru
176
176
  - lib/smart_proxy_remote_execution_ssh/job_storage.rb
177
177
  - lib/smart_proxy_remote_execution_ssh/log_filter.rb
178
+ - lib/smart_proxy_remote_execution_ssh/mqtt.rb
179
+ - lib/smart_proxy_remote_execution_ssh/mqtt/dispatcher.rb
178
180
  - lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb
179
181
  - lib/smart_proxy_remote_execution_ssh/net_ssh_compat.rb
180
182
  - lib/smart_proxy_remote_execution_ssh/plugin.rb