smart_proxy_remote_execution_ssh 0.10.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '048f8694430967060fae6bd7790fe56930b5c2bac37f3bc26e7b13db69081da6'
4
- data.tar.gz: 5a3b1e344a47aa4c97dcce97f43dc9e424d398f79c67a6f1e066d670758c99f0
3
+ metadata.gz: e155b0459ac25c633c9c38de0d288b22779f5ea7f6d82146e5505b83cfd7e12e
4
+ data.tar.gz: 700810a05b8fac1b57ebcc4ca52c5c2de45575dac1b81fe9b84f5c066c6f1dad
5
5
  SHA512:
6
- metadata.gz: a4bc90b3dda6d190c8f94aeb6731a3a6679788ea4f950e1bb4196c1d5da4630b170fe152e9c96a5e4cb7dcd25ba1d6371292e52b4059773759c1fbe8461bd347
7
- data.tar.gz: f4849b32f7529cbb29d127ae1c4c9fd9642b0700c834b1e3eebf59b5d9cfca282fca674188a734a37b36d45ff3e423096aa3512ceae118aef7ff831cb1600a38
6
+ metadata.gz: 11d92de711f06ee33deabf98442e5eb998a24cbd78f8b548a977cd2189a19e7ee998106d8935dbbfe7f1d7b74cc0fc7eb924430fb6f6a98b9565bcde09f917fd
7
+ data.tar.gz: f23cce7baef47c6651c1ef894b7f39d27b6b043153ed04e388940bab4898633689acd5aa66587be5f01651037d754357c8dd79f162e3d8a72a3430d6f7294efd
@@ -4,9 +4,20 @@ require 'time'
4
4
 
5
5
  module Proxy::RemoteExecution::Ssh::Actions
6
6
  class PullScript < Proxy::Dynflow::Action::Runner
7
+ include ::Dynflow::Action::Timeouts
8
+
7
9
  JobDelivered = Class.new
8
10
  PickupTimeout = Class.new
9
11
 
12
+ # The proxy has the job stored in its job storage
13
+ READY_FOR_PICKUP = 'ready_for_pickup'
14
+ # The host was notified over MQTT at least once
15
+ NOTIFIED = 'notified'
16
+ # The host has picked up the job
17
+ DELIVERED = 'delivered'
18
+ # We received at least one output from the host
19
+ RUNNING = 'running'
20
+
10
21
  execution_plan_hooks.use :cleanup, :on => :stopped
11
22
 
12
23
  def plan(action_input, mqtt: false)
@@ -16,14 +27,22 @@ module Proxy::RemoteExecution::Ssh::Actions
16
27
 
17
28
  def run(event = nil)
18
29
  if event == JobDelivered
19
- output[:state] = :delivered
30
+ output[:state] = DELIVERED
20
31
  suspend
21
32
  elsif event == PickupTimeout
22
33
  process_pickup_timeout
34
+ elsif event == ::Dynflow::Action::Timeouts::Timeout
35
+ process_timeout
36
+ elsif event.nil?
37
+ if (timeout = input['execution_timeout_interval'])
38
+ schedule_timeout(timeout, optional: true)
39
+ end
40
+ super
23
41
  else
24
42
  super
25
43
  end
26
44
  rescue => e
45
+ cleanup
27
46
  action_logger.error(e)
28
47
  process_update(Proxy::Dynflow::Runner::Update.encode_exception('Proxy error', e))
29
48
  end
@@ -35,8 +54,10 @@ module Proxy::RemoteExecution::Ssh::Actions
35
54
 
36
55
  plan_event(PickupTimeout, input[:time_to_pickup], optional: true) if input[:time_to_pickup]
37
56
 
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])
39
- output[:state] = :ready_for_pickup
57
+ input[:job_uuid] =
58
+ job_storage.store_job(host_name, execution_plan_id, run_step_id, input[:script].tr("\r", ''),
59
+ effective_user: input[:effective_user])
60
+ output[:state] = READY_FOR_PICKUP
40
61
  output[:result] = []
41
62
 
42
63
  mqtt_start(otp_password) if input[:with_mqtt]
@@ -50,7 +71,7 @@ module Proxy::RemoteExecution::Ssh::Actions
50
71
  end
51
72
 
52
73
  def process_external_event(event)
53
- output[:state] = :running
74
+ output[:state] = RUNNING
54
75
  data = event.data
55
76
  case data['version']
56
77
  when nil
@@ -64,7 +85,11 @@ module Proxy::RemoteExecution::Ssh::Actions
64
85
 
65
86
  def process_external_unversioned(payload)
66
87
  continuous_output = Proxy::Dynflow::ContinuousOutput.new
67
- Array(payload['output']).each { |line| continuous_output.add_output(line, payload['type']) } if payload.key?('output')
88
+ if payload.key?('output')
89
+ Array(payload['output']).each do |line|
90
+ continuous_output.add_output(line, payload['type'])
91
+ end
92
+ end
68
93
  exit_code = payload['exit_code'].to_i if payload['exit_code']
69
94
  process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
70
95
  end
@@ -89,19 +114,34 @@ module Proxy::RemoteExecution::Ssh::Actions
89
114
  process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
90
115
  end
91
116
 
92
- def kill_run
117
+ def process_timeout
118
+ kill_run "Execution timeout exceeded"
119
+ end
120
+
121
+ def kill_run(fail_msg = 'The job was cancelled by the user')
122
+ continuous_output = Proxy::Dynflow::ContinuousOutput.new
123
+ exit_code = nil
124
+
93
125
  case output[:state]
94
- when :ready_for_pickup
126
+ when READY_FOR_PICKUP, NOTIFIED
95
127
  # If the job is not running yet on the client, wipe it from storage
96
128
  cleanup
97
- # TODO: Stop the action
98
- when :notified, :running
129
+ exit_code = 'EXCEPTION'
130
+ when DELIVERED, RUNNING
99
131
  # Client was notified or is already running, dealing with this situation
100
132
  # is only supported if mqtt is available
101
133
  # Otherwise we have to wait it out
102
- mqtt_cancel if input[:with_mqtt]
134
+ if input[:with_mqtt]
135
+ mqtt_cancel
136
+ fail_msg += ', notifying the host over MQTT'
137
+ else
138
+ fail_msg += ', however the job was triggered without MQTT and cannot be stopped'
139
+ end
103
140
  end
104
- suspend
141
+ continuous_output.add_output(fail_msg + "\n")
142
+ process_update(Proxy::Dynflow::Runner::Update.new(continuous_output, exit_code))
143
+
144
+ suspend unless exit_code
105
145
  end
106
146
 
107
147
  def mqtt_start(otp_password)
@@ -118,12 +158,12 @@ module Proxy::RemoteExecution::Ssh::Actions
118
158
  },
119
159
  )
120
160
  Proxy::RemoteExecution::Ssh::MQTT::Dispatcher.instance.new(input[:job_uuid], mqtt_topic, payload)
121
- output[:state] = :notified
161
+ output[:state] = NOTIFIED
122
162
  end
123
163
 
124
164
  def mqtt_cancel
125
- cleanup
126
165
  payload = mqtt_payload_base.merge(
166
+ content: "#{input[:proxy_url]}/ssh/jobs/#{input[:job_uuid]}/cancel",
127
167
  metadata: {
128
168
  'event': 'cancel',
129
169
  'job_uuid': input[:job_uuid]
@@ -163,11 +203,9 @@ module Proxy::RemoteExecution::Ssh::Actions
163
203
  end
164
204
 
165
205
  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
206
+ suspend unless [READY_FOR_PICKUP, NOTIFIED].include? output[:state]
207
+
208
+ kill_run 'The job was not picked up in time'
171
209
  end
172
210
  end
173
211
  end
@@ -71,6 +71,14 @@ module Proxy::RemoteExecution
71
71
  end
72
72
  end
73
73
 
74
+ get "/jobs/:job_uuid/cancel" do |job_uuid|
75
+ do_authorize_with_ssl_client
76
+
77
+ with_authorized_job(job_uuid) do |job_record|
78
+ {}
79
+ end
80
+ end
81
+
74
82
  private
75
83
 
76
84
  def notify_job(job_record, event)
@@ -2,15 +2,39 @@ require 'concurrent'
2
2
  require 'mqtt'
3
3
 
4
4
  class Proxy::RemoteExecution::Ssh::MQTT
5
+ class DispatcherSupervisor < Concurrent::Actor::RestartingContext
6
+ def initialize
7
+ limit = Proxy::RemoteExecution::Ssh::Plugin.settings[:mqtt_rate_limit]
8
+ @dispatcher = DispatcherActor.spawn('MQTT dispatcher',
9
+ Proxy::Dynflow::Core.world.clock,
10
+ limit)
11
+ end
12
+
13
+ def on_message(message)
14
+ case message
15
+ when :dispatcher_reference
16
+ @dispatcher
17
+ when :resumed
18
+ # Carry on
19
+ else
20
+ pass
21
+ end
22
+ end
23
+
24
+ # In case an exception is raised during processing, instruct concurrent-ruby
25
+ # to keep going without losing state
26
+ def behaviour_definition
27
+ Concurrent::Actor::Behaviour.restarting_behaviour_definition(:resume!)
28
+ end
29
+ end
30
+
5
31
  class Dispatcher
6
32
  include Singleton
7
33
 
8
34
  attr_reader :reference
9
35
  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)
36
+ @supervisor = DispatcherSupervisor.spawn(name: 'RestartingSupervisor', args: [])
37
+ @reference = @supervisor.ask!(:dispatcher_reference)
14
38
  end
15
39
 
16
40
  def new(uuid, topic, payload)
@@ -159,11 +183,5 @@ class Proxy::RemoteExecution::Ssh::MQTT
159
183
  def timer_off
160
184
  @timer.shutdown
161
185
  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
186
  end
169
187
  end
@@ -103,7 +103,8 @@ 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, '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,
107
+ 'true'].flatten
107
108
  log_command(full_command)
108
109
  pm = Proxy::Dynflow::ProcessManager.new(full_command)
109
110
  pm.start!
@@ -154,6 +155,8 @@ module Proxy::RemoteExecution::Ssh::Runners
154
155
  ssh_options << "-o ControlPath=#{socket_file}"
155
156
  ssh_options << "-o ControlPersist=yes"
156
157
  ssh_options << "-o ProxyCommand=none"
158
+ ssh_options << "-o ServerAliveInterval=15"
159
+ ssh_options << "-o ServerAliveCountMax=3" # This is the default, but let's be explicit
157
160
  @establish_ssh_options = ssh_options
158
161
  end
159
162
 
@@ -90,7 +90,6 @@ module Proxy::RemoteExecution::Ssh::Runners
90
90
  def reset; end
91
91
  end
92
92
 
93
- # rubocop:disable Metrics/ClassLength
94
93
  class ScriptRunner < Proxy::Dynflow::Runner::Base
95
94
  include Proxy::Dynflow::Runner::ProcessManagerCommand
96
95
  include CommandLogging
@@ -180,7 +179,10 @@ module Proxy::RemoteExecution::Ssh::Runners
180
179
  @output_path = File.join(File.dirname(@remote_script), 'output')
181
180
  @exit_code_path = File.join(File.dirname(@remote_script), 'exit_code')
182
181
  @pid_path = File.join(File.dirname(@remote_script), 'pid')
183
- @remote_script_wrapper = upload_data("echo $$ > #{@pid_path}; exec \"$@\";", File.join(File.dirname(@remote_script), 'script-wrapper'), 555)
182
+ @remote_script_wrapper = upload_data(
183
+ "echo $$ > #{@pid_path}; exec \"$@\";",
184
+ File.join(File.dirname(@remote_script), 'script-wrapper'),
185
+ 555)
184
186
  end
185
187
 
186
188
  # the script that initiates the execution
@@ -192,7 +194,11 @@ module Proxy::RemoteExecution::Ssh::Runners
192
194
  #{@remote_script_wrapper} #{@user_method.cli_command_prefix}#{su_method ? "'#{@remote_script} < /dev/null '" : "#{@remote_script} < /dev/null"}
193
195
  echo \\$?>#{@exit_code_path}
194
196
  EOF
195
- exit $(cat #{@exit_code_path})
197
+ if [ -f #{@exit_code_path} ] && [ $(wc -l < #{@exit_code_path}) -gt 0 ]; then
198
+ exit $(cat #{@exit_code_path})
199
+ else
200
+ exit 1
201
+ fi
196
202
  SCRIPT
197
203
  end
198
204
 
@@ -394,5 +400,4 @@ module Proxy::RemoteExecution::Ssh::Runners
394
400
  end
395
401
  end
396
402
  end
397
- # rubocop:enable Metrics/ClassLength
398
403
  end
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module RemoteExecution
3
3
  module Ssh
4
- VERSION = '0.10.0'
4
+ VERSION = '0.10.2'
5
5
  end
6
6
  end
7
7
  end
@@ -50,7 +50,10 @@ module Proxy::RemoteExecution
50
50
  raise 'mqtt_port has to be set when pull-mqtt mode is used' if Plugin.settings.mqtt_port.nil?
51
51
 
52
52
  if Plugin.settings.mqtt_tls.nil?
53
- Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key], [:foreman_ssl_ca, :ssl_ca_file]].all? { |(client, server)| ::Proxy::SETTINGS[client] || ::Proxy::SETTINGS[server] }
53
+ Plugin.settings.mqtt_tls = [[:foreman_ssl_cert, :ssl_certificate], [:foreman_ssl_key, :ssl_private_key],
54
+ [:foreman_ssl_ca, :ssl_ca_file]].all? do |(client, server)|
55
+ ::Proxy::SETTINGS[client] || ::Proxy::SETTINGS[server]
56
+ end
54
57
  end
55
58
  end
56
59
 
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.10.0
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Nečas
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-12 00:00:00.000000000 Z
11
+ date: 2023-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1'
61
+ version: '2.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1'
68
+ version: '2.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: webmock
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -192,7 +192,7 @@ homepage: https://github.com/theforeman/smart_proxy_remote_execution_ssh
192
192
  licenses:
193
193
  - GPL-3.0
194
194
  metadata: {}
195
- post_install_message:
195
+ post_install_message:
196
196
  rdoc_options: []
197
197
  require_paths:
198
198
  - lib
@@ -207,8 +207,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
209
  requirements: []
210
- rubygems_version: 3.3.20
211
- signing_key:
210
+ rubygems_version: 3.1.6
211
+ signing_key:
212
212
  specification_version: 4
213
213
  summary: Ssh remote execution provider for Foreman Smart-Proxy
214
214
  test_files: []