smart_proxy_dynflow 0.4.0 → 0.6.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.
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