smart_proxy_dynflow 0.2.3 → 0.5.1
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/Gemfile +6 -25
- data/{bundler.plugins.d → bundler.d}/dynflow.rb +0 -0
- data/lib/smart_proxy_dynflow.rb +16 -1
- 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 +63 -40
- data/lib/smart_proxy_dynflow/callback.rb +69 -25
- data/lib/smart_proxy_dynflow/continuous_output.rb +50 -0
- data/lib/smart_proxy_dynflow/core.rb +121 -0
- data/lib/smart_proxy_dynflow/helpers.rb +52 -6
- data/lib/smart_proxy_dynflow/http_config.ru +6 -16
- data/lib/smart_proxy_dynflow/log.rb +52 -0
- data/lib/smart_proxy_dynflow/middleware/keep_current_request_id.rb +59 -0
- data/lib/smart_proxy_dynflow/otp_manager.rb +36 -0
- data/lib/smart_proxy_dynflow/plugin.rb +9 -14
- 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 +9 -0
- 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 +31 -0
- data/lib/smart_proxy_dynflow/testing.rb +24 -0
- data/lib/smart_proxy_dynflow/ticker.rb +47 -0
- data/lib/smart_proxy_dynflow/version.rb +2 -2
- data/settings.d/dynflow.yml.example +6 -5
- 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
|
-
|
2
|
+
module Dynflow
|
3
3
|
module Helpers
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
2
|
-
# and the core gem can be loaded
|
1
|
+
require 'smart_proxy_dynflow/api'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
17
|
-
|
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
|
-
|
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.
|
13
|
-
default_settings :
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|