smart_proxy_dynflow 0.4.0 → 0.5.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/smart_proxy_dynflow.rb +2 -7
  3. data/lib/smart_proxy_dynflow/action.rb +12 -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 +76 -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/api.rb +1 -1
  12. data/lib/smart_proxy_dynflow/callback.rb +1 -1
  13. data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
  14. data/lib/smart_proxy_dynflow/core.rb +2 -2
  15. data/lib/smart_proxy_dynflow/helpers.rb +5 -5
  16. data/lib/smart_proxy_dynflow/log.rb +1 -1
  17. data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
  18. data/lib/smart_proxy_dynflow/plugin.rb +6 -1
  19. data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
  20. data/lib/smart_proxy_dynflow/runner.rb +10 -0
  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/settings.rb +1 -1
  28. data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
  29. data/lib/smart_proxy_dynflow/task_launcher.rb +9 -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_registry.rb +1 -1
  35. data/lib/smart_proxy_dynflow/testing.rb +1 -1
  36. data/lib/smart_proxy_dynflow/ticker.rb +47 -0
  37. data/lib/smart_proxy_dynflow/version.rb +2 -2
  38. metadata +26 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3c381a5358391ea92bacf8c59c3a279b49a50a49369bd962c0ff94e7c4d459e
4
- data.tar.gz: 3d83db86f0ce2a76b5d1e1f4ca5afd57eb82a5b327ba1ad2f561c77217e4ae0f
3
+ metadata.gz: be2013a41681b30175177e9bcdb2bdc24186e6b6edccf3dcbdadcb5ca006f49b
4
+ data.tar.gz: 7c54a27ae6b0055cdd5b1189a8c89a1c644626fbe0386a9e48e3a48da3ebf71e
5
5
  SHA512:
6
- metadata.gz: da9ce875255e93fcc491fc438eb6cbe3fb0b803b6524539a71575f47b29e0a65bcffb1378a6bacc3eee85e4c5194a3d0e3a9f77faff3fe5a82211f4242f671ea
7
- data.tar.gz: 55b4f92fdf94ca2b0cde9282f02be12d167674bd358729bd26a766d320a45de0dcc2f8535d94dc31b164963691e076b6b3740c60f385b4f9ca18b2d06c8abf82
6
+ metadata.gz: 6fb8325c06bdac95fbc2b8b3e8b59cb9cf75f43ba71a944dc1cf423a36ea7cfe7a02be1b928f6f2f21728c7de90011cc5c24867dcce58ba90e66bda6396922cc
7
+ data.tar.gz: 913260f459727549e5559938558f09f398dce1d78e0c7cd42ad596e9da58394d92b2f9ba9dc5dd421371cffa71ae9f4c2038db15d0ce1e6ca4290627910a61ef
@@ -3,10 +3,9 @@ require 'dynflow'
3
3
  require 'smart_proxy_dynflow/task_launcher_registry'
4
4
  require 'smart_proxy_dynflow/middleware/keep_current_request_id'
5
5
 
6
- require 'foreman_tasks_core'
7
-
8
6
  require 'smart_proxy_dynflow/log'
9
7
  require 'smart_proxy_dynflow/settings'
8
+ require 'smart_proxy_dynflow/ticker'
10
9
  require 'smart_proxy_dynflow/core'
11
10
  require 'smart_proxy_dynflow/callback'
12
11
 
@@ -16,10 +15,6 @@ require 'smart_proxy_dynflow/helpers'
16
15
  require 'smart_proxy_dynflow/api'
17
16
 
18
17
  module Proxy
19
- class Dynflow
20
- Core.after_initialize do |dynflow_core|
21
- ForemanTasksCore.dynflow_setup(dynflow_core.world)
22
- end
23
- Core.register_silencer_matchers ForemanTasksCore.silent_dead_letter_matchers
18
+ module Dynflow
24
19
  end
25
20
  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'
@@ -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
+ SmartProxyDynflowCore::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 = SmartProxyDynflowCore::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::Runner::Action
3
+ def init_run
4
+ output[:result] = []
5
+ suspend
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,76 @@
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
+ end
56
+
57
+ def process_external_event(event)
58
+ runner_dispatcher.external_event(output[:runner_id], event)
59
+ suspend
60
+ end
61
+
62
+ def process_update(update)
63
+ output[:result].concat(update.continuous_output.raw_outputs)
64
+ if update.exit_status
65
+ finish_run(update)
66
+ else
67
+ suspend
68
+ end
69
+ end
70
+
71
+ def failed_run?
72
+ output[:exit_status] != 0
73
+ end
74
+ end
75
+ end
76
+ 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(SmartProxyDynflowCore::Callback::Action, callback, planned_action.output)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def on_proxy?
22
+ defined?(SmartProxyDynflowCore::Callback)
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
@@ -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,6 +1,6 @@
1
1
  require 'rest-client'
2
2
 
3
- class Proxy::Dynflow
3
+ module Proxy::Dynflow
4
4
  module Callback
5
5
  class Request
6
6
  class << self
@@ -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
@@ -51,7 +51,7 @@ module Proxy
51
51
  def dispatch_external_event(task_id, params)
52
52
  world.event(task_id,
53
53
  params['step_id'].to_i,
54
- ::ForemanTasksCore::Runner::ExternalEvent.new(params))
54
+ ::Proxy::Dynflow::Runner::ExternalEvent.new(params))
55
55
  end
56
56
  end
57
57
  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,11 @@ 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
+
18
23
  Proxy::Dynflow::Core.ensure_initialized
19
24
  end
20
25
  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,10 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ end
4
+ end
5
+
6
+ require 'smart_proxy_dynflow/runner/update'
7
+ require 'smart_proxy_dynflow/runner/base'
8
+ require 'smart_proxy_dynflow/runner/dispatcher'
9
+ require 'smart_proxy_dynflow/action/runner'
10
+ require 'smart_proxy_dynflow/runner/parent'
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'io/wait'
2
+ require 'pty'
3
+ require 'smart_proxy_dynflow/runner/command'
4
+
5
+ module Proxy::Dynflow
6
+ module Runner
7
+ class CommandRunner < Base
8
+ include Command
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,191 @@
1
+ require 'smart_proxy_dynflow/ticker'
2
+
3
+ module Proxy::Dynflow
4
+ module Runner
5
+ class Dispatcher
6
+ def self.instance
7
+ return @instance if @instance
8
+ @instance = new(Proxy::Dynflow::Core.world.clock,
9
+ Proxy::Dynflow::Core.world.logger)
10
+ end
11
+
12
+ class RunnerActor < ::Dynflow::Actor
13
+ def initialize(dispatcher, suspended_action, runner, clock, logger, _options = {})
14
+ @dispatcher = dispatcher
15
+ @clock = clock
16
+ @ticker = dispatcher.ticker
17
+ @logger = logger
18
+ @suspended_action = suspended_action
19
+ @runner = runner
20
+ @finishing = false
21
+ end
22
+
23
+ def on_envelope(*args)
24
+ super
25
+ rescue => e
26
+ handle_exception(e)
27
+ end
28
+
29
+ def start_runner
30
+ @logger.debug("start runner #{@runner.id}")
31
+ set_timeout if @runner.timeout_interval
32
+ @runner.start
33
+ refresh_runner
34
+ ensure
35
+ plan_next_refresh
36
+ end
37
+
38
+ def refresh_runner
39
+ @logger.debug("refresh runner #{@runner.id}")
40
+ dispatch_updates(@runner.run_refresh)
41
+ ensure
42
+ @refresh_planned = false
43
+ plan_next_refresh
44
+ end
45
+
46
+ def dispatch_updates(updates)
47
+ updates.each { |receiver, update| (receiver || @suspended_action) << update }
48
+
49
+ # This is a workaround when the runner does not accept the suspended action
50
+ main_key = updates.keys.any?(&:nil?) ? nil : @suspended_action
51
+ main_process = updates[main_key]
52
+ finish if main_process&.exit_status
53
+ end
54
+
55
+ def timeout_runner
56
+ @logger.debug("timeout runner #{@runner.id}")
57
+ @runner.timeout
58
+ rescue => e
59
+ handle_exception(e, false)
60
+ end
61
+
62
+ def kill
63
+ @logger.debug("kill runner #{@runner.id}")
64
+ @runner.kill
65
+ rescue => e
66
+ handle_exception(e, false)
67
+ end
68
+
69
+ def finish
70
+ @logger.debug("finish runner #{@runner.id}")
71
+ @finishing = true
72
+ @dispatcher.finish(@runner.id)
73
+ end
74
+
75
+ def start_termination(*args)
76
+ @logger.debug("terminate #{@runner.id}")
77
+ super
78
+ @runner.close
79
+ finish_termination
80
+ end
81
+
82
+ def external_event(event)
83
+ dispatch_updates(@runner.external_event(event))
84
+ end
85
+
86
+ private
87
+
88
+ def set_timeout
89
+ timeout_time = Time.now.getlocal + @runner.timeout_interval
90
+ @logger.debug("setting timeout for #{@runner.id} to #{timeout_time}")
91
+ @clock.ping(reference, timeout_time, :timeout_runner)
92
+ end
93
+
94
+ def plan_next_refresh
95
+ if !@finishing && !@refresh_planned
96
+ @logger.debug("planning to refresh #{@runner.id}")
97
+ @ticker.tell([:add_event, reference, :refresh_runner])
98
+ @refresh_planned = true
99
+ end
100
+ end
101
+
102
+ def handle_exception(exception, fatal = true)
103
+ @dispatcher.handle_command_exception(@runner.id, exception, fatal)
104
+ end
105
+ end
106
+
107
+ attr_reader :ticker
108
+ def initialize(clock, logger)
109
+ @mutex = Mutex.new
110
+ @clock = clock
111
+ @logger = logger
112
+ @ticker = ::Proxy::Dynflow::Ticker.spawn('dispatcher-ticker', @clock, @logger, refresh_interval)
113
+ @runner_actors = {}
114
+ @runner_suspended_actions = {}
115
+ end
116
+
117
+ def synchronize(&block)
118
+ @mutex.synchronize(&block)
119
+ end
120
+
121
+ def start(suspended_action, runner)
122
+ synchronize do
123
+ raise "Actor with runner id #{runner.id} already exists" if @runner_actors[runner.id]
124
+ runner.logger = @logger
125
+ runner_actor = RunnerActor.spawn("runner-actor-#{runner.id}", self, suspended_action, runner, @clock, @logger)
126
+ @runner_actors[runner.id] = runner_actor
127
+ @runner_suspended_actions[runner.id] = suspended_action
128
+ runner_actor.tell(:start_runner)
129
+ return runner.id
130
+ rescue => exception
131
+ _handle_command_exception(runner.id, exception)
132
+ return nil
133
+ end
134
+ end
135
+
136
+ def kill(runner_id)
137
+ synchronize do
138
+ runner_actor = @runner_actors[runner_id]
139
+ runner_actor&.tell(:kill)
140
+ rescue => exception
141
+ _handle_command_exception(runner_id, exception, false)
142
+ end
143
+ end
144
+
145
+ def finish(runner_id)
146
+ synchronize do
147
+ _finish(runner_id)
148
+ rescue => exception
149
+ _handle_command_exception(runner_id, exception, false)
150
+ end
151
+ end
152
+
153
+ def external_event(runner_id, external_event)
154
+ synchronize do
155
+ runner_actor = @runner_actors[runner_id]
156
+ runner_actor&.tell([:external_event, external_event])
157
+ end
158
+ end
159
+
160
+ def handle_command_exception(*args)
161
+ synchronize { _handle_command_exception(*args) }
162
+ end
163
+
164
+ def refresh_interval
165
+ 1
166
+ end
167
+
168
+ private
169
+
170
+ def _finish(runner_id)
171
+ runner_actor = @runner_actors.delete(runner_id)
172
+ return unless runner_actor
173
+ @logger.debug("closing session for command [#{runner_id}]," \
174
+ "#{@runner_actors.size} actors left ")
175
+ runner_actor.tell([:start_termination, Concurrent::Promises.resolvable_future])
176
+ ensure
177
+ @runner_suspended_actions.delete(runner_id)
178
+ end
179
+
180
+ def _handle_command_exception(runner_id, exception, fatal = true)
181
+ @logger.error("error while dispatching request to runner #{runner_id}:"\
182
+ "#{exception.class} #{exception.message}:\n #{exception.backtrace.join("\n")}")
183
+ suspended_action = @runner_suspended_actions[runner_id]
184
+ if suspended_action
185
+ suspended_action << Runner::Update.encode_exception('Runner error', exception, fatal)
186
+ end
187
+ _finish(runner_id) if fatal
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,57 @@
1
+ module Proxy::Dynflow
2
+ module Runner
3
+ class Parent < Base
4
+ # targets = { identifier => { :execution_plan_id => "...", :run_step_id => id,
5
+ # :input => { ... } }
6
+ def initialize(targets = {}, suspended_action: nil)
7
+ @targets = targets
8
+ @exit_statuses = {}
9
+ super suspended_action: suspended_action
10
+ end
11
+
12
+ def generate_updates
13
+ base = {}
14
+ base[@suspended_action] = Runner::Update.new(Proxy::Dynflow::ContinuousOutput.new, @exit_status) if @exit_status
15
+ # Operate on all hosts if the main process ended or only on hosts for which we have updates
16
+ @outputs.reject { |_, output| @exit_status.nil? && output.empty? }
17
+ .reduce(base) do |acc, (identifier, output)|
18
+ @outputs[identifier] = Proxy::Dynflow::ContinuousOutput.new # Create a new ContinuousOutput for next round of updates
19
+ exit_status = @exit_statuses[identifier] || @exit_status if @exit_status
20
+ acc.merge(host_action(identifier) => Runner::Update.new(output, exit_status))
21
+ end
22
+ end
23
+
24
+ def initialize_continuous_outputs
25
+ @outputs = @targets.keys.reduce({}) do |acc, target|
26
+ acc.merge(target => Proxy::Dynflow::ContinuousOutput.new)
27
+ end
28
+ end
29
+
30
+ def host_action(identifier)
31
+ options = @targets[identifier].slice('execution_plan_id', 'run_step_id')
32
+ .merge(:world => Proxy::Dynflow::Core.world)
33
+ Dynflow::Action::Suspended.new OpenStruct.new(options)
34
+ end
35
+
36
+ def broadcast_data(data, type)
37
+ @outputs.each_value { |output| output.add_output(data, type) }
38
+ end
39
+
40
+ def publish_data(_data, _type)
41
+ true
42
+ end
43
+
44
+ def publish_data_for(identifier, data, type)
45
+ @outputs[identifier].add_output(data, type)
46
+ end
47
+
48
+ def dispatch_exception(context, exception)
49
+ @outputs.each_value { |output| output.add_exception(context, exception) }
50
+ end
51
+
52
+ def publish_exit_status_for(identifier, exit_status)
53
+ @exit_statuses[identifier] = exit_status
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ require 'smart_proxy_dynflow/continuous_output'
2
+
3
+ module Proxy::Dynflow
4
+ module Runner
5
+ # Runner::Update represents chunk of data produced by runner that
6
+ # can be consumed by other components, such as RunnerAction
7
+ class Update
8
+ attr_reader :continuous_output, :exit_status
9
+ def initialize(continuous_output, exit_status)
10
+ @continuous_output = continuous_output
11
+ @exit_status = exit_status
12
+ end
13
+
14
+ def self.encode_exception(context, exception, fatal = true)
15
+ continuous_output = ::Proxy::Dynflow::ContinuousOutput.new
16
+ continuous_output.add_exception(context, exception)
17
+ new(continuous_output, fatal ? 'EXCEPTION' : nil)
18
+ end
19
+ end
20
+
21
+ class ExternalEvent
22
+ attr_reader :data
23
+ def initialize(data = {})
24
+ @data = data
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,6 +1,6 @@
1
1
  # require 'ostruct'
2
2
 
3
- class Proxy::Dynflow
3
+ module Proxy::Dynflow
4
4
  class Settings
5
5
  def self.instance
6
6
  Proxy::Dynflow::Plugin.settings
@@ -0,0 +1,53 @@
1
+ module Proxy::Dynflow
2
+ module SettingsLoader
3
+ def self.settings_registry
4
+ @settings_registry ||= {}
5
+ end
6
+
7
+ def self.name_to_settings
8
+ @name_to_settings ||= {}
9
+ end
10
+
11
+ def self.settings_keys
12
+ @settings_keys ||= []
13
+ end
14
+
15
+ def self.settings_registered?(name)
16
+ name_to_settings.key?(name)
17
+ end
18
+
19
+ def self.register_settings(names, object)
20
+ names = [names] unless names.is_a? Array
21
+ names.each do |name|
22
+ raise 'settings name has to be a symbol' unless name.is_a? Symbol
23
+ raise "settings #{name} already registered" if SettingsLoader.settings_registered?(name)
24
+ name_to_settings[name] = object
25
+ end
26
+ settings_registry[names] = object
27
+ end
28
+
29
+ def self.setup_settings(name, settings)
30
+ raise "Settings for #{name} were not registered" unless settings_registered?(name)
31
+ name_to_settings[name].initialize_settings(settings)
32
+ end
33
+
34
+ def register_settings(names, defaults = {})
35
+ SettingsLoader.register_settings(names, self)
36
+ @defaults = defaults
37
+ end
38
+
39
+ def initialize_settings(settings = {})
40
+ @settings = @defaults.merge(settings)
41
+ validate_settings!
42
+ end
43
+
44
+ def settings
45
+ raise "Settings for #{self} not initalized" unless @settings
46
+ @settings
47
+ end
48
+
49
+ def validate_settings!
50
+ raise 'Only symbols expected in keys' unless @settings.keys.all? { |key| key.is_a? Symbol }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ end
4
+ end
5
+
6
+ require 'smart_proxy_dynflow/task_launcher/abstract'
7
+ require 'smart_proxy_dynflow/task_launcher/single'
8
+ require 'smart_proxy_dynflow/task_launcher/batch'
9
+ require 'smart_proxy_dynflow/task_launcher/group'
@@ -0,0 +1,44 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Abstract
4
+ attr_reader :callback, :options, :results, :world
5
+ def initialize(world, callback, options = {})
6
+ @world = world
7
+ @callback = callback
8
+ @options = options
9
+ @results = {}
10
+ end
11
+
12
+ def launch!(_input)
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def self.input_format; end
17
+
18
+ private
19
+
20
+ def format_result(result)
21
+ if result.triggered?
22
+ { :result => 'success', :task_id => result.execution_plan_id }
23
+ else
24
+ plan = world.persistence.load_execution_plan(result.id)
25
+ { :result => 'error', :errors => plan.errors }
26
+ end
27
+ end
28
+
29
+ def action_class(input)
30
+ options[:action_class_override] || ::Dynflow::Utils.constantize(input['action_class'])
31
+ end
32
+
33
+ def with_callback(input)
34
+ input.merge(:callback_host => callback)
35
+ end
36
+
37
+ def trigger(parent, klass, *input)
38
+ world.trigger do
39
+ world.plan_with_options(caller_action: parent, action_class: klass, args: input)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Batch < Abstract
4
+ def launch!(input)
5
+ trigger(nil, Proxy::Dynflow::Action::Batch, self, input)
6
+ end
7
+
8
+ def launch_children(parent, input_hash)
9
+ input_hash.each do |task_id, input|
10
+ launcher = child_launcher(parent)
11
+ launcher.launch!(transform_input(input))
12
+ results[task_id] = launcher.results
13
+ end
14
+ end
15
+
16
+ def prepare_batch(input_hash)
17
+ success_tasks = input_hash.select do |task_id, _input|
18
+ results[task_id][:result] == 'success'
19
+ end
20
+ success_tasks.reduce({}) do |acc, (key, value)|
21
+ acc.merge(results[key][:task_id] => value['action_input']['callback'])
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def child_launcher(parent)
28
+ Single.new(world, callback, :parent => parent)
29
+ end
30
+
31
+ # Identity by default
32
+ def transform_input(input)
33
+ input
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ require 'smart_proxy_dynflow/runner'
2
+
3
+ module Proxy::Dynflow
4
+ module TaskLauncher
5
+ class AbstractGroup < Batch
6
+ def self.runner_class
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def launch!(input)
11
+ trigger(nil, Action::SingleRunnerBatch, self, input)
12
+ end
13
+
14
+ def launch_children(parent, input_hash)
15
+ super(parent, input_hash)
16
+ trigger(parent, Action::BatchRunner, self, input_hash)
17
+ end
18
+
19
+ def operation
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def runner_input(input)
24
+ input.reduce({}) do |acc, (id, input)|
25
+ input = { :execution_plan_id => results[id][:task_id],
26
+ :run_step_id => 2,
27
+ :input => input }
28
+ acc.merge(id => input)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def child_launcher(parent)
35
+ Single.new(world, callback, :parent => parent, :action_class_override => Action::OutputCollector)
36
+ end
37
+
38
+ def transform_input(input)
39
+ wipe_callback(input)
40
+ end
41
+
42
+ def wipe_callback(input)
43
+ callback = input['action_input']['callback']
44
+ input.merge('action_input' => input['action_input'].merge('callback' => nil, :task_id => callback['task_id']))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ module Proxy::Dynflow
2
+ module TaskLauncher
3
+ class Single < Abstract
4
+ def self.input_format
5
+ { :action_class => "MyActionClass", :action_input => {} }
6
+ end
7
+
8
+ def launch!(input)
9
+ triggered = trigger(options[:parent],
10
+ action_class(input),
11
+ with_callback(input.fetch('action_input', {})))
12
+ @results = format_result(triggered)
13
+ triggered
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,4 +1,4 @@
1
- class Proxy::Dynflow
1
+ module Proxy::Dynflow
2
2
  class TaskLauncherRegistry
3
3
  class << self
4
4
  def register(name, launcher)
@@ -4,7 +4,7 @@ unless defined? DYNFLOW_TESTING_LOG_LEVEL
4
4
  DYNFLOW_TESTING_LOG_LEVEL = 4
5
5
  end
6
6
 
7
- class Proxy::Dynflow
7
+ module Proxy::Dynflow
8
8
  # Helper for usage in other dependent plugins that need Dynflow
9
9
  # related things, such as testing instance of world etc.
10
10
  module Testing
@@ -0,0 +1,47 @@
1
+ require 'dynflow'
2
+
3
+ module Proxy::Dynflow
4
+ class Ticker < ::Dynflow::Actor
5
+ attr_reader :clock
6
+
7
+ def initialize(clock, logger, refresh_interval)
8
+ @clock = clock
9
+ @logger = logger
10
+ @events = []
11
+ @refresh_interval = refresh_interval
12
+ plan_next_tick
13
+ end
14
+
15
+ def tick
16
+ @logger.debug("Ticker ticking for #{@events.size} events")
17
+ @events.each do |(target, args)|
18
+ pass_event(target, args)
19
+ end
20
+ @events = []
21
+ ensure
22
+ @planned = false
23
+ plan_next_tick
24
+ end
25
+
26
+ def add_event(target, args)
27
+ @events << [target, args]
28
+ plan_next_tick
29
+ end
30
+
31
+ private
32
+
33
+ def pass_event(target, args)
34
+ target.tell(args)
35
+ rescue => e
36
+ @logger.error("Failed passing event to #{target} with #{args}")
37
+ @logger.error(e)
38
+ end
39
+
40
+ def plan_next_tick
41
+ if !@planned && !@events.empty?
42
+ @clock.ping(reference, Time.now.getlocal + @refresh_interval, :tick)
43
+ @planned = true
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
- class Dynflow
3
- VERSION = '0.4.0'.freeze
2
+ module Dynflow
3
+ VERSION = '0.5.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_dynflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.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: 2021-06-07 00:00:00.000000000 Z
11
+ date: 2021-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logging
@@ -119,18 +119,42 @@ files:
119
119
  - LICENSE
120
120
  - bundler.plugins.d/dynflow.rb
121
121
  - lib/smart_proxy_dynflow.rb
122
+ - lib/smart_proxy_dynflow/action.rb
123
+ - lib/smart_proxy_dynflow/action/batch.rb
124
+ - lib/smart_proxy_dynflow/action/batch_callback.rb
125
+ - lib/smart_proxy_dynflow/action/batch_runner.rb
126
+ - lib/smart_proxy_dynflow/action/output_collector.rb
127
+ - lib/smart_proxy_dynflow/action/runner.rb
128
+ - lib/smart_proxy_dynflow/action/shareable.rb
129
+ - lib/smart_proxy_dynflow/action/single_runner_batch.rb
122
130
  - lib/smart_proxy_dynflow/api.rb
123
131
  - lib/smart_proxy_dynflow/callback.rb
132
+ - lib/smart_proxy_dynflow/continuous_output.rb
124
133
  - lib/smart_proxy_dynflow/core.rb
125
134
  - lib/smart_proxy_dynflow/helpers.rb
126
135
  - lib/smart_proxy_dynflow/http_config.ru
127
136
  - lib/smart_proxy_dynflow/log.rb
128
137
  - lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb
138
+ - lib/smart_proxy_dynflow/otp_manager.rb
129
139
  - lib/smart_proxy_dynflow/plugin.rb
130
140
  - lib/smart_proxy_dynflow/proxy_adapter.rb
141
+ - lib/smart_proxy_dynflow/runner.rb
142
+ - lib/smart_proxy_dynflow/runner/base.rb
143
+ - lib/smart_proxy_dynflow/runner/command.rb
144
+ - lib/smart_proxy_dynflow/runner/command_runner.rb
145
+ - lib/smart_proxy_dynflow/runner/dispatcher.rb
146
+ - lib/smart_proxy_dynflow/runner/parent.rb
147
+ - lib/smart_proxy_dynflow/runner/update.rb
131
148
  - lib/smart_proxy_dynflow/settings.rb
149
+ - lib/smart_proxy_dynflow/settings_loader.rb
150
+ - lib/smart_proxy_dynflow/task_launcher.rb
151
+ - lib/smart_proxy_dynflow/task_launcher/abstract.rb
152
+ - lib/smart_proxy_dynflow/task_launcher/batch.rb
153
+ - lib/smart_proxy_dynflow/task_launcher/group.rb
154
+ - lib/smart_proxy_dynflow/task_launcher/single.rb
132
155
  - lib/smart_proxy_dynflow/task_launcher_registry.rb
133
156
  - lib/smart_proxy_dynflow/testing.rb
157
+ - lib/smart_proxy_dynflow/ticker.rb
134
158
  - lib/smart_proxy_dynflow/version.rb
135
159
  - settings.d/dynflow.yml.example
136
160
  homepage: https://github.com/theforeman/smart_proxy_dynflow