smart_proxy_dynflow_core 0.0.7
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 +7 -0
- data/Gemfile +14 -0
- data/LICENSE +674 -0
- data/bin/smart_proxy_dynflow_core +24 -0
- data/bundler.d/dynflow.rb +1 -0
- data/config/settings.yml.example +18 -0
- data/lib/smart_proxy_dynflow_core.rb +5 -0
- data/lib/smart_proxy_dynflow_core/api.rb +31 -0
- data/lib/smart_proxy_dynflow_core/bundler_helper.rb +29 -0
- data/lib/smart_proxy_dynflow_core/callback.rb +69 -0
- data/lib/smart_proxy_dynflow_core/core.rb +92 -0
- data/lib/smart_proxy_dynflow_core/helpers.rb +48 -0
- data/lib/smart_proxy_dynflow_core/launcher.rb +123 -0
- data/lib/smart_proxy_dynflow_core/settings.rb +74 -0
- data/lib/smart_proxy_dynflow_core/testing.rb +27 -0
- data/lib/smart_proxy_dynflow_core/version.rb +3 -0
- metadata +242 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'dynflow'
|
3
|
+
require 'rack'
|
4
|
+
require 'smart_proxy_dynflow_core/launcher'
|
5
|
+
require 'yaml'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.on('-c', '--config-dir CONFIG_DIR', String, 'Directory to load settings from') do |value|
|
11
|
+
options[:config_dir] = value
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on('-1', '--one-config', String, 'Do not load more than 1 config') do |value|
|
15
|
+
options[:one_config] = true
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on_tail('-h', '--help', 'Show usage help') do
|
19
|
+
puts opts
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
end.parse!
|
23
|
+
|
24
|
+
SmartProxyDynflowCore::Launcher.launch! options
|
@@ -0,0 +1 @@
|
|
1
|
+
gem 'smart_proxy_dynflow'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
# Path to dynflow database, leave blank for in-memory non-persistent database
|
3
|
+
:database:
|
4
|
+
|
5
|
+
# URL of the smart proxy, used for reporting back
|
6
|
+
:callback_url: 'https://localhost:8443'
|
7
|
+
|
8
|
+
# Listen on address
|
9
|
+
:listen: 127.0.0.1
|
10
|
+
|
11
|
+
# Listen on port
|
12
|
+
:port: 8008
|
13
|
+
|
14
|
+
# SSL settings for client authentication against smart proxy.
|
15
|
+
# :use_https: false
|
16
|
+
# :ssl_ca_file: ssl/ca.pem
|
17
|
+
# :ssl_private_key: ssl/localhost.pem
|
18
|
+
# :ssl_certificate: ssl/certs/localhost.pem
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'dynflow'
|
4
|
+
|
5
|
+
module SmartProxyDynflowCore
|
6
|
+
class Api < ::Sinatra::Base
|
7
|
+
helpers Helpers
|
8
|
+
|
9
|
+
before do
|
10
|
+
authorize_with_ssl_client
|
11
|
+
content_type :json
|
12
|
+
end
|
13
|
+
|
14
|
+
post "/tasks/?" do
|
15
|
+
params = MultiJson.load(request.body.read)
|
16
|
+
trigger_task(::Dynflow::Utils.constantize(params['action_name']), params['action_input']).to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
post "/tasks/:task_id/cancel" do |task_id|
|
20
|
+
cancel_task(task_id).to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
get "/tasks/:task_id/status" do |task_id|
|
24
|
+
task_status(task_id).to_json
|
25
|
+
end
|
26
|
+
|
27
|
+
get "/tasks/count" do
|
28
|
+
tasks_count(params['state']).to_json
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'dynflow'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'smart_proxy_dynflow/callback'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
module SmartProxyDynflowCore
|
10
|
+
module Callback
|
11
|
+
class Request
|
12
|
+
def callback(payload)
|
13
|
+
response = callback_resource.post payload
|
14
|
+
if response.code != 200
|
15
|
+
raise "Failed performing callback to smart proxy: #{response.code} #{response.body}"
|
16
|
+
end
|
17
|
+
response
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.callback(callback, data)
|
21
|
+
self.new.callback(self.prepare_payload(callback, data))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def self.prepare_payload(callback, data)
|
27
|
+
{ :callback => callback, :data => data }.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
def callback_resource
|
31
|
+
@resource ||= RestClient::Resource.new Settings.instance.callback_url + '/dynflow/tasks/callback',
|
32
|
+
ssl_options
|
33
|
+
end
|
34
|
+
|
35
|
+
def ssl_options
|
36
|
+
return {} unless Settings.instance.use_https
|
37
|
+
client_key = File.read Settings.instance.ssl_private_key
|
38
|
+
client_cert = File.read Settings.instance.ssl_certificate
|
39
|
+
{
|
40
|
+
:ssl_client_cert => OpenSSL::X509::Certificate.new(client_cert),
|
41
|
+
:ssl_client_key => OpenSSL::PKey::RSA.new(client_key),
|
42
|
+
:ssl_ca_file => Settings.instance.ssl_ca_file,
|
43
|
+
:verify_ssl => OpenSSL::SSL::VERIFY_PEER
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Action < ::Dynflow::Action
|
49
|
+
def plan(callback, data)
|
50
|
+
plan_self(:callback => callback, :data => data)
|
51
|
+
end
|
52
|
+
|
53
|
+
def run
|
54
|
+
callback = (Settings.instance.standalone ? Callback::Request : Proxy::Dynflow::Callback::Request).new
|
55
|
+
callback.callback(SmartProxyDynflowCore::Callback::Request.prepare_payload(input[:callback], input[:data]))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module PlanHelper
|
60
|
+
def plan_with_callback(input)
|
61
|
+
input = input.dup
|
62
|
+
callback = input.delete('callback')
|
63
|
+
|
64
|
+
planned_action = plan_self(input)
|
65
|
+
plan_action(Callback::Action, callback, planned_action.output) if callback
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module SmartProxyDynflowCore
|
2
|
+
class Core
|
3
|
+
|
4
|
+
attr_accessor :world
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@world = create_world
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_world(&block)
|
11
|
+
config = default_world_config(&block)
|
12
|
+
::Dynflow::World.new(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
def persistence_conn_string
|
16
|
+
return ENV['DYNFLOW_DB_CONN_STRING'] if ENV.key? 'DYNFLOW_DB_CONN_STRING'
|
17
|
+
db_conn_string = 'sqlite:/'
|
18
|
+
|
19
|
+
db_file = Settings.instance.database
|
20
|
+
if db_file.nil? || db_file.empty?
|
21
|
+
# TODO: Use some kind of logger
|
22
|
+
STDERR.puts "Could not open DB for dynflow at '#{db_file}', will keep data in memory. Restart will drop all dynflow data."
|
23
|
+
else
|
24
|
+
db_conn_string += "/#{db_file}"
|
25
|
+
end
|
26
|
+
|
27
|
+
db_conn_string
|
28
|
+
end
|
29
|
+
|
30
|
+
def persistence_adapter
|
31
|
+
::Dynflow::PersistenceAdapters::Sequel.new persistence_conn_string
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_world_config
|
35
|
+
::Dynflow::Config.new.tap do |config|
|
36
|
+
config.auto_rescue = true
|
37
|
+
config.logger_adapter = logger_adapter
|
38
|
+
config.persistence_adapter = persistence_adapter
|
39
|
+
yield config if block_given?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger_adapter
|
44
|
+
::Dynflow::LoggerAdapters::Simple.new $stderr, 0
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_reader :instance
|
49
|
+
|
50
|
+
def ensure_initialized
|
51
|
+
return @instance if @instance
|
52
|
+
@instance = Core.new
|
53
|
+
after_initialize_blocks.each(&:call)
|
54
|
+
@instance
|
55
|
+
end
|
56
|
+
|
57
|
+
def web_console
|
58
|
+
require 'dynflow/web'
|
59
|
+
dynflow_console = ::Dynflow::Web.setup do
|
60
|
+
# we can't use the proxy's after_activation hook, as
|
61
|
+
# it happens before the Daemon forks the process (including
|
62
|
+
# closing opened file descriptors)
|
63
|
+
# TODO: extend smart proxy to enable hooks that happen after
|
64
|
+
# the forking
|
65
|
+
helpers Helpers
|
66
|
+
|
67
|
+
before do
|
68
|
+
authorize_with_ssl_client
|
69
|
+
end
|
70
|
+
|
71
|
+
Core.ensure_initialized
|
72
|
+
set :world, Core.world
|
73
|
+
end
|
74
|
+
dynflow_console
|
75
|
+
end
|
76
|
+
|
77
|
+
def world
|
78
|
+
instance.world
|
79
|
+
end
|
80
|
+
|
81
|
+
def after_initialize(&block)
|
82
|
+
after_initialize_blocks << block
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def after_initialize_blocks
|
88
|
+
@after_initialize_blocks ||= []
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module SmartProxyDynflowCore
|
2
|
+
module Helpers
|
3
|
+
def world
|
4
|
+
SmartProxyDynflowCore::Core.world
|
5
|
+
end
|
6
|
+
|
7
|
+
def authorize_with_ssl_client
|
8
|
+
if %w(yes on 1).include? request.env['HTTPS'].to_s
|
9
|
+
if request.env['SSL_CLIENT_CERT'].to_s.empty?
|
10
|
+
status 403
|
11
|
+
# TODO: Use a logger
|
12
|
+
STDERR.puts "No client SSL certificate supplied"
|
13
|
+
halt MultiJson.dump(:error => "No client SSL certificate supplied")
|
14
|
+
end
|
15
|
+
else
|
16
|
+
# TODO: Use a logger
|
17
|
+
# logger.debug('require_ssl_client_verification: skipping, non-HTTPS request')
|
18
|
+
puts 'require_ssl_client_verification: skipping, non-HTTPS request'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def trigger_task(*args)
|
23
|
+
triggered = world.trigger(*args)
|
24
|
+
{ :task_id => triggered.id }
|
25
|
+
end
|
26
|
+
|
27
|
+
def cancel_task(task_id)
|
28
|
+
execution_plan = world.persistence.load_execution_plan(task_id)
|
29
|
+
cancel_events = execution_plan.cancel
|
30
|
+
{ :task_id => task_id, :canceled_steps_count => cancel_events.size }
|
31
|
+
end
|
32
|
+
|
33
|
+
def task_status(task_id)
|
34
|
+
ep = world.persistence.load_execution_plan(task_id)
|
35
|
+
ep.to_hash.merge(:actions => ep.actions.map(&:to_hash))
|
36
|
+
rescue KeyError => _e
|
37
|
+
status 404
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
def tasks_count(state)
|
42
|
+
state ||= 'all'
|
43
|
+
filter = state != 'all' ? { :filters => { :state => [state] } } : {}
|
44
|
+
tasks = world.persistence.find_execution_plans(filter)
|
45
|
+
{ :count => tasks.count, :state => state }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'webrick/https'
|
2
|
+
require 'smart_proxy_dynflow_core/bundler_helper'
|
3
|
+
require 'smart_proxy_dynflow_core/settings'
|
4
|
+
module SmartProxyDynflowCore
|
5
|
+
class Launcher
|
6
|
+
|
7
|
+
def self.launch!(options)
|
8
|
+
self.new.start options
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(options)
|
12
|
+
load_settings!(options[:config_dir], options[:one_config])
|
13
|
+
Settings.instance.standalone = true
|
14
|
+
Core.ensure_initialized
|
15
|
+
Rack::Server.new(rack_settings).start
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_settings!(config_dir = nil, one_config = false)
|
19
|
+
possible_config_dirs = [
|
20
|
+
'/etc/smart_proxy_dynflow_core',
|
21
|
+
File.expand_path('~/.config/smart_proxy_dynflow_core'),
|
22
|
+
File.join(File.dirname(__FILE__), '..', '..', 'config'),
|
23
|
+
]
|
24
|
+
possible_config_dirs << config_dir if config_dir
|
25
|
+
BundlerHelper.require_groups(:default)
|
26
|
+
possible_config_dirs.reverse! if one_config
|
27
|
+
possible_config_dirs.select { |config_dir| File.directory? config_dir }.each do |config_dir|
|
28
|
+
break if load_config_dir(config_dir) && one_config
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.route_mapping(rack_builder)
|
33
|
+
rack_builder.map '/console' do
|
34
|
+
run Core.web_console
|
35
|
+
end
|
36
|
+
|
37
|
+
rack_builder.map '/' do
|
38
|
+
run Api
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def rack_settings
|
45
|
+
settings = if https_enabled?
|
46
|
+
# TODO: Use a logger
|
47
|
+
puts "Using HTTPS"
|
48
|
+
https_app
|
49
|
+
else
|
50
|
+
# TODO: Use a logger
|
51
|
+
puts "Using HTTP"
|
52
|
+
{}
|
53
|
+
end
|
54
|
+
settings.merge(base_settings)
|
55
|
+
end
|
56
|
+
|
57
|
+
def app
|
58
|
+
Rack::Builder.new do
|
59
|
+
SmartProxyDynflowCore::Launcher.route_mapping(self)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def base_settings
|
64
|
+
{
|
65
|
+
:app => app,
|
66
|
+
:Host => Settings.instance.listen,
|
67
|
+
:Port => Settings.instance.port,
|
68
|
+
:daemonize => false
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def https_app
|
73
|
+
ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
|
74
|
+
ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE)
|
75
|
+
# This is required to disable SSLv3 on Ruby 1.8.7
|
76
|
+
ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
|
77
|
+
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
|
78
|
+
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
|
79
|
+
|
80
|
+
{
|
81
|
+
:SSLEnable => true,
|
82
|
+
:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
|
83
|
+
:SSLPrivateKey => ssl_private_key,
|
84
|
+
:SSLCertificate => ssl_certificate,
|
85
|
+
:SSLCACertificateFile => Settings.instance.ssl_ca_file,
|
86
|
+
:SSLOptions => ssl_options
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def https_enabled?
|
91
|
+
Settings.instance.use_https
|
92
|
+
end
|
93
|
+
|
94
|
+
def ssl_private_key
|
95
|
+
OpenSSL::PKey::RSA.new(File.read(Settings.instance.ssl_private_key))
|
96
|
+
rescue Exception => e
|
97
|
+
# TODO: Use a logger
|
98
|
+
STDERR.puts "Unable to load private SSL key. Are the values correct in settings.yml and do permissions allow reading?: #{e}"
|
99
|
+
# logger.error "Unable to load private SSL key. Are the values correct in settings.yml and do permissions allow reading?: #{e}"
|
100
|
+
raise e
|
101
|
+
end
|
102
|
+
|
103
|
+
def ssl_certificate
|
104
|
+
OpenSSL::X509::Certificate.new(File.read(Settings.instance.ssl_certificate))
|
105
|
+
rescue Exception => e
|
106
|
+
# TODO: Use a logger
|
107
|
+
STDERR.puts "Unable to load SSL certificate. Are the values correct in settings.yml and do permissions allow reading?: #{e}"
|
108
|
+
# logger.error "Unable to load SSL certificate. Are the values correct in settings.yml and do permissions allow reading?: #{e}"
|
109
|
+
raise e
|
110
|
+
end
|
111
|
+
|
112
|
+
def load_config_dir(dir)
|
113
|
+
settings_yml = File.join(dir, 'settings.yml')
|
114
|
+
if File.exist? settings_yml
|
115
|
+
# TODO: Use a logger
|
116
|
+
puts "Loading settings from #{dir}"
|
117
|
+
Settings.load_global_settings settings_yml
|
118
|
+
Dir[File.join(dir, 'settings.d', '*.yml')].each { |path| Settings.load_plugin_settings(path) }
|
119
|
+
true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|