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.
@@ -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,5 @@
1
+ require 'smart_proxy_dynflow_core/settings'
2
+ require 'smart_proxy_dynflow_core/core'
3
+ require 'smart_proxy_dynflow_core/helpers'
4
+ require 'smart_proxy_dynflow_core/callback'
5
+ require 'smart_proxy_dynflow_core/api'
@@ -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