smart_proxy_remote_execution_ssh 0.8.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: 1a599d3732d4f6b064a850ae8baced28354980096785a340828533f953f844c4
4
- data.tar.gz: 160b80ab983f0eb7b987987191b48c19db42d3b76079b0ba6639ec831dccc6f3
3
+ metadata.gz: '048f8694430967060fae6bd7790fe56930b5c2bac37f3bc26e7b13db69081da6'
4
+ data.tar.gz: 5a3b1e344a47aa4c97dcce97f43dc9e424d398f79c67a6f1e066d670758c99f0
5
5
  SHA512:
6
- metadata.gz: 349961aa474809225d9781bda09687eb23f00db51cca9f758a57e075fa3d19fc07b729569b064887f91bb7f07190152d6ec9cb01e8ccf48df0841e9c3f85d10e
7
- data.tar.gz: 8ad40859c92f7d1cd089adf0f37bb0aaa8add745cd461c2e1ed5b484d3ae6b856ac0ee53e61c62e89d564a4ba676b22bf5f01618da970986cd60e42591ff6346
6
+ metadata.gz: a4bc90b3dda6d190c8f94aeb6731a3a6679788ea4f950e1bb4196c1d5da4630b170fe152e9c96a5e4cb7dcd25ba1d6371292e52b4059773759c1fbe8461bd347
7
+ data.tar.gz: f4849b32f7529cbb29d127ae1c4c9fd9642b0700c834b1e3eebf59b5d9cfca282fca674188a734a37b36d45ff3e423096aa3512ceae118aef7ff831cb1600a38
@@ -5,6 +5,7 @@ require 'time'
5
5
  module Proxy::RemoteExecution::Ssh::Actions
6
6
  class PullScript < Proxy::Dynflow::Action::Runner
7
7
  JobDelivered = Class.new
8
+ PickupTimeout = Class.new
8
9
 
9
10
  execution_plan_hooks.use :cleanup, :on => :stopped
10
11
 
@@ -17,9 +18,14 @@ module Proxy::RemoteExecution::Ssh::Actions
17
18
  if event == JobDelivered
18
19
  output[:state] = :delivered
19
20
  suspend
21
+ elsif event == PickupTimeout
22
+ process_pickup_timeout
20
23
  else
21
24
  super
22
25
  end
26
+ rescue => e
27
+ action_logger.error(e)
28
+ process_update(Proxy::Dynflow::Runner::Update.encode_exception('Proxy error', e))
23
29
  end
24
30
 
25
31
  def init_run
@@ -27,9 +33,12 @@ module Proxy::RemoteExecution::Ssh::Actions
27
33
  ::Proxy::Dynflow::OtpManager.generate_otp(execution_plan_id)
28
34
  end
29
35
 
30
- input[:job_uuid] = job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''))
36
+ plan_event(PickupTimeout, input[:time_to_pickup], optional: true) if input[:time_to_pickup]
37
+
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])
31
39
  output[:state] = :ready_for_pickup
32
40
  output[:result] = []
41
+
33
42
  mqtt_start(otp_password) if input[:with_mqtt]
34
43
  suspend
35
44
  end
@@ -37,6 +46,7 @@ module Proxy::RemoteExecution::Ssh::Actions
37
46
  def cleanup(_plan = nil)
38
47
  job_storage.drop_job(execution_plan_id, run_step_id)
39
48
  Proxy::Dynflow::OtpManager.passwords.delete(execution_plan_id)
49
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.done(input[:job_uuid])
40
50
  end
41
51
 
42
52
  def process_external_event(event)
@@ -103,9 +113,11 @@ module Proxy::RemoteExecution::Ssh::Actions
103
113
  'username': execution_plan_id,
104
114
  'password': otp_password,
105
115
  'return_url': "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/update",
116
+ 'version': 'v1',
117
+ 'effective_user': input[:effective_user]
106
118
  },
107
119
  )
108
- mqtt_notify payload
120
+ Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.new(input[:job_uuid], mqtt_topic, payload)
109
121
  output[:state] = :notified
110
122
  end
111
123
 
@@ -121,18 +133,7 @@ module Proxy::RemoteExecution::Ssh::Actions
121
133
  end
122
134
 
123
135
  def mqtt_notify(payload)
124
- with_mqtt_client do |c|
125
- c.publish(mqtt_topic, JSON.dump(payload), false, 1)
126
- end
127
- end
128
-
129
- def with_mqtt_client(&block)
130
- MQTT::Client.connect(settings.mqtt_broker, settings.mqtt_port,
131
- :ssl => settings.mqtt_tls,
132
- :cert_file => ::Proxy::SETTINGS.foreman_ssl_cert || ::Proxy::SETTINGS.ssl_certificate,
133
- :key_file => ::Proxy::SETTINGS.foreman_ssl_key || ::Proxy::SETTINGS.ssl_private_key,
134
- :ca_file => ::Proxy::SETTINGS.foreman_ssl_ca || ::Proxy::SETTINGS.ssl_ca_file,
135
- &block)
136
+ Proxy::RemoteExecution::Ssh::MQTT.publish(mqtt_topic, JSON.dump(payload))
136
137
  end
137
138
 
138
139
  def host_name
@@ -147,10 +148,6 @@ module Proxy::RemoteExecution::Ssh::Actions
147
148
  "yggdrasil/#{host_name}/data/in"
148
149
  end
149
150
 
150
- def settings
151
- Proxy::RemoteExecution::Ssh::Plugin.settings
152
- end
153
-
154
151
  def job_storage
155
152
  Proxy::RemoteExecution::Ssh.job_storage
156
153
  end
@@ -164,5 +161,13 @@ module Proxy::RemoteExecution::Ssh::Actions
164
161
  directive: 'foreman'
165
162
  }
166
163
  end
164
+
165
+ def process_pickup_timeout
166
+ if output[:state] != :delivered
167
+ raise "The job was not picked up in time"
168
+ else
169
+ suspend
170
+ end
171
+ end
167
172
  end
168
173
  end
@@ -64,7 +64,9 @@ 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)
69
+ response.headers['X-Foreman-Effective-User'] = job_record[:effective_user]
68
70
  job_record[:job]
69
71
  end
70
72
  end
@@ -14,7 +14,7 @@ module Proxy::RemoteExecution::Ssh::Runners
14
14
  logger.debug(line.chomp) if user_method.nil? || !user_method.filter_password?(line)
15
15
  user_method.on_data(data, pm.stdin) if user_method
16
16
  end
17
- ''
17
+ data
18
18
  end
19
19
  pm.on_stdout(&callback)
20
20
  pm.on_stderr(&callback)
@@ -11,6 +11,7 @@ module Proxy::RemoteExecution::Ssh
11
11
  String :hostname, null: false, index: true
12
12
  String :execution_plan_uuid, fixed: true, size: 36, null: false, index: true
13
13
  Integer :run_step_id, null: false
14
+ String :effective_user
14
15
  String :job, text: true
15
16
  end
16
17
  end
@@ -24,13 +25,14 @@ module Proxy::RemoteExecution::Ssh
24
25
  .select_map(:uuid)
25
26
  end
26
27
 
27
- def store_job(hostname, execution_plan_uuid, run_step_id, job, uuid: SecureRandom.uuid, timestamp: Time.now.utc)
28
+ def store_job(hostname, execution_plan_uuid, run_step_id, job, uuid: SecureRandom.uuid, timestamp: Time.now.utc, effective_user: nil)
28
29
  jobs.insert(timestamp: timestamp,
29
30
  uuid: uuid,
30
31
  hostname: hostname,
31
32
  execution_plan_uuid: execution_plan_uuid,
32
33
  run_step_id: run_step_id,
33
- job: job)
34
+ job: job,
35
+ effective_user: effective_user)
34
36
  uuid
35
37
  end
36
38
 
@@ -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
@@ -32,7 +32,7 @@ module Proxy::RemoteExecution::Ssh::Runners
32
32
  return [] unless @password
33
33
 
34
34
  prompt = ['-P', @prompt] if @prompt
35
- [{'SSHPASS' => SensitiveString.new(@password)}, '/usr/bin/sshpass', '-e', prompt].compact
35
+ [{'SSHPASS' => SensitiveString.new(@password)}, 'sshpass', '-e', prompt].compact
36
36
  end
37
37
 
38
38
  def ssh_options
@@ -93,7 +93,7 @@ module Proxy::RemoteExecution::Ssh::Runners
93
93
  def command(cmd)
94
94
  raise "Cannot build command to run over multiplexed connection without having an established connection" unless connected?
95
95
 
96
- ['/usr/bin/ssh', reuse_ssh_options, cmd].flatten
96
+ ['ssh', reuse_ssh_options, cmd].flatten
97
97
  end
98
98
 
99
99
  private
@@ -103,7 +103,7 @@ module Proxy::RemoteExecution::Ssh::Runners
103
103
  # does not close its stderr which trips up the process manager which
104
104
  # expects all FDs to be closed
105
105
 
106
- full_command = [method.ssh_command_prefix, '/usr/bin/ssh', establish_ssh_options, method.ssh_options, @host, 'true'].flatten
106
+ full_command = [method.ssh_command_prefix, 'ssh', establish_ssh_options, method.ssh_options, @host, 'true'].flatten
107
107
  log_command(full_command)
108
108
  pm = Proxy::Dynflow::ProcessManager.new(full_command)
109
109
  pm.start!
@@ -166,7 +166,7 @@ module Proxy::RemoteExecution::Ssh::Runners
166
166
  end
167
167
 
168
168
  def verify_key_passphrase
169
- command = ['/usr/bin/ssh-keygen', '-y', '-f', File.expand_path(@client_private_key_file)]
169
+ command = ['ssh-keygen', '-y', '-f', File.expand_path(@client_private_key_file)]
170
170
  log_command(command, label: "Checking if private key has passphrase")
171
171
  pm = Proxy::Dynflow::ProcessManager.new(command)
172
172
  pm.start!
@@ -25,7 +25,10 @@ module Proxy::RemoteExecution::Ssh
25
25
  # :mqtt_broker => nil,
26
26
  # :mqtt_port => nil,
27
27
  # :mqtt_tls => nil,
28
- :mode => :ssh
28
+ # :mqtt_rate_limit => nil
29
+ :mode => :ssh,
30
+ :mqtt_resend_interval => 900,
31
+ :mqtt_ttl => 5
29
32
 
30
33
  capability(proc { 'cockpit' if settings.cockpit_integration })
31
34
 
@@ -45,6 +48,11 @@ module Proxy::RemoteExecution::Ssh
45
48
  Proxy::RemoteExecution::Ssh.validate!
46
49
 
47
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
48
56
  end
49
57
 
50
58
  def self.simulate?
@@ -133,7 +133,6 @@ module Proxy::RemoteExecution::Ssh::Runners
133
133
  def cleanup
134
134
  if @cleanup_working_dirs
135
135
  ensure_remote_command("rm -rf #{remote_command_dir}",
136
- publish: true,
137
136
  error: "Unable to remove working directory #{remote_command_dir} on remote system, exit code: %{exit_code}")
138
137
  end
139
138
  end
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.8.0'
4
+ VERSION = '0.10.0'
5
5
  end
6
6
  end
7
7
  end
@@ -32,3 +32,7 @@
32
32
  # unset, SSL gets used if smart-proxy's foreman_ssl_cert, foreman_ssl_key and
33
33
  # foreman_ssl_ca settings are set available.
34
34
  # :mqtt_tls:
35
+
36
+ # The notification is sent over mqtt every $mqtt_resend_interval seconds, until
37
+ # the job is picked up by the host or cancelled
38
+ # :mqtt_resend_interval: 900
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.8.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: 1980-01-01 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
@@ -205,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
207
  - !ruby/object:Gem::Version
206
208
  version: '0'
207
209
  requirements: []
208
- rubygems_version: 3.2.26
210
+ rubygems_version: 3.3.20
209
211
  signing_key:
210
212
  specification_version: 4
211
213
  summary: Ssh remote execution provider for Foreman Smart-Proxy