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.
- 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
|