smart_proxy_remote_execution_ssh 0.9.0 → 0.10.0

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 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