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.
- checksums.yaml +4 -4
- data/lib/smart_proxy_dynflow.rb +2 -7
- data/lib/smart_proxy_dynflow/action.rb +12 -0
- data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
- data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
- data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
- data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
- data/lib/smart_proxy_dynflow/action/runner.rb +76 -0
- data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
- data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
- data/lib/smart_proxy_dynflow/api.rb +1 -1
- data/lib/smart_proxy_dynflow/callback.rb +1 -1
- data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
- data/lib/smart_proxy_dynflow/core.rb +2 -2
- data/lib/smart_proxy_dynflow/helpers.rb +5 -5
- data/lib/smart_proxy_dynflow/log.rb +1 -1
- data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
- data/lib/smart_proxy_dynflow/plugin.rb +6 -1
- data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
- data/lib/smart_proxy_dynflow/runner.rb +10 -0
- data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
- data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
- data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
- data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
- data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
- data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
- data/lib/smart_proxy_dynflow/settings.rb +1 -1
- data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
- data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
- data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
- data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
- data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
- data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
- data/lib/smart_proxy_dynflow/task_launcher_registry.rb +1 -1
- data/lib/smart_proxy_dynflow/testing.rb +1 -1
- data/lib/smart_proxy_dynflow/ticker.rb +47 -0
- data/lib/smart_proxy_dynflow/version.rb +2 -2
- metadata +26 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: be2013a41681b30175177e9bcdb2bdc24186e6b6edccf3dcbdadcb5ca006f49b
         | 
| 4 | 
            +
              data.tar.gz: 7c54a27ae6b0055cdd5b1189a8c89a1c644626fbe0386a9e48e3a48da3ebf71e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 6fb8325c06bdac95fbc2b8b3e8b59cb9cf75f43ba71a944dc1cf423a36ea7cfe7a02be1b928f6f2f21728c7de90011cc5c24867dcce58ba90e66bda6396922cc
         | 
| 7 | 
            +
              data.tar.gz: 913260f459727549e5559938558f09f398dce1d78e0c7cd42ad596e9da58394d92b2f9ba9dc5dd421371cffa71ae9f4c2038db15d0ce1e6ca4290627910a61ef
         | 
    
        data/lib/smart_proxy_dynflow.rb
    CHANGED
    
    | @@ -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 | 
            -
               | 
| 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,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
         | 
| @@ -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 | 
            -
             | 
| 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 | 
            -
               | 
| 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?(:: | 
| 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 | 
            -
                            | 
| 15 | 
            -
             | 
| 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 | 
            -
                                :: | 
| 54 | 
            +
                                ::Proxy::Dynflow::Runner::ExternalEvent.new(params))
         | 
| 55 55 | 
             
                  end
         | 
| 56 56 | 
             
                end
         | 
| 57 57 | 
             
              end
         | 
| @@ -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 | 
            -
             | 
| 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
         | 
| @@ -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,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
         | 
| @@ -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
         | 
| @@ -4,7 +4,7 @@ unless defined? DYNFLOW_TESTING_LOG_LEVEL | |
| 4 4 | 
             
              DYNFLOW_TESTING_LOG_LEVEL = 4
         | 
| 5 5 | 
             
            end
         | 
| 6 6 |  | 
| 7 | 
            -
             | 
| 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
         | 
    
        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 | 
            +
              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- | 
| 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
         |