smart_proxy_dynflow 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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