smart_proxy_dynflow 0.2.3 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -25
  3. data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
  4. data/lib/smart_proxy_dynflow.rb +16 -1
  5. data/lib/smart_proxy_dynflow/action.rb +12 -0
  6. data/lib/smart_proxy_dynflow/action/batch.rb +21 -0
  7. data/lib/smart_proxy_dynflow/action/batch_callback.rb +20 -0
  8. data/lib/smart_proxy_dynflow/action/batch_runner.rb +14 -0
  9. data/lib/smart_proxy_dynflow/action/output_collector.rb +8 -0
  10. data/lib/smart_proxy_dynflow/action/runner.rb +76 -0
  11. data/lib/smart_proxy_dynflow/action/shareable.rb +25 -0
  12. data/lib/smart_proxy_dynflow/action/single_runner_batch.rb +39 -0
  13. data/lib/smart_proxy_dynflow/api.rb +63 -40
  14. data/lib/smart_proxy_dynflow/callback.rb +69 -25
  15. data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
  16. data/lib/smart_proxy_dynflow/core.rb +121 -0
  17. data/lib/smart_proxy_dynflow/helpers.rb +52 -6
  18. data/lib/smart_proxy_dynflow/http_config.ru +6 -16
  19. data/lib/smart_proxy_dynflow/log.rb +52 -0
  20. data/lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb +59 -0
  21. data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
  22. data/lib/smart_proxy_dynflow/plugin.rb +9 -14
  23. data/lib/smart_proxy_dynflow/proxy_adapter.rb +1 -1
  24. data/lib/smart_proxy_dynflow/runner.rb +10 -0
  25. data/lib/smart_proxy_dynflow/runner/base.rb +98 -0
  26. data/lib/smart_proxy_dynflow/runner/command.rb +40 -0
  27. data/lib/smart_proxy_dynflow/runner/command_runner.rb +11 -0
  28. data/lib/smart_proxy_dynflow/runner/dispatcher.rb +191 -0
  29. data/lib/smart_proxy_dynflow/runner/parent.rb +57 -0
  30. data/lib/smart_proxy_dynflow/runner/update.rb +28 -0
  31. data/lib/smart_proxy_dynflow/settings.rb +9 -0
  32. data/lib/smart_proxy_dynflow/settings_loader.rb +53 -0
  33. data/lib/smart_proxy_dynflow/task_launcher.rb +9 -0
  34. data/lib/smart_proxy_dynflow/task_launcher/abstract.rb +44 -0
  35. data/lib/smart_proxy_dynflow/task_launcher/batch.rb +37 -0
  36. data/lib/smart_proxy_dynflow/task_launcher/group.rb +48 -0
  37. data/lib/smart_proxy_dynflow/task_launcher/single.rb +17 -0
  38. data/lib/smart_proxy_dynflow/task_launcher_registry.rb +31 -0
  39. data/lib/smart_proxy_dynflow/testing.rb +24 -0
  40. data/lib/smart_proxy_dynflow/ticker.rb +47 -0
  41. data/lib/smart_proxy_dynflow/version.rb +2 -2
  42. data/settings.d/dynflow.yml.example +6 -5
  43. metadata +83 -12
@@ -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
@@ -0,0 +1,121 @@
1
+ module Proxy::Dynflow
2
+ class Core
3
+ attr_accessor :world, :accepted_cert_serial
4
+
5
+ def initialize
6
+ @world = create_world
7
+ cert_file = Proxy::SETTINGS.foreman_ssl_cert || Proxy::SETTINGS.ssl_certificate
8
+ if cert_file
9
+ client_cert = File.read(cert_file)
10
+ # we trust only requests using the same certificate as we are
11
+ # (in other words the local proxy only)
12
+ @accepted_cert_serial = OpenSSL::X509::Certificate.new(client_cert).serial
13
+ end
14
+ end
15
+
16
+ def create_world(&block)
17
+ config = default_world_config(&block)
18
+ world = ::Dynflow::World.new(config)
19
+ world.middleware.use ::Actions::Middleware::KeepCurrentRequestID
20
+ world
21
+ end
22
+
23
+ def persistence_conn_string
24
+ return ENV['DYNFLOW_DB_CONN_STRING'] if ENV.key? 'DYNFLOW_DB_CONN_STRING'
25
+ db_conn_string = 'sqlite:/'
26
+
27
+ db_file = Settings.instance.database
28
+ if db_file.nil? || db_file.empty?
29
+ Log.instance.warn "Could not open DB for dynflow at '#{db_file}', " \
30
+ "will keep data in memory. Restart will drop all dynflow data."
31
+ else
32
+ db_conn_string += "/#{db_file}"
33
+ end
34
+
35
+ db_conn_string
36
+ end
37
+
38
+ def persistence_adapter
39
+ ::Dynflow::PersistenceAdapters::Sequel.new persistence_conn_string
40
+ end
41
+
42
+ def default_world_config
43
+ ::Dynflow::Config.new.tap do |config|
44
+ config.auto_rescue = true
45
+ config.logger_adapter = logger_adapter
46
+ config.persistence_adapter = persistence_adapter
47
+ config.execution_plan_cleaner = execution_plan_cleaner
48
+ # TODO: There has to be a better way
49
+ matchers = config.silent_dead_letter_matchers.call.concat(self.class.silencer_matchers)
50
+ config.silent_dead_letter_matchers = matchers
51
+ yield config if block_given?
52
+ end
53
+ end
54
+
55
+ def logger_adapter
56
+ Log::ProxyAdapter.new(Proxy::LogBuffer::Decorator.instance, Log.instance.level)
57
+ end
58
+
59
+ def execution_plan_cleaner
60
+ proc do |world|
61
+ age = Settings.instance.execution_plan_cleaner_age
62
+ options = { :poll_interval => age, :max_age => age }
63
+ ::Dynflow::Actors::ExecutionPlanCleaner.new(world, options)
64
+ end
65
+ end
66
+
67
+ class << self
68
+ attr_reader :instance
69
+
70
+ def ensure_initialized
71
+ return @instance if @instance
72
+ @instance = Core.new
73
+ after_initialize_blocks.each { |block| block.call(@instance) }
74
+ @instance
75
+ end
76
+
77
+ def silencer_matchers
78
+ @matchers ||= [::Dynflow::DeadLetterSilencer::Matcher.new(Ticker)]
79
+ end
80
+
81
+ def register_silencer_matchers(matchers)
82
+ silencer_matchers.concat matchers
83
+ end
84
+
85
+ def web_console
86
+ require 'dynflow/web'
87
+ dynflow_console = ::Dynflow::Web.setup do
88
+ # we can't use the proxy's after_activation hook, as
89
+ # it happens before the Daemon forks the process (including
90
+ # closing opened file descriptors)
91
+ # TODO: extend smart proxy to enable hooks that happen after
92
+ # the forking
93
+ helpers Helpers
94
+ include ::Sinatra::Authorization::Helpers
95
+
96
+ before do
97
+ do_authorize_with_ssl_client if Settings.instance.console_auth
98
+ end
99
+
100
+ Core.ensure_initialized
101
+ set :world, Core.world
102
+ end
103
+ dynflow_console
104
+ end
105
+
106
+ def world
107
+ instance.world
108
+ end
109
+
110
+ def after_initialize(&block)
111
+ after_initialize_blocks << block
112
+ end
113
+
114
+ private
115
+
116
+ def after_initialize_blocks
117
+ @after_initialize_blocks ||= []
118
+ end
119
+ end
120
+ end
121
+ end
@@ -1,11 +1,57 @@
1
1
  module Proxy
2
- class Dynflow
2
+ module Dynflow
3
3
  module Helpers
4
- def relay_request(from = %r{^/dynflow}, to = '')
5
- response = Proxy::Dynflow::Callback::Core.relay(request, from, to)
6
- content_type response.content_type
7
- status response.code
8
- body response.body
4
+ def world
5
+ Proxy::Dynflow::Core.world
6
+ end
7
+
8
+ def authorize_with_token(task_id:, clear: true)
9
+ if request.env.key? 'HTTP_AUTHORIZATION'
10
+ if defined?(::Proxy::Dynflow)
11
+ auth = request.env['HTTP_AUTHORIZATION']
12
+ basic_prefix = /\ABasic /
13
+ if !auth.to_s.empty? && auth =~ basic_prefix &&
14
+ Proxy::Dynflow::OtpManager.authenticate(auth.gsub(basic_prefix, ''),
15
+ expected_user: task_id, clear: clear)
16
+ Log.instance.debug('authorized with token')
17
+ return true
18
+ end
19
+ end
20
+ halt 403, MultiJson.dump(:error => 'Invalid username or password supplied')
21
+ end
22
+ false
23
+ end
24
+
25
+ def trigger_task(*args)
26
+ triggered = world.trigger(*args)
27
+ { :task_id => triggered.id }
28
+ end
29
+
30
+ def cancel_task(task_id)
31
+ execution_plan = world.persistence.load_execution_plan(task_id)
32
+ cancel_events = execution_plan.cancel
33
+ { :task_id => task_id, :canceled_steps_count => cancel_events.size }
34
+ end
35
+
36
+ def task_status(task_id)
37
+ ep = world.persistence.load_execution_plan(task_id)
38
+ ep.to_hash.merge(:actions => ep.actions.map(&:to_hash))
39
+ rescue KeyError => _e
40
+ status 404
41
+ {}
42
+ end
43
+
44
+ def tasks_count(state)
45
+ state ||= 'all'
46
+ filter = state != 'all' ? { :filters => { :state => [state] } } : {}
47
+ tasks = world.persistence.find_execution_plans(filter)
48
+ { :count => tasks.count, :state => state }
49
+ end
50
+
51
+ def dispatch_external_event(task_id, params)
52
+ world.event(task_id,
53
+ params['step_id'].to_i,
54
+ ::Proxy::Dynflow::Runner::ExternalEvent.new(params))
9
55
  end
10
56
  end
11
57
  end
@@ -1,21 +1,11 @@
1
- # Internal core will be used if external core is either disabled or unset
2
- # and the core gem can be loaded
1
+ require 'smart_proxy_dynflow/api'
3
2
 
4
- if !::Proxy::Dynflow::Plugin.settings.external_core && Proxy::Dynflow::Plugin.internal_core_available?
5
- require 'smart_proxy_dynflow_core/api'
6
- require 'smart_proxy_dynflow_core/launcher'
7
-
8
- SmartProxyDynflowCore::Settings.load_from_proxy(p)
9
-
10
- map "/dynflow" do
11
- SmartProxyDynflowCore::Launcher.route_mapping(self)
3
+ map "/dynflow" do
4
+ map '/console' do
5
+ run Proxy::Dynflow::Core.web_console
12
6
  end
13
- else
14
- require 'smart_proxy_dynflow/api'
15
7
 
16
- map "/dynflow" do
17
- map '/' do
18
- run Proxy::Dynflow::Api
19
- end
8
+ map '/' do
9
+ run Proxy::Dynflow::Api
20
10
  end
21
11
  end
@@ -0,0 +1,52 @@
1
+ require 'logging'
2
+
3
+ module Proxy::Dynflow
4
+ class Log
5
+ LOGGER_NAME = 'dynflow-core'.freeze
6
+
7
+ begin
8
+ require 'syslog/logger'
9
+ @syslog_available = true
10
+ rescue LoadError
11
+ @syslog_available = false
12
+ end
13
+
14
+ class << self
15
+ def reload!
16
+ Logging.logger[LOGGER_NAME].appenders.each(&:close)
17
+ Logging.logger[LOGGER_NAME].clear_appenders
18
+ @logger = nil
19
+ instance
20
+ end
21
+
22
+ def instance
23
+ ::Proxy::LogBuffer::Decorator.instance
24
+ end
25
+ end
26
+
27
+ class ProxyStructuredFormater < ::Dynflow::LoggerAdapters::Formatters::Abstract
28
+ def format(message)
29
+ if message.is_a?(Exception)
30
+ subject = "#{message.message} (#{message.class})"
31
+ if @base.respond_to?(:exception)
32
+ @base.exception("Error details", message)
33
+ subject
34
+ else
35
+ "#{subject}\n#{message.backtrace.join("\n")}"
36
+ end
37
+ else
38
+ @original_formatter.call(severity, datetime, prog_name, message)
39
+ end
40
+ end
41
+ end
42
+
43
+ class ProxyAdapter < ::Dynflow::LoggerAdapters::Simple
44
+ def initialize(logger, level = Logger::DEBUG, _formatters = [])
45
+ @logger = logger
46
+ @logger.level = level
47
+ @action_logger = apply_formatters(ProgNameWrapper.new(@logger, ' action'), [])
48
+ @dynflow_logger = apply_formatters(ProgNameWrapper.new(@logger, 'dynflow'), [])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,59 @@
1
+ module Actions
2
+ module Middleware
3
+ class KeepCurrentRequestID < Dynflow::Middleware
4
+ def delay(*args)
5
+ pass(*args).tap { store_current_request_id }
6
+ end
7
+
8
+ def plan(*args)
9
+ with_current_request_id do
10
+ pass(*args).tap { store_current_request_id }
11
+ end
12
+ end
13
+
14
+ def run(*args)
15
+ restore_current_request_id { pass(*args) }
16
+ end
17
+
18
+ def finalize
19
+ restore_current_request_id { pass }
20
+ end
21
+
22
+ # Run all execution plan lifecycle hooks as the original request_id
23
+ def hook(*args)
24
+ restore_current_request_id { pass(*args) }
25
+ end
26
+
27
+ private
28
+
29
+ def with_current_request_id
30
+ if action.input[:current_request_id].nil?
31
+ yield
32
+ else
33
+ restore_current_request_id { yield }
34
+ end
35
+ end
36
+
37
+ def store_current_request_id
38
+ action.input[:current_request_id] = ::Logging.mdc['request']
39
+ end
40
+
41
+ def restore_current_request_id
42
+ unless (restored_id = action.input[:current_request_id]).nil?
43
+ old_id = ::Logging.mdc['request']
44
+ if !old_id.nil? && old_id != restored_id
45
+ action.action_logger.warn('Changing request id %{request_id} to saved id %{saved_id}' % { :saved_id => restored_id, :request_id => old_id })
46
+ end
47
+ ::Logging.mdc['request'] = restored_id
48
+ end
49
+ yield
50
+ ensure
51
+ # Reset to original request id only when not nil
52
+ # Otherwise, keep the id until it's cleaned in Dynflow's run_user_code block
53
+ # so that it will stay valid for the rest of the processing of the current step
54
+ # (even outside of the middleware lifecycle)
55
+ ::Logging.mdc['request'] = old_id unless old_id.nil?
56
+ end
57
+ end
58
+ end
59
+ 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,30 +2,25 @@ 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
9
9
  https_rackup_path rackup_path
10
10
 
11
11
  settings_file "dynflow.yml"
12
- requires :foreman_proxy, ">= 1.12.0"
13
- default_settings :core_url => 'http://localhost:8008'
12
+ requires :foreman_proxy, ">= 1.16.0"
13
+ default_settings :console_auth => true,
14
+ :execution_plan_cleaner_age => 60 * 60 * 24
14
15
  plugin :dynflow, Proxy::Dynflow::VERSION
15
16
 
16
17
  after_activation do
17
- # Ensure the core gem is loaded, if configure NOT to use the external core
18
- if Proxy::Dynflow::Plugin.settings.external_core == false && !internal_core_available?
19
- raise "'smart_proxy_dynflow_core' gem is required, but not available"
20
- end
21
- end
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
22
 
23
- def self.internal_core_available?
24
- @core_available ||= begin
25
- require 'smart_proxy_dynflow_core'
26
- true
27
- rescue LoadError # rubocop:disable Lint/HandleExceptions
28
- end
23
+ Proxy::Dynflow::Core.ensure_initialized
29
24
  end
30
25
  end
31
26
  end