smart_proxy_dynflow_core 0.3.0 → 0.4.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.
@@ -1,92 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # Init script for foreman smart proxy dynflow core service
4
- #
5
- # chkconfig: - 85 15
6
- # description: Init script for foreman proxy dynflow core service
7
-
8
- # Source function library.
9
- . /etc/rc.d/init.d/functions
10
-
11
- prog=smart_proxy_dynflow_core
12
- RETVAL=0
13
- SMART_PROXY_DYNFLOW_CORE_PID=/var/run/foreman-proxy/$prog.pid
14
- SMART_PROXY_DYNFLOW_CORE_USER=${SMART_PROXY_DYNFLOW_CORE_USER:-foreman-proxy}
15
-
16
- start() {
17
- echo -n $"Starting $prog: "
18
- ulimit -n 65536
19
- daemon --user ${SMART_PROXY_DYNFLOW_CORE_USER} /usr/bin/smart_proxy_dynflow_core -d -p $SMART_PROXY_DYNFLOW_CORE_PID > /dev/null
20
- RETVAL=$?
21
- if [ $RETVAL = 0 ]
22
- then
23
- echo_success
24
- else
25
- echo_failure
26
- fi
27
-
28
- echo
29
- return $RETVAL
30
- }
31
-
32
- stop() {
33
- echo -n $"Stopping $prog: "
34
- if [ -f ${SMART_PROXY_DYNFLOW_CORE_PID} ]; then
35
- killproc -p ${SMART_PROXY_DYNFLOW_CORE_PID}
36
- RETVAL=$?
37
- else
38
- echo -n $"$prog was not running.";
39
- failure $"$prog was not running.";
40
- echo
41
- return 1
42
- fi
43
- echo
44
- [ $RETVAL -eq 0 ] && rm -f ${SMART_PROXY_DYNFLOW_CORE_PID}
45
- return $RETVAL
46
- }
47
-
48
- logrotate() {
49
- echo -n $"Rotating logs for $prog: "
50
- if [ -f ${SMART_PROXY_DYNFLOW_CORE_PID} ]; then
51
- killproc -p ${SMART_PROXY_DYNFLOW_CORE_PID} $prog -USR1
52
- RETVAL=$?
53
- echo
54
- else
55
- echo -n $"$prog was not running.";
56
- failure $"$prog was not running.";
57
- echo
58
- return 1
59
- fi
60
- return $RETVAL
61
- }
62
-
63
- # See how we were called.
64
- case "$1" in
65
- start)
66
- start
67
- ;;
68
- stop)
69
- stop
70
- ;;
71
- status)
72
- echo -n "$prog"
73
- status -p $SMART_PROXY_DYNFLOW_CORE_PID
74
- RETVAL=$?
75
- ;;
76
- restart)
77
- stop
78
- start
79
- ;;
80
- condrestart)
81
- stop
82
- [ $? -eq 0 ] && start
83
- ;;
84
- logrotate)
85
- logrotate
86
- ;;
87
- *)
88
- echo $"Usage: $prog {start|stop|restart|condrestart|logrotate}"
89
- exit 1
90
- esac
91
-
92
- exit $RETVAL
@@ -1,13 +0,0 @@
1
- [Unit]
2
- Description=Foreman smart proxy dynflow core service
3
- Documentation=https://github.com/theforeman/smart_proxy_dynflow
4
- After=network.target remote-fs.target nss-lookup.target
5
-
6
- [Service]
7
- Type=notify
8
- User=foreman-proxy
9
- ExecStart=/usr/bin/smart_proxy_dynflow_core --no-daemonize
10
- EnvironmentFile=-/etc/sysconfig/smart_proxy_dynflow_core
11
-
12
- [Install]
13
- WantedBy=multi-user.target
@@ -1,89 +0,0 @@
1
- require 'sinatra/base'
2
- require 'multi_json'
3
-
4
- module SmartProxyDynflowCore
5
- class Api < ::Sinatra::Base
6
- TASK_UPDATE_REGEXP_PATH = %r{/tasks/(\S+)/(update|done)}
7
- helpers Helpers
8
-
9
- configure do
10
- if Settings.instance.standalone
11
- ::Sinatra::Base.set :logging, false
12
- ::Sinatra::Base.use ::SmartProxyDynflowCore::RequestIdMiddleware
13
- ::Sinatra::Base.use ::SmartProxyDynflowCore::LoggerMiddleware
14
- end
15
- end
16
-
17
- before do
18
- if match = request.path_info.match(TASK_UPDATE_REGEXP_PATH)
19
- task_id = match[1]
20
- action = match[2]
21
- authorize_with_token(task_id: task_id, clear: action == 'done')
22
- else
23
- authorize_with_ssl_client
24
- end
25
- content_type :json
26
- end
27
-
28
- post "/tasks/status" do
29
- params = MultiJson.load(request.body.read)
30
- ids = params.fetch('task_ids', [])
31
- result = world.persistence
32
- .find_execution_plans(:filters => { :uuid => ids }).reduce({}) do |acc, plan|
33
- acc.update(plan.id => { 'state' => plan.state, 'result' => plan.result })
34
- end
35
- MultiJson.dump(result)
36
- end
37
-
38
- post "/tasks/launch/?" do
39
- params = MultiJson.load(request.body.read)
40
- launcher = launcher_class(params).new(world, callback_host(params, request), params.fetch('options', {}))
41
- launcher.launch!(params['input'])
42
- launcher.results.to_json
43
- end
44
-
45
- post "/tasks/?" do
46
- params = MultiJson.load(request.body.read)
47
- trigger_task(::Dynflow::Utils.constantize(params['action_name']),
48
- params['action_input'].merge(:callback_host => callback_host(params, request))).to_json
49
- end
50
-
51
- post "/tasks/:task_id/cancel" do |task_id|
52
- cancel_task(task_id).to_json
53
- end
54
-
55
- get "/tasks/:task_id/status" do |task_id|
56
- task_status(task_id).to_json
57
- end
58
-
59
- get "/tasks/count" do
60
- tasks_count(params['state']).to_json
61
- end
62
-
63
- # capturing post "/tasks/:task_id/(update|done)"
64
- post TASK_UPDATE_REGEXP_PATH do |task_id, _action|
65
- data = MultiJson.load(request.body.read)
66
- dispatch_external_event(task_id, data)
67
- end
68
-
69
- get "/tasks/operations" do
70
- TaskLauncherRegistry.operations.to_json
71
- end
72
-
73
- private
74
-
75
- def callback_host(params, request)
76
- params.fetch('action_input', {})['proxy_url'] ||
77
- request.env.values_at('HTTP_X_FORWARDED_FOR', 'HTTP_HOST').compact.first
78
- end
79
-
80
- def launcher_class(params)
81
- operation = params.fetch('operation')
82
- if TaskLauncherRegistry.key?(operation)
83
- TaskLauncherRegistry.fetch(operation)
84
- else
85
- halt 404, MultiJson.dump(:error => "Unknown operation '#{operation}' requested.")
86
- end
87
- end
88
- end
89
- end
@@ -1,30 +0,0 @@
1
- module SmartProxyDynflowCore
2
- class BundlerHelper
3
- def self.require_groups(*groups)
4
- if File.exist?(File.expand_path('../../../Gemfile.in', __FILE__))
5
- # If there is a Gemfile.in file, we will not use Bundler but BundlerExt
6
- # gem which parses this file and loads all dependencies from the system
7
- # rathern then trying to download them from rubygems.org. It always
8
- # loads all gemfile groups.
9
- begin
10
- require 'bundler_ext' unless defined?(BundlerExt)
11
- rescue LoadError
12
- # Debian packaging guidelines state to avoid needing rubygems, so
13
- # we only try to load it if the first require fails (for RPMs)
14
- begin
15
- require 'rubygems' rescue nil
16
- require 'bundler_ext'
17
- rescue LoadError
18
- puts "`bundler_ext` gem is required to run smart_proxy"
19
- exit 1
20
- end
21
- end
22
- BundlerExt.system_require(File.expand_path('../../../Gemfile.in', __FILE__), *groups)
23
- else
24
- require 'bundler' unless defined?(Bundler)
25
- Bundler.require(*groups)
26
- end
27
- end
28
- # rubocop:enable Metrics/PerceivedComplexity
29
- end
30
- end
@@ -1,85 +0,0 @@
1
- require 'rest-client'
2
-
3
- # rubocop:disable Lint/HandleExceptions
4
- begin
5
- require 'smart_proxy_dynflow/callback'
6
- rescue LoadError
7
- end
8
- # rubocop:enable Lint/HandleExceptions
9
-
10
- module SmartProxyDynflowCore
11
- module Callback
12
- class Request
13
- class << self
14
- def send_to_foreman_tasks(callback_info, data)
15
- self.new.callback(prepare_payload(callback_info, data))
16
- end
17
-
18
- def ssl_options
19
- return @ssl_options if defined? @ssl_options
20
- @ssl_options = {}
21
- settings = Settings.instance
22
- return @ssl_options unless URI.parse(settings.foreman_url).scheme == 'https'
23
-
24
- @ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
25
-
26
- private_key_file = settings.foreman_ssl_key || settings.ssl_private_key
27
- if private_key_file
28
- private_key = File.read(private_key_file)
29
- @ssl_options[:ssl_client_key] = OpenSSL::PKey::RSA.new(private_key)
30
- end
31
- certificate_file = settings.foreman_ssl_cert || settings.ssl_certificate
32
- if certificate_file
33
- certificate = File.read(certificate_file)
34
- @ssl_options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(certificate)
35
- end
36
- ca_file = settings.foreman_ssl_ca || settings.ssl_ca_file
37
- @ssl_options[:ssl_ca_file] = ca_file if ca_file
38
- @ssl_options
39
- end
40
- # rubocop:enable Metrics/PerceivedComplexity
41
-
42
- private
43
-
44
- def prepare_payload(callback, data)
45
- { :callback => callback, :data => data }.to_json
46
- end
47
- end
48
-
49
- def callback(payload)
50
- response = callback_resource.post(payload, :content_type => :json)
51
- if response.code.to_s != "200"
52
- raise "Failed performing callback to Foreman server: #{response.code} #{response.body}"
53
- end
54
- response
55
- end
56
-
57
- private
58
-
59
- def callback_resource
60
- @resource ||= RestClient::Resource.new(Settings.instance.foreman_url + '/foreman_tasks/api/tasks/callback',
61
- self.class.ssl_options)
62
- end
63
- end
64
-
65
- class Action < ::Dynflow::Action
66
- def plan(callback, data)
67
- plan_self(:callback => callback, :data => data)
68
- end
69
-
70
- def run
71
- Callback::Request.send_to_foreman_tasks(input[:callback], input[:data])
72
- end
73
- end
74
-
75
- module PlanHelper
76
- def plan_with_callback(input)
77
- input = input.dup
78
- callback = input.delete('callback')
79
-
80
- planned_action = plan_self(input)
81
- plan_action(Callback::Action, callback, planned_action.output) if callback
82
- end
83
- end
84
- end
85
- end
@@ -1,124 +0,0 @@
1
- module SmartProxyDynflowCore
2
- class Core
3
- attr_accessor :world, :accepted_cert_serial
4
-
5
- def initialize
6
- @world = create_world
7
- cert_file = Settings.instance.foreman_ssl_cert || Settings.instance.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
- if Settings.instance.standalone
57
- Log::ProxyAdapter.new(Log.instance, Log.instance.level)
58
- else
59
- Log::ProxyAdapter.new(Proxy::LogBuffer::Decorator.instance, Log.instance.level)
60
- end
61
- end
62
-
63
- def execution_plan_cleaner
64
- proc do |world|
65
- age = Settings.instance.execution_plan_cleaner_age
66
- options = { :poll_interval => age, :max_age => age }
67
- ::Dynflow::Actors::ExecutionPlanCleaner.new(world, options)
68
- end
69
- end
70
-
71
- class << self
72
- attr_reader :instance
73
-
74
- def ensure_initialized
75
- return @instance if @instance
76
- @instance = Core.new
77
- after_initialize_blocks.each { |block| block.call(@instance) }
78
- @instance
79
- end
80
-
81
- def silencer_matchers
82
- @matchers ||= []
83
- end
84
-
85
- def register_silencer_matchers(matchers)
86
- silencer_matchers.concat matchers
87
- end
88
-
89
- def web_console
90
- require 'dynflow/web'
91
- dynflow_console = ::Dynflow::Web.setup do
92
- # we can't use the proxy's after_activation hook, as
93
- # it happens before the Daemon forks the process (including
94
- # closing opened file descriptors)
95
- # TODO: extend smart proxy to enable hooks that happen after
96
- # the forking
97
- helpers Helpers
98
-
99
- before do
100
- authorize_with_ssl_client if Settings.instance.console_auth
101
- end
102
-
103
- Core.ensure_initialized
104
- set :world, Core.world
105
- end
106
- dynflow_console
107
- end
108
-
109
- def world
110
- instance.world
111
- end
112
-
113
- def after_initialize(&block)
114
- after_initialize_blocks << block
115
- end
116
-
117
- private
118
-
119
- def after_initialize_blocks
120
- @after_initialize_blocks ||= []
121
- end
122
- end
123
- end
124
- end
@@ -1,73 +0,0 @@
1
- module SmartProxyDynflowCore
2
- module Helpers
3
- def world
4
- SmartProxyDynflowCore::Core.world
5
- end
6
-
7
- def authorize_with_token(task_id:, clear: true)
8
- if request.env.key? 'HTTP_AUTHORIZATION'
9
- if defined?(::ForemanTasksCore)
10
- auth = request.env['HTTP_AUTHORIZATION']
11
- basic_prefix = /\ABasic /
12
- if !auth.to_s.empty? && auth =~ basic_prefix &&
13
- ForemanTasksCore::OtpManager.authenticate(auth.gsub(basic_prefix, ''),
14
- expected_user: task_id, clear: clear)
15
- Log.instance.debug('authorized with token')
16
- return true
17
- end
18
- end
19
- halt 403, MultiJson.dump(:error => 'Invalid username or password supplied')
20
- end
21
- false
22
- end
23
-
24
- def authorize_with_ssl_client
25
- if %w[yes on 1].include? request.env['HTTPS'].to_s
26
- if request.env['SSL_CLIENT_CERT'].to_s.empty?
27
- Log.instance.error "No client SSL certificate supplied"
28
- halt 403, MultiJson.dump(:error => "No client SSL certificate supplied")
29
- else
30
- client_cert = OpenSSL::X509::Certificate.new(request.env['SSL_CLIENT_CERT'])
31
- unless SmartProxyDynflowCore::Core.instance.accepted_cert_serial == client_cert.serial
32
- Log.instance.error "SSL certificate with unexpected serial supplied"
33
- halt 403, MultiJson.dump(:error => "SSL certificate with unexpected serial supplied")
34
- end
35
- end
36
- else
37
- Log.instance.debug 'require_ssl_client_verification: skipping, non-HTTPS request'
38
- end
39
- end
40
-
41
- def trigger_task(*args)
42
- triggered = world.trigger(*args)
43
- { :task_id => triggered.id }
44
- end
45
-
46
- def cancel_task(task_id)
47
- execution_plan = world.persistence.load_execution_plan(task_id)
48
- cancel_events = execution_plan.cancel
49
- { :task_id => task_id, :canceled_steps_count => cancel_events.size }
50
- end
51
-
52
- def task_status(task_id)
53
- ep = world.persistence.load_execution_plan(task_id)
54
- ep.to_hash.merge(:actions => ep.actions.map(&:to_hash))
55
- rescue KeyError => _e
56
- status 404
57
- {}
58
- end
59
-
60
- def tasks_count(state)
61
- state ||= 'all'
62
- filter = state != 'all' ? { :filters => { :state => [state] } } : {}
63
- tasks = world.persistence.find_execution_plans(filter)
64
- { :count => tasks.count, :state => state }
65
- end
66
-
67
- def dispatch_external_event(task_id, params)
68
- world.event(task_id,
69
- params['step_id'].to_i,
70
- ::ForemanTasksCore::Runner::ExternalEvent.new(params))
71
- end
72
- end
73
- end