smart_proxy_dynflow_core 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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