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,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