smart_proxy_remote_execution_ssh 0.10.0 → 0.10.2

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: '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: []