smart_proxy_dynflow_core 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,166 +0,0 @@
1
- require 'webrick/https'
2
- require 'smart_proxy_dynflow_core/bundler_helper'
3
- require 'smart_proxy_dynflow_core/settings'
4
- require 'sd_notify'
5
-
6
- module SmartProxyDynflowCore
7
- class Launcher
8
- CIPHERS = ['ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384',
9
- 'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA256',
10
- 'AES256-SHA256', 'AES128-SHA', 'AES256-SHA'].freeze
11
-
12
- def self.launch!(options)
13
- self.new.start options
14
- end
15
-
16
- def start(options)
17
- Settings.instance.standalone = true
18
- load_settings!(options)
19
- install_usr1_trap
20
- Rack::Server.new(rack_settings).start do |_server|
21
- SmartProxyDynflowCore::Core.ensure_initialized
22
- ::SdNotify.ready
23
- end
24
- Log.instance.info "Finished shutting down"
25
- Logging.shutdown
26
- end
27
-
28
- def load_settings!(options = {})
29
- config_dir, one_config = options.values_at(:config_dir, :one_config)
30
- possible_config_dirs = [
31
- '/etc/smart_proxy_dynflow_core',
32
- File.expand_path('~/.config/smart_proxy_dynflow_core'),
33
- File.join(File.dirname(__FILE__), '..', '..', 'config'),
34
- ]
35
- possible_config_dirs << config_dir if config_dir
36
- BundlerHelper.require_groups(:default)
37
- possible_config_dirs.reverse! if one_config
38
- possible_config_dirs.select { |config_dir| File.directory? config_dir }.each do |config_dir|
39
- break if load_config_dir(config_dir) && one_config
40
- end
41
- Settings.instance.daemonize = options[:daemonize] if options.key?(:daemonize)
42
- Settings.instance.pid_file = options[:pid_file] if options.key?(:pid_file)
43
- Settings.loaded!
44
- end
45
-
46
- def self.route_mapping(rack_builder)
47
- rack_builder.map '/console' do
48
- run Core.web_console
49
- end
50
-
51
- rack_builder.map '/' do
52
- run Api
53
- end
54
- end
55
-
56
- def install_usr1_trap
57
- trap(:USR1) do
58
- Log.reopen
59
- end
60
- end
61
-
62
- private
63
-
64
- def rack_settings
65
- settings = if https_enabled?
66
- Log.instance.debug "Using HTTPS"
67
- https_app
68
- else
69
- Log.instance.debug "Using HTTP"
70
- {}
71
- end
72
- settings.merge(base_settings)
73
- end
74
-
75
- def app
76
- Rack::Builder.new do
77
- SmartProxyDynflowCore::Launcher.route_mapping(self)
78
- end
79
- end
80
-
81
- def base_settings
82
- {
83
- :app => app,
84
- :Host => Settings.instance.listen,
85
- :Port => Settings.instance.port,
86
- :AccessLog => [],
87
- :Logger => Log.instance,
88
- :daemonize => Settings.instance.daemonize,
89
- :pid => Settings.instance.daemonize && Settings.instance.pid_file,
90
- :server => :webrick
91
- }
92
- end
93
-
94
- def https_app
95
- ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
96
- ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE)
97
- # This is required to disable SSLv3 on Ruby 1.8.7
98
- ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
99
- ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
100
- ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
101
- ssl_options |= OpenSSL::SSL::OP_NO_TLSv1_1 if defined?(OpenSSL::SSL::OP_NO_TLSv1_1)
102
-
103
- Settings.instance.tls_disabled_versions&.each do |version|
104
- constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil
105
-
106
- if constant
107
- Log.instance.info "TLSv#{version} will be disabled."
108
- ssl_options |= constant
109
- else
110
- Log.instance.warn "TLSv#{version} was not found."
111
- end
112
- end
113
-
114
- {
115
- :SSLEnable => true,
116
- :SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
117
- :SSLPrivateKey => ssl_private_key,
118
- :SSLCertificate => ssl_certificate,
119
- :SSLCACertificateFile => Settings.instance.ssl_ca_file,
120
- :SSLCiphers => CIPHERS - SmartProxyDynflowCore::Settings.instance.ssl_disabled_ciphers,
121
- :SSLOptions => ssl_options
122
- }
123
- end
124
- # rubocop:enable Metrics/PerceivedComplexity
125
-
126
- def https_enabled?
127
- Settings.instance.use_https
128
- end
129
-
130
- def ssl_private_key
131
- OpenSSL::PKey::RSA.new(File.read(Settings.instance.ssl_private_key))
132
- rescue Exception => e
133
- Log.instance.fatal "Unable to load private SSL key. Are the values "\
134
- "correct in settings.yml and do permissions allow reading?: #{e}"
135
- raise e
136
- end
137
-
138
- def ssl_certificate
139
- OpenSSL::X509::Certificate.new(File.read(Settings.instance.ssl_certificate))
140
- rescue Exception => e
141
- Log.instance.fatal "Unable to load SSL certificate. Are the values " \
142
- "correct in settings.yml and do permissions allow reading?: #{e}"
143
- raise e
144
- end
145
-
146
- def load_config_dir(dir)
147
- settings_yml = File.join(dir, 'settings.yml')
148
- if File.exist? settings_yml
149
- Log.instance.debug "Loading settings from #{dir}"
150
- Settings.load_global_settings settings_yml
151
- Dir[File.join(dir, 'settings.d', '*.yml')].each { |path| Settings.load_plugin_settings(path) }
152
- true
153
- end
154
- ForemanTasksCore::SettingsLoader.settings_registry.each_key do |settings_keys|
155
- settings = settings_keys.inject({}) do |h, settings_key|
156
- if SETTINGS.plugins.key?(settings_key.to_s)
157
- h.merge(SETTINGS.plugins[settings_key.to_s].to_h)
158
- else
159
- h
160
- end
161
- end
162
- ForemanTasksCore::SettingsLoader.setup_settings(settings_keys.first, settings)
163
- end
164
- end
165
- end
166
- end
@@ -1,146 +0,0 @@
1
- require 'logging'
2
-
3
- module SmartProxyDynflowCore
4
- class ReopenAppender < ::Logging::Appender
5
- def initialize(name, logger, opts = {})
6
- @reopen = false
7
- @logger = logger
8
- super(name, opts)
9
- end
10
-
11
- def set(status = true)
12
- @reopen = status
13
- end
14
-
15
- def append(_event)
16
- if @reopen
17
- Logging.reopen
18
- @reopen = false
19
- end
20
- end
21
- end
22
-
23
- class Log
24
- BASE_LOG_SIZE = 1024 * 1024 # 1 MiB
25
- LOGGER_NAME = 'dynflow-core'.freeze
26
-
27
- begin
28
- require 'syslog/logger'
29
- @syslog_available = true
30
- rescue LoadError
31
- @syslog_available = false
32
- end
33
-
34
- class << self
35
- def reload!
36
- Logging.logger[LOGGER_NAME].appenders.each(&:close)
37
- Logging.logger[LOGGER_NAME].clear_appenders
38
- @logger = nil
39
- instance
40
- end
41
-
42
- def reopen
43
- return if @logger.nil? || @reopen.nil?
44
- if Settings.instance.log_file !~ /^(STDOUT|SYSLOG|JOURNALD?)$/i
45
- @reopen.set
46
- end
47
- end
48
-
49
- def instance
50
- return ::Proxy::LogBuffer::Decorator.instance unless Settings.instance.standalone
51
- return @logger if @logger
52
- layout = Logging::Layouts.pattern(pattern: Settings.instance.file_logging_pattern + "\n")
53
- notime_layout = Logging::Layouts.pattern(pattern: Settings.instance.system_logging_pattern + "\n")
54
- log_file = Settings.instance.log_file || ''
55
- @logger = Logging.logger[LOGGER_NAME]
56
- @reopen = ReopenAppender.new("Reopen dummy appender", @logger)
57
- @logger.add_appenders(@reopen)
58
- if !Settings.instance.loaded || log_file.casecmp('STDOUT').zero?
59
- @logger.add_appenders(Logging.appenders.stdout(LOGGER_NAME, layout: layout))
60
- elsif log_file.casecmp('SYSLOG').zero?
61
- unless @syslog_available
62
- puts "Syslog is not supported on this platform, use STDOUT or a file"
63
- exit(1)
64
- end
65
- @logger.add_appenders(Logging.appenders.syslog(LOGGER_NAME, layout: notime_layout, facility: ::Syslog::Constants::LOG_LOCAL5))
66
- elsif log_file.casecmp('JOURNAL').zero? || log_file.casecmp('JOURNALD').zero?
67
- begin
68
- @logger.add_appenders(Logging.appenders.journald(LOGGER_NAME, LOGGER_NAME: :proxy_logger, layout: notime_layout, facility: ::Syslog::Constants::LOG_LOCAL5))
69
- rescue NoMethodError
70
- @logger.add_appenders(Logging.appenders.stdout(LOGGER_NAME, layout: layout))
71
- @logger.warn "Journald is not available on this platform. Falling back to STDOUT."
72
- end
73
- else
74
- begin
75
- keep = Settings.instance.file_rolling_keep
76
- size = BASE_LOG_SIZE * Settings.instance.file_rolling_size
77
- age = Settings.instance.file_rolling_age
78
- if size.positive?
79
- @logger.add_appenders(Logging.appenders.rolling_file(LOGGER_NAME, layout: layout, filename: log_file, keep: keep, size: size, age: age, roll_by: 'number'))
80
- else
81
- @logger.add_appenders(Logging.appenders.file(LOGGER_NAME, layout: layout, filename: log_file))
82
- end
83
- rescue ArgumentError => ae
84
- @logger.add_appenders(Logging.appenders.stdout(LOGGER_NAME, layout: layout))
85
- @logger.warn "Log file #{log_file} cannot be opened. Falling back to STDOUT: #{ae}"
86
- end
87
- end
88
- @logger.level = ::Logging.level_num(Settings.instance.log_level)
89
- @logger
90
- end
91
-
92
- def with_fields(fields = {})
93
- ::Logging.ndc.push(fields) do
94
- yield
95
- end
96
- end
97
-
98
- # Standard way for logging exceptions to get the most data in the log. By default
99
- # it logs via warn level, this can be changed via options[:level]
100
- def exception(context_message, exception, options = {})
101
- level = options[:level] || :warn
102
- unless ::Logging::LEVELS.keys.include?(level.to_s)
103
- raise "Unexpected log level #{level}, expected one of #{::Logging::LEVELS.keys}"
104
- end
105
- # send class, message and stack as structured fields in addition to message string
106
- backtrace = exception.backtrace ? exception.backtrace : []
107
- extra_fields = {
108
- exception_class: exception.class.name,
109
- exception_message: exception.message,
110
- exception_backtrace: backtrace
111
- }
112
- extra_fields[:foreman_code] = exception.code if exception.respond_to?(:code)
113
- with_fields(extra_fields) do
114
- @logger.public_send(level) do
115
- ([context_message, "#{exception.class}: #{exception.message}"] + backtrace).join("\n")
116
- end
117
- end
118
- end
119
- end
120
-
121
- class ProxyStructuredFormater < ::Dynflow::LoggerAdapters::Formatters::Abstract
122
- def format(message)
123
- if message.is_a?(Exception)
124
- subject = "#{message.message} (#{message.class})"
125
- if @base.respond_to?(:exception)
126
- @base.exception("Error details", message)
127
- subject
128
- else
129
- "#{subject}\n#{message.backtrace.join("\n")}"
130
- end
131
- else
132
- @original_formatter.call(severity, datetime, prog_name, message)
133
- end
134
- end
135
- end
136
-
137
- class ProxyAdapter < ::Dynflow::LoggerAdapters::Simple
138
- def initialize(logger, level = Logger::DEBUG, _formatters = [])
139
- @logger = logger
140
- @logger.level = level
141
- @action_logger = apply_formatters(ProgNameWrapper.new(@logger, ' action'), [])
142
- @dynflow_logger = apply_formatters(ProgNameWrapper.new(@logger, 'dynflow'), [])
143
- end
144
- end
145
- end
146
- end
@@ -1,31 +0,0 @@
1
- module SmartProxyDynflowCore
2
- class LoggerMiddleware
3
- def initialize(app)
4
- @logger = SmartProxyDynflowCore::Log.instance
5
- @app = app
6
- end
7
-
8
- def call(env)
9
- before = Time.now.to_f
10
- status = 500
11
- env['rack.logger'] = @logger
12
- @logger.info { "Started #{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['QUERY_STRING']}" }
13
- @logger.debug { 'Headers: ' + env.select { |k, v| k.start_with? 'HTTP_' }.inspect }
14
- if @logger.debug? && env['rack.input']
15
- body = env['rack.input'].read
16
- @logger.debug('Body: ' + body) unless body.empty?
17
- env['rack.input'].rewind
18
- end
19
- status, = @app.call(env)
20
- rescue Exception => e
21
- Log.exception "Error processing request '#{::Logging.mdc['request']}", e
22
- raise e
23
- ensure
24
- @logger.info do
25
- after = Time.now.to_f
26
- duration = (after - before) * 1000
27
- "Finished #{env['REQUEST_METHOD']} #{env['PATH_INFO']} with #{status} (#{duration.round(2)} ms)"
28
- end
29
- end
30
- end
31
- end
@@ -1,18 +0,0 @@
1
- module SmartProxyDynflowCore
2
- class RequestIdMiddleware
3
- def initialize(app)
4
- @app = app
5
- end
6
-
7
- def call(env)
8
- ::Logging.mdc['remote_ip'] = env['REMOTE_ADDR']
9
- if env.has_key?('HTTP_X_REQUEST_ID')
10
- ::Logging.mdc['request'] = env['HTTP_X_REQUEST_ID']
11
- else
12
- ::Logging.mdc['request'] = SecureRandom.uuid
13
- end
14
- status, header, body = @app.call(env)
15
- [status, header, ::Rack::BodyProxy.new(body) { ::Logging.mdc.clear }]
16
- end
17
- end
18
- end
@@ -1,86 +0,0 @@
1
- require 'ostruct'
2
-
3
- module SmartProxyDynflowCore
4
- class Settings < OpenStruct
5
- DEFAULT_SETTINGS = {
6
- :database => '/var/lib/foreman-proxy/dynflow/dynflow.sqlite',
7
- :foreman_url => 'https://127.0.0.1:3000',
8
- :console_auth => true,
9
- :listen => '127.0.0.1',
10
- :port => '8008',
11
- :use_https => false,
12
- :ssl_ca_file => nil,
13
- :ssl_private_key => nil,
14
- :ssl_certificate => nil,
15
- :ssl_disabled_ciphers => [],
16
- :tls_disabled_versions => [],
17
- :foreman_ssl_ca => nil,
18
- :foreman_ssl_key => nil,
19
- :foreman_ssl_cert => nil,
20
- :standalone => false,
21
- :log_file => '/var/log/foreman-proxy/smart_proxy_dynflow_core.log',
22
- :log_level => :ERROR,
23
- :plugins => {},
24
- :pid_file => '/var/run/foreman-proxy/smart_proxy_dynflow_core.pid',
25
- :daemonize => false,
26
- :execution_plan_cleaner_age => 60 * 60 * 24,
27
- :loaded => false,
28
- :file_logging_pattern => '%d %.8X{request} [%.1l] %m',
29
- :system_logging_pattern => '%.8X{request} [%.1l] %m',
30
- :file_rolling_keep => 6,
31
- :file_rolling_size => 0,
32
- :file_rolling_age => 'weekly'
33
- }.freeze
34
-
35
- PROXY_SETTINGS = %i[ssl_ca_file ssl_certificate ssl_private_key foreman_url
36
- foreman_ssl_ca foreman_ssl_cert foreman_ssl_key
37
- log_file log_level ssl_disabled_ciphers].freeze
38
- PLUGIN_SETTINGS = %i[database core_url console_auth
39
- execution_plan_cleaner_age].freeze
40
-
41
- def initialize(settings = {})
42
- super(DEFAULT_SETTINGS.merge(settings))
43
- end
44
-
45
- def self.instance
46
- SmartProxyDynflowCore::SETTINGS
47
- end
48
-
49
- def self.load_global_settings(path)
50
- if File.exist? File.join(path)
51
- YAML.load_file(path).each do |key, value|
52
- SETTINGS[key] = value
53
- end
54
- end
55
- end
56
-
57
- def self.loaded!
58
- Settings.instance.loaded = true
59
- Log.instance.info 'Settings loaded, reloading logger'
60
- Log.reload!
61
- end
62
-
63
- def self.load_from_proxy(plugin)
64
- settings = plugin.settings.to_h
65
- PROXY_SETTINGS.each do |key|
66
- SETTINGS[key] = Proxy::SETTINGS[key]
67
- end
68
- PLUGIN_SETTINGS.each do |key|
69
- SETTINGS[key] = settings[key] if settings.key?(key)
70
- end
71
- SETTINGS.plugins.values.each(&:load_settings_from_proxy)
72
- Settings.loaded!
73
- end
74
-
75
- def self.load_plugin_settings(path)
76
- settings = YAML.load_file(path)
77
- name = File.basename(path).gsub(/\.yml$/, '')
78
- if SETTINGS.plugins.key? name
79
- settings = SETTINGS.plugins[name].to_h.merge(settings)
80
- end
81
- SETTINGS.plugins[name] = OpenStruct.new settings
82
- end
83
- end
84
- end
85
-
86
- SmartProxyDynflowCore::SETTINGS = SmartProxyDynflowCore::Settings.new