smart_proxy_dynflow 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -7
  3. data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
  4. data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
  5. data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
  6. data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
  7. data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
  8. data/lib/smart_proxy_dynflow/action/runner.rb +81 -0
  9. data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
  10. data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
  11. data/lib/smart_proxy_dynflow/action.rb +12 -0
  12. data/lib/smart_proxy_dynflow/api.rb +1 -1
  13. data/lib/smart_proxy_dynflow/callback.rb +8 -44
  14. data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
  15. data/lib/smart_proxy_dynflow/core.rb +2 -2
  16. data/lib/smart_proxy_dynflow/helpers.rb +11 -6
  17. data/lib/smart_proxy_dynflow/log.rb +1 -1
  18. data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
  19. data/lib/smart_proxy_dynflow/plugin.rb +9 -1
  20. data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
  21. data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
  22. data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
  23. data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
  24. data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
  25. data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
  26. data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
  27. data/lib/smart_proxy_dynflow/runner.rb +10 -0
  28. data/lib/smart_proxy_dynflow/settings.rb +1 -1
  29. data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
  30. data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
  31. data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
  32. data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
  33. data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
  34. data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
  35. data/lib/smart_proxy_dynflow/task_launcher_registry.rb +1 -1
  36. data/lib/smart_proxy_dynflow/testing.rb +1 -1
  37. data/lib/smart_proxy_dynflow/ticker.rb +47 -0
  38. data/lib/smart_proxy_dynflow/version.rb +2 -2
  39. data/lib/smart_proxy_dynflow.rb +2 -7
  40. metadata +57 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3c381a5358391ea92bacf8c59c3a279b49a50a49369bd962c0ff94e7c4d459e
4
- data.tar.gz: 3d83db86f0ce2a76b5d1e1f4ca5afd57eb82a5b327ba1ad2f561c77217e4ae0f
3
+ metadata.gz: b904806b1f4b0084f948d91f800d255ecf7bca92b1ed6d78fee49761a29affc9
4
+ data.tar.gz: 1eb7b63daaac6fbc3bdcb24d2938a93d2bd5728bcdb934f8384f41709a072cbe
5
5
  SHA512:
6
- metadata.gz: da9ce875255e93fcc491fc438eb6cbe3fb0b803b6524539a71575f47b29e0a65bcffb1378a6bacc3eee85e4c5194a3d0e3a9f77faff3fe5a82211f4242f671ea
7
- data.tar.gz: 55b4f92fdf94ca2b0cde9282f02be12d167674bd358729bd26a766d320a45de0dcc2f8535d94dc31b164963691e076b6b3740c60f385b4f9ca18b2d06c8abf82
6
+ metadata.gz: 887d7788a14a93d693c3ec2a7ddc110fc5fd6fe62619da946f6e0d68f2ec8542b69670769d0cf9e7eafccee3af04b9bf89fef3565346d69bfa1eb4f71492b119
7
+ data.tar.gz: e1c2f822a02805f2136ba363d13028699dea85e05ed41ba988853c5aadf3358d5f59f65006e5404321c087aea7a96699582ba8dd1fbc8f1a67a5e09943509d2d
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec :name => 'smart_proxy_dynflow_core'
3
+ gemspec :name => 'smart_proxy_dynflow'
4
4
 
5
5
  group :development do
6
6
  gem 'pry'
@@ -8,10 +8,12 @@ end
8
8
 
9
9
  group :test do
10
10
  gem 'smart_proxy', :git => "https://github.com/theforeman/smart-proxy", :branch => "develop"
11
- gem 'smart_proxy_dynflow', :path => '.'
12
11
 
12
+ gem 'minitest'
13
+ gem 'mocha'
13
14
  gem 'public_suffix'
14
15
  gem 'rack-test'
16
+ gem 'rake'
15
17
  gem 'rubocop', '~> 0.52.1'
16
18
  end
17
19
 
@@ -19,11 +21,6 @@ gem 'logging-journald', '~> 2.0', :platforms => [:ruby], :require => false
19
21
  gem 'rack', '>= 1.1'
20
22
  gem 'sinatra'
21
23
 
22
- # load bundler.d
23
- Dir["#{File.dirname(__FILE__)}/bundler.d/*.rb"].each do |bundle|
24
- self.instance_eval(Bundler.read_file(bundle))
25
- end
26
-
27
24
  # load local gemfile
28
25
  local_gemfile = File.join(File.dirname(__FILE__), 'Gemfile.local.rb')
29
26
  self.instance_eval(Bundler.read_file(local_gemfile)) if File.exist?(local_gemfile)
File without changes
@@ -0,0 +1,21 @@
1
+ module Proxy::Dynflow::Action
2
+ class Batch < ::Dynflow::Action
3
+ include Dynflow::Action::WithSubPlans
4
+ include Dynflow::Action::WithPollingSubPlans
5
+
6
+ # { task_id => { :action_class => Klass, :input => input } }
7
+ def plan(launcher, input_hash)
8
+ launcher.launch_children(self, input_hash)
9
+ plan_self
10
+ end
11
+
12
+ def initiate
13
+ ping suspended_action
14
+ wait_for_sub_plans sub_plans
15
+ end
16
+
17
+ def rescue_strategy
18
+ Dynflow::Action::Rescue::Fail
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module Proxy::Dynflow::Action
2
+ class BatchCallback < ::Dynflow::Action
3
+ def plan(input_hash, results)
4
+ plan_self :targets => input_hash, :results => results
5
+ end
6
+
7
+ def run
8
+ payload = format_payload(input['targets'], input['results'])
9
+ Proxy::Dynflow::Callback::Request.new.callback({ :callbacks => payload }.to_json)
10
+ end
11
+
12
+ private
13
+
14
+ def format_payload(input_hash, results)
15
+ input_hash.map do |task_id, callback|
16
+ { :callback => callback, :data => results[task_id] }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require 'smart_proxy_dynflow/action/runner'
2
+
3
+ module Proxy::Dynflow::Action
4
+ class BatchRunner < ::Proxy::Dynflow::Action::Runner
5
+ def plan(launcher, input)
6
+ plan_self :targets => launcher.runner_input(input), :operation => launcher.operation
7
+ end
8
+
9
+ def initiate_runner
10
+ launcher = Proxy::Dynflow::TaskLauncherRegistry.fetch(input[:operation])
11
+ launcher.runner_class.new(input[:targets], suspended_action: suspended_action)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ module Proxy::Dynflow::Action
2
+ class OutputCollector < ::Proxy::Dynflow::Action::Runner
3
+ def init_run
4
+ output[:result] = []
5
+ suspend
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,81 @@
1
+ require 'smart_proxy_dynflow/action/shareable'
2
+ module Proxy::Dynflow
3
+ module Action
4
+ class Runner < Shareable
5
+ include ::Dynflow::Action::Cancellable
6
+
7
+ def run(event = nil)
8
+ case event
9
+ when nil
10
+ init_run
11
+ when Proxy::Dynflow::Runner::Update
12
+ process_update(event)
13
+ when Proxy::Dynflow::Runner::ExternalEvent
14
+ process_external_event(event)
15
+ when ::Dynflow::Action::Cancellable::Cancel
16
+ kill_run
17
+ else
18
+ raise "Unexpected event #{event.inspect}"
19
+ end
20
+ rescue => e
21
+ action_logger.error(e)
22
+ process_update(Proxy::Dynflow::Runner::Update.encode_exception('Proxy error', e))
23
+ end
24
+
25
+ def finalize
26
+ # To mark the task as a whole as failed
27
+ error! 'Script execution failed' if on_proxy? && failed_run?
28
+ end
29
+
30
+ def rescue_strategy_for_self
31
+ ::Dynflow::Action::Rescue::Fail
32
+ end
33
+
34
+ def initiate_runner
35
+ raise NotImplementedError
36
+ end
37
+
38
+ def init_run
39
+ output[:result] = []
40
+ output[:runner_id] = runner_dispatcher.start(suspended_action, initiate_runner)
41
+ suspend
42
+ end
43
+
44
+ def runner_dispatcher
45
+ Proxy::Dynflow::Runner::Dispatcher.instance
46
+ end
47
+
48
+ def kill_run
49
+ runner_dispatcher.kill(output[:runner_id])
50
+ suspend
51
+ end
52
+
53
+ def finish_run(update)
54
+ output[:exit_status] = update.exit_status
55
+ output[:result] = output_result
56
+ end
57
+
58
+ def process_external_event(event)
59
+ runner_dispatcher.external_event(output[:runner_id], event)
60
+ suspend
61
+ end
62
+
63
+ def process_update(update)
64
+ output_chunk(update.continuous_output.raw_outputs) unless update.continuous_output.raw_outputs.empty?
65
+ if update.exit_status
66
+ finish_run(update)
67
+ else
68
+ suspend
69
+ end
70
+ end
71
+
72
+ def failed_run?
73
+ output[:exit_status] != 0
74
+ end
75
+
76
+ def output_result
77
+ stored_output_chunks.map { |c| c[:chunk] }.reduce(&:concat)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,25 @@
1
+ module Proxy::Dynflow::Action
2
+ class Shareable < ::Dynflow::Action
3
+ def plan(input)
4
+ input = input.dup
5
+ callback = input.delete('callback')
6
+ if callback
7
+ input[:task_id] = callback['task_id']
8
+ else
9
+ input[:task_id] ||= SecureRandom.uuid
10
+ end
11
+
12
+ planned_action = plan_self(input)
13
+ # code only applicable, when run with SmartProxyDynflowCore in place
14
+ if on_proxy? && callback
15
+ plan_action(Proxy::Dynflow::Callback::Action, callback, planned_action.output)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def on_proxy?
22
+ true
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module Proxy::Dynflow::Action
2
+ class SingleRunnerBatch < Batch
3
+ def plan(launcher, input_hash)
4
+ launcher.launch_children(self, input_hash)
5
+ sequence do
6
+ results = plan_self
7
+ plan_action BatchCallback, launcher.prepare_batch(input_hash), results.output[:results]
8
+ end
9
+ end
10
+
11
+ def run(event = nil)
12
+ super unless event == Dynflow::Action::Skip
13
+ end
14
+
15
+ def initiate
16
+ ping suspended_action
17
+ wait_for_sub_plans sub_plans
18
+ end
19
+
20
+ def check_for_errors!(optional = true)
21
+ super unless optional
22
+ end
23
+
24
+ def on_finish
25
+ output[:results] = sub_plans.map(&:entry_action).reduce({}) do |acc, cur|
26
+ acc.merge(cur.execution_plan_id => cur.output)
27
+ end
28
+ end
29
+
30
+ def finalize
31
+ output.delete(:results)
32
+ check_for_errors!
33
+ end
34
+
35
+ def rescue_strategy_for_self
36
+ Dynflow::Action::Rescue::Skip
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ module Proxy::Dynflow
2
+ module Action
3
+ end
4
+ end
5
+
6
+ require 'smart_proxy_dynflow/action/batch'
7
+ require 'smart_proxy_dynflow/action/batch_callback'
8
+ require 'smart_proxy_dynflow/action/batch_runner'
9
+ require 'smart_proxy_dynflow/action/output_collector'
10
+ require 'smart_proxy_dynflow/action/shareable'
11
+ require 'smart_proxy_dynflow/action/runner'
12
+ require 'smart_proxy_dynflow/action/single_runner_batch'
@@ -3,7 +3,7 @@ require 'proxy/helpers'
3
3
  require 'sinatra/authorization'
4
4
 
5
5
  module Proxy
6
- class Dynflow
6
+ module Dynflow
7
7
  class Api < ::Sinatra::Base
8
8
  helpers ::Proxy::Helpers
9
9
  helpers ::Proxy::Log
@@ -1,58 +1,22 @@
1
1
  require 'rest-client'
2
2
 
3
- class Proxy::Dynflow
3
+ module Proxy::Dynflow
4
4
  module Callback
5
- class Request
6
- class << self
7
- def send_to_foreman_tasks(callback_info, data)
8
- self.new.callback(prepare_payload(callback_info, data))
9
- end
10
-
11
- def ssl_options
12
- return @ssl_options if defined? @ssl_options
13
- @ssl_options = {}
14
- settings = Proxy::SETTINGS
15
- return @ssl_options unless URI.parse(settings.foreman_url).scheme == 'https'
16
-
17
- @ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
18
-
19
- private_key_file = settings.foreman_ssl_key || settings.ssl_private_key
20
- if private_key_file
21
- private_key = File.read(private_key_file)
22
- @ssl_options[:ssl_client_key] = OpenSSL::PKey::RSA.new(private_key)
23
- end
24
- certificate_file = settings.foreman_ssl_cert || settings.ssl_certificate
25
- if certificate_file
26
- certificate = File.read(certificate_file)
27
- @ssl_options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(certificate)
28
- end
29
- ca_file = settings.foreman_ssl_ca || settings.ssl_ca_file
30
- @ssl_options[:ssl_ca_file] = ca_file if ca_file
31
- @ssl_options
32
- end
33
- # rubocop:enable Metrics/PerceivedComplexity
34
-
35
- private
36
-
37
- def prepare_payload(callback, data)
38
- { :callback => callback, :data => data }.to_json
39
- end
5
+ class Request < ::Proxy::HttpRequest::ForemanRequest
6
+ def self.send_to_foreman_tasks(callback_info, data)
7
+ self.new.callback({ :callback => callback_info, :data => data }.to_json)
40
8
  end
41
9
 
42
10
  def callback(payload)
43
- response = callback_resource.post(payload, :content_type => :json)
11
+ request = request_factory.create_post '/foreman_tasks/api/tasks/callback',
12
+ payload
13
+ response = send_request(request)
14
+
44
15
  if response.code.to_s != "200"
45
16
  raise "Failed performing callback to Foreman server: #{response.code} #{response.body}"
46
17
  end
47
18
  response
48
19
  end
49
-
50
- private
51
-
52
- def callback_resource
53
- @resource ||= RestClient::Resource.new(Proxy::SETTINGS.foreman_url + '/foreman_tasks/api/tasks/callback',
54
- self.class.ssl_options)
55
- end
56
20
  end
57
21
 
58
22
  class Action < ::Dynflow::Action
@@ -0,0 +1,50 @@
1
+ module Proxy::Dynflow
2
+ class ContinuousOutput
3
+ attr_accessor :raw_outputs
4
+
5
+ def initialize(raw_outputs = [])
6
+ @raw_outputs = []
7
+ raw_outputs.each { |raw_output| add_raw_output(raw_output) }
8
+ end
9
+
10
+ def add_raw_output(raw_output)
11
+ missing_args = %w[output_type output timestamp] - raw_output.keys
12
+ unless missing_args.empty?
13
+ raise ArgumentError, "Missing args for raw output: #{missing_args.inspect}"
14
+ end
15
+ @raw_outputs << raw_output
16
+ end
17
+
18
+ def empty?
19
+ @raw_outputs.empty?
20
+ end
21
+
22
+ def last_timestamp
23
+ return if @raw_outputs.empty?
24
+ @raw_outputs.last.fetch('timestamp')
25
+ end
26
+
27
+ def sort!
28
+ @raw_outputs.sort_by! { |record| record['timestamp'].to_f }
29
+ end
30
+
31
+ def humanize
32
+ sort!
33
+ raw_outputs.map { |output| output['output'] }.join("\n")
34
+ end
35
+
36
+ def add_exception(context, exception, timestamp = Time.now.getlocal)
37
+ add_output(context + ": #{exception.class} - #{exception.message}", 'debug', timestamp)
38
+ end
39
+
40
+ def add_output(*args)
41
+ add_raw_output(self.class.format_output(*args))
42
+ end
43
+
44
+ def self.format_output(message, type = 'debug', timestamp = Time.now.getlocal)
45
+ { 'output_type' => type,
46
+ 'output' => message,
47
+ 'timestamp' => timestamp.to_f }
48
+ end
49
+ end
50
+ end
@@ -1,4 +1,4 @@
1
- class Proxy::Dynflow
1
+ module Proxy::Dynflow
2
2
  class Core
3
3
  attr_accessor :world, :accepted_cert_serial
4
4
 
@@ -75,7 +75,7 @@ class Proxy::Dynflow
75
75
  end
76
76
 
77
77
  def silencer_matchers
78
- @matchers ||= []
78
+ @matchers ||= [::Dynflow::DeadLetterSilencer::Matcher.new(Ticker)]
79
79
  end
80
80
 
81
81
  def register_silencer_matchers(matchers)
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
- class Dynflow
2
+ module Dynflow
3
3
  module Helpers
4
4
  def world
5
5
  Proxy::Dynflow::Core.world
@@ -7,12 +7,12 @@ module Proxy
7
7
 
8
8
  def authorize_with_token(task_id:, clear: true)
9
9
  if request.env.key? 'HTTP_AUTHORIZATION'
10
- if defined?(::ForemanTasksCore)
10
+ if defined?(::Proxy::Dynflow)
11
11
  auth = request.env['HTTP_AUTHORIZATION']
12
12
  basic_prefix = /\ABasic /
13
13
  if !auth.to_s.empty? && auth =~ basic_prefix &&
14
- ForemanTasksCore::OtpManager.authenticate(auth.gsub(basic_prefix, ''),
15
- expected_user: task_id, clear: clear)
14
+ Proxy::Dynflow::OtpManager.authenticate(auth.gsub(basic_prefix, ''),
15
+ expected_user: task_id, clear: clear)
16
16
  Log.instance.debug('authorized with token')
17
17
  return true
18
18
  end
@@ -35,7 +35,12 @@ module Proxy
35
35
 
36
36
  def task_status(task_id)
37
37
  ep = world.persistence.load_execution_plan(task_id)
38
- ep.to_hash.merge(:actions => ep.actions.map(&:to_hash))
38
+ actions = ep.actions.map do |action|
39
+ hash = action.to_hash
40
+ hash[:output][:result] = action.output_result if action.is_a?(Proxy::Dynflow::Action::Runner)
41
+ hash
42
+ end
43
+ ep.to_hash.merge(:actions => actions)
39
44
  rescue KeyError => _e
40
45
  status 404
41
46
  {}
@@ -51,7 +56,7 @@ module Proxy
51
56
  def dispatch_external_event(task_id, params)
52
57
  world.event(task_id,
53
58
  params['step_id'].to_i,
54
- ::ForemanTasksCore::Runner::ExternalEvent.new(params))
59
+ ::Proxy::Dynflow::Runner::ExternalEvent.new(params))
55
60
  end
56
61
  end
57
62
  end
@@ -1,6 +1,6 @@
1
1
  require 'logging'
2
2
 
3
- class Proxy::Dynflow
3
+ module Proxy::Dynflow
4
4
  class Log
5
5
  LOGGER_NAME = 'dynflow-core'.freeze
6
6
 
@@ -0,0 +1,36 @@
1
+ require 'base64'
2
+ require 'securerandom'
3
+
4
+ module Proxy::Dynflow
5
+ class OtpManager
6
+ class << self
7
+ def generate_otp(username)
8
+ otp = SecureRandom.hex
9
+ passwords[username] = otp.to_s
10
+ end
11
+
12
+ def drop_otp(username, password)
13
+ passwords.delete(username) if passwords[username] == password
14
+ end
15
+
16
+ def passwords
17
+ @password ||= {}
18
+ end
19
+
20
+ def authenticate(hash, expected_user: nil, clear: true)
21
+ plain = Base64.decode64(hash)
22
+ username, otp = plain.split(':', 2)
23
+ if expected_user
24
+ return false unless expected_user == username
25
+ end
26
+ password_matches = passwords[username] == otp
27
+ passwords.delete(username) if clear && password_matches
28
+ password_matches
29
+ end
30
+
31
+ def tokenize(username, password)
32
+ Base64.strict_encode64("#{username}:#{password}")
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,7 +2,7 @@ require 'proxy/log'
2
2
  require 'proxy/pluggable'
3
3
  require 'proxy/plugin'
4
4
 
5
- class Proxy::Dynflow
5
+ module Proxy::Dynflow
6
6
  class Plugin < Proxy::Plugin
7
7
  rackup_path = File.expand_path('http_config.ru', __dir__)
8
8
  http_rackup_path rackup_path
@@ -15,6 +15,14 @@ class Proxy::Dynflow
15
15
  plugin :dynflow, Proxy::Dynflow::VERSION
16
16
 
17
17
  after_activation do
18
+ require 'smart_proxy_dynflow/settings_loader'
19
+ require 'smart_proxy_dynflow/otp_manager'
20
+ require 'smart_proxy_dynflow/action'
21
+ require 'smart_proxy_dynflow/task_launcher'
22
+
23
+ Proxy::Dynflow::TaskLauncherRegistry.register('single',
24
+ Proxy::Dynflow::TaskLauncher::Single)
25
+
18
26
  Proxy::Dynflow::Core.ensure_initialized
19
27
  end
20
28
  end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
- class Dynflow
2
+ module Dynflow
3
3
  class ProxyAdapter < ::Dynflow::LoggerAdapters::Simple
4
4
  def initialize(logger, level = Logger::DEBUG, formatters = [Formatters::Exception])
5
5
  super(nil, level, formatters)
@@ -0,0 +1,98 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ # Runner is an object that is able to initiate some action and
4
+ # provide update data on refresh call.
5
+ class Base
6
+ attr_reader :id
7
+ attr_writer :logger
8
+
9
+ def initialize(*_args, suspended_action: nil)
10
+ @suspended_action = suspended_action
11
+ @id = SecureRandom.uuid
12
+ initialize_continuous_outputs
13
+ end
14
+
15
+ def logger
16
+ @logger ||= Logger.new(STDERR)
17
+ end
18
+
19
+ def run_refresh
20
+ logger.debug('refreshing runner')
21
+ refresh
22
+ generate_updates
23
+ end
24
+
25
+ # by default, external event just causes the refresh to be triggered: this allows the descendants
26
+ # of the Base to add custom logic to process the external events.
27
+ # Similarly as `run_refresh`, it's expected to return updates to be dispatched.
28
+ def external_event(_event)
29
+ run_refresh
30
+ end
31
+
32
+ def start
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def refresh
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def kill
41
+ # Override when you can kill the runner in the middle
42
+ end
43
+
44
+ def close
45
+ # if cleanup is needed
46
+ end
47
+
48
+ def timeout
49
+ # Override when timeouts and regular kills should be handled differently
50
+ publish_data('Timeout for execution passed, trying to stop the job', 'debug')
51
+ kill
52
+ end
53
+
54
+ def timeout_interval
55
+ # A number of seconds after which the runner should receive a #timeout
56
+ # or nil for no timeout
57
+ end
58
+
59
+ def publish_data(data, type)
60
+ @continuous_output.add_output(data, type)
61
+ end
62
+
63
+ def publish_exception(context, exception, fatal = true)
64
+ logger.error("#{context} - #{exception.class} #{exception.message}:\n" + \
65
+ exception.backtrace.join("\n"))
66
+ dispatch_exception context, exception
67
+ publish_exit_status('EXCEPTION') if fatal
68
+ end
69
+
70
+ def publish_exit_status(status)
71
+ @exit_status = status
72
+ end
73
+
74
+ def dispatch_exception(context, exception)
75
+ @continuous_output.add_exception(context, exception)
76
+ end
77
+
78
+ def generate_updates
79
+ return no_update if @continuous_output.empty? && @exit_status.nil?
80
+ new_data = @continuous_output
81
+ @continuous_output = Proxy::Dynflow::ContinuousOutput.new
82
+ new_update(new_data, @exit_status)
83
+ end
84
+
85
+ def no_update
86
+ {}
87
+ end
88
+
89
+ def new_update(data, exit_status)
90
+ { @suspended_action => Runner::Update.new(data, exit_status) }
91
+ end
92
+
93
+ def initialize_continuous_outputs
94
+ @continuous_output = ::Proxy::Dynflow::ContinuousOutput.new
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,40 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ module Command
4
+ def initialize_command(*command)
5
+ @command_out, @command_in, @command_pid = PTY.spawn(*command)
6
+ rescue Errno::ENOENT => e
7
+ publish_exception("Error running command '#{command.join(' ')}'", e)
8
+ end
9
+
10
+ def refresh
11
+ return if @command_out.nil?
12
+ ready_outputs, * = IO.select([@command_out], nil, nil, 0.1)
13
+ if ready_outputs
14
+ if @command_out.nread.positive?
15
+ lines = @command_out.read_nonblock(@command_out.nread)
16
+ else
17
+ close_io
18
+ Process.wait(@command_pid)
19
+ publish_exit_status($CHILD_STATUS.exitstatus)
20
+ end
21
+ publish_data(lines, 'stdout') if lines && !lines.empty?
22
+ end
23
+ end
24
+
25
+ def close
26
+ close_io
27
+ end
28
+
29
+ private
30
+
31
+ def close_io
32
+ @command_out.close if @command_out && !@command_out.closed?
33
+ @command_out = nil
34
+
35
+ @command_in.close if @command_in && !@command_in.closed?
36
+ @command_in = nil
37
+ end
38
+ end
39
+ end
40
+ end