smart_proxy_monitoring 0.0.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.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # Smart Proxy - Monitoring
2
+
3
+ This plug-in adds support for Monitoring to Foreman's Smart Proxy.
4
+ It requires also the Foreman Monitoring plug-in.
5
+
6
+ # Installation
7
+
8
+ Please see the Foreman manual for appropriate instructions:
9
+
10
+ * [Foreman: How to Install a Plugin](http://theforeman.org/manuals/latest/index.html#6.Plugins)
11
+
12
+ The gem name is `smart_proxy_monitoring`.
13
+
14
+ RPM users can install the `rubygem-smart_proxy_monitoring` packages.
15
+
16
+ This plug-in has not been packaged for Debian, yet.
17
+
18
+ # Configuration
19
+
20
+ The plug-in requires some configuration on the Monitoring server and the Smart Proxy.
21
+ For now the only supported Monitoring solution is Icinga 2.
22
+
23
+ ## Icinga 2
24
+
25
+ The Smart Proxy connects to the Icinga 2 API using an API User with password or
26
+ certificate to get Monitoring information. It requires at least Icinga 2 version 2.5.
27
+
28
+ The Icinga project provides detailed [documentation on Icinga 2](http://docs.icinga.org/icinga2/).
29
+ The required steps for connecting the Smart Proxy and Icinga 2 will be found below.
30
+
31
+ ### Monitoring Server
32
+
33
+ On the Monitoring Server you have to enable the API and create API User.
34
+
35
+ For testing the fastest way to setup this will be the following commands.
36
+
37
+ ```
38
+ # icinga2 api setup
39
+ # systemctl restart icinga2.service
40
+ ```
41
+
42
+ This will create the certficates, enable the API feature and create and API User `root` with
43
+ a random password. The configuration of the API User will be located in `/etc/icinga2/conf.d/api-users.conf`.
44
+
45
+ More detailed instructions:
46
+
47
+ To enable the API follow the next steps, if the API is already enabled skip this steps
48
+ and start by creating an API User. The API will already be enabled if you use the Icingaweb 2
49
+ Module Director for configuration, Icinga 2 as Agents or in a distributed or high-available
50
+ setup.
51
+
52
+ Before you can enable the API a CA and a host certificate are required, the instructions
53
+ will help you to setup Icinga 2's own CA. You can also use your Puppet's certificates or
54
+ any other CA.
55
+
56
+ To create Icinga 2's own CA run:
57
+
58
+ ```
59
+ # icinga2 pki new-ca
60
+ ```
61
+
62
+ Afterwards copy the CA certificate to Icinga 2's pki directory:
63
+
64
+ ```
65
+ # cp /var/lib/icinga2/ca/ca.crt /etc/icinga2/pki/
66
+ ```
67
+
68
+ To create a certificate request for the node run:
69
+
70
+ ```
71
+ # icinga2 pki new-cert --cn $(hostname -f) --key /etc/icinga2/pki/$(hostname -f).key --csr /etc/icinga2/pki/$(hostname -f).csr
72
+ ```
73
+
74
+ And then sign the certficate request to get a certificate by executing:
75
+
76
+ ```
77
+ # icinga2 pki sign-csr --csr /etc/icinga2/pki/$(hostname -f).csr --cert /etc/icinga2/pki/$(hostname -f).crt
78
+ ```
79
+
80
+ With the certificates created and placed in Icinga 2's pki directory you can enable the API feature.
81
+
82
+ ```
83
+ # icinga2 feature enable api
84
+ # systemctl restart icinga2.service
85
+ ```
86
+
87
+ To allow API connections you have to create an API User. You should name him according to the use case,
88
+ so instructions will create an user named `foreman`.
89
+
90
+ Password authentication is easier to setup, but certificate-based authentication is more secure.
91
+
92
+ Password authentication only requires you to create an API User object in a configuration file
93
+ read by Icinga 2.
94
+
95
+ ```
96
+ # vi /etc/icinga2/conf.d/api-users.conf
97
+ object ApiUser "foreman" {
98
+ password = "foreman"
99
+ permissions = [ "*" ]
100
+ }
101
+ # systemctl reload icinga2.service
102
+ ```
103
+
104
+ Certificate-based authentication requires the API User object and a signed certificate.
105
+
106
+ ```
107
+ # vi /etc/icinga2/conf.d/api-users.conf
108
+ object ApiUser "foreman" {
109
+ client_cn = "foreman"
110
+ permissions = [ "*" ]
111
+ }
112
+ # systemctl reload icinga2.service
113
+ # icinga2 pki new-cert --cn foreman --key /etc/icinga2/pki/foreman.key --csr /etc/icinga2/pki/foreman.csr
114
+ # icinga2 pki sign-csr --csr /etc/icinga2/pki/foreman.csr --cert /etc/icinga2/pki/foreman.crt
115
+ ```
116
+
117
+ ### Smart Proxy
118
+
119
+ Ensure that the Monitoring module is enabled and uses the provider monitoring_icinga2.
120
+ It is the default provider so also no setting for use_provider is fine.
121
+ If you configured hosts in Icinga2 only with hostname instead of FQDN, you can add `:strip_domain` with
122
+ all the parts to strip, e.g. `.localdomain`.
123
+
124
+ ```
125
+ # vi /etc/foreman-proxy/settings.d/monitoring.yaml
126
+ ---
127
+ :enabled: true
128
+ :use_provider: monitoring_icinga2
129
+ ```
130
+
131
+ Configure the provider with your server details and the API User information.
132
+ Typically you will have to change the server attribute, copy the CA certificate from the server (located
133
+ in /etc/icinga2/pki/) and provide the authentication details of the API User. If using the IP address
134
+ instead of the FQDN of the server, you will have to set verify_ssl to false.
135
+
136
+ ```
137
+ # vi /etc/foreman-proxy/settings.d/monitoring_icinga2.yaml
138
+ ---
139
+ :enabled: true
140
+ :server: icinga2.localdomain
141
+ :api_cacert: /usr/share/foreman-proxy/monitoring/ca.crt
142
+ :api_user: foreman
143
+ :api_usercert: /usr/share/foreman-proxy/monitoring/foreman.crt
144
+ :api_userkey: /usr/share/foreman-proxy/monitoring/foreman.key
145
+ #:api_password: foreman
146
+ :verify_ssl: true
147
+ ```
148
+
149
+
150
+ # TODO
151
+
152
+ Monitoring:
153
+ * Add host creation and update
154
+
155
+ Provider Icinga2:
156
+ * Add endpoint and zone management for Icinga 2 as agent
157
+
158
+ # Copyright
159
+
160
+ Copyright (c) 2016 The Foreman developers
161
+
162
+ This program is free software: you can redistribute it and/or modify
163
+ it under the terms of the GNU General Public License as published by
164
+ the Free Software Foundation, either version 3 of the License, or
165
+ (at your option) any later version.
166
+
167
+ This program is distributed in the hope that it will be useful,
168
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
169
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
170
+ GNU General Public License for more details.
171
+
172
+ You should have received a copy of the GNU General Public License
173
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
174
+
@@ -0,0 +1,2 @@
1
+ gem 'smart_proxy_monitoring'
2
+ gem 'rest-client'
@@ -0,0 +1,8 @@
1
+ module ::Proxy::Monitoring
2
+ class ConfigurationLoader
3
+ def load_classes
4
+ require 'smart_proxy_monitoring/dependency_injection'
5
+ require 'smart_proxy_monitoring/monitoring_api'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Proxy::Monitoring
2
+ module DependencyInjection
3
+ include Proxy::DependencyInjection::Accessors
4
+ def container_instance
5
+ @container_instance ||= ::Proxy::Plugins.instance.find { |p| p[:name] == :monitoring }[:di_container]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,77 @@
1
+ require 'sinatra'
2
+ require 'smart_proxy_monitoring/monitoring_plugin'
3
+
4
+ module Proxy::Monitoring
5
+ class Api < ::Sinatra::Base
6
+ extend Proxy::Monitoring::DependencyInjection
7
+ inject_attr :monitoring_provider, :server
8
+
9
+ include ::Proxy::Log
10
+ helpers ::Proxy::Helpers
11
+ authorize_with_trusted_hosts
12
+ authorize_with_ssl_client
13
+
14
+ delete '/host/:host' do |host|
15
+ begin
16
+ validate_dns_name!(host)
17
+ host = strip_domain(host)
18
+
19
+ server.remove_host(host)
20
+ rescue Proxy::Monitoring::NotFound => e
21
+ log_halt 404, e
22
+ rescue Proxy::Monitoring::ConnectionError => e
23
+ log_halt 503, e
24
+ rescue Exception => e
25
+ log_halt 400, e
26
+ end
27
+ end
28
+
29
+ post '/downtime/host/:host?' do |host|
30
+ author = params[:author] || 'foreman'
31
+ comment = params[:comment] || 'triggered by foreman'
32
+ start_time = params[:start_time] || Time.now.to_i
33
+ end_time = params[:end_time] || (Time.now.to_i + (24 * 3600))
34
+
35
+ begin
36
+ validate_dns_name!(host)
37
+ host = strip_domain(host)
38
+
39
+ server.set_downtime_host(host, author, comment, start_time, end_time)
40
+ rescue Proxy::Monitoring::NotFound => e
41
+ log_halt 404, e
42
+ rescue Proxy::Monitoring::ConnectionError => e
43
+ log_halt 503, e
44
+ rescue Exception => e
45
+ log_halt 400, e
46
+ end
47
+ end
48
+
49
+ delete '/downtime/host/:host?' do |host|
50
+ author = params[:author] || 'foreman'
51
+ comment = params[:comment] || 'triggered by foreman'
52
+
53
+ begin
54
+ validate_dns_name!(host)
55
+ host = strip_domain(host)
56
+
57
+ server.remove_downtime_host(host, author, comment)
58
+ rescue Proxy::Monitoring::NotFound => e
59
+ log_halt 404, e
60
+ rescue Proxy::Monitoring::ConnectionError => e
61
+ log_halt 503, e
62
+ rescue Exception => e
63
+ log_halt 400, e
64
+ end
65
+ end
66
+
67
+ def validate_dns_name!(name)
68
+ raise Proxy::Monitoring::Error.new("Invalid DNS name #{name}") unless name =~ /^([a-zA-Z0-9]([-a-zA-Z0-9]+)?\.?)+$/
69
+ end
70
+
71
+ def strip_domain(name)
72
+ domain = Proxy::Monitoring::Plugin.settings.strip_domain
73
+ name.slice!(domain) unless domain.nil?
74
+ name
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ require 'smart_proxy_monitoring/monitoring_api'
2
+
3
+ map '/monitoring' do
4
+ run Proxy::Monitoring::Api
5
+ end
@@ -0,0 +1,19 @@
1
+ require 'smart_proxy_monitoring/version'
2
+
3
+ module Proxy::Monitoring
4
+ class NotFound < RuntimeError; end
5
+ class ConnectionError < RuntimeError; end
6
+ class Error < RuntimeError; end
7
+
8
+ class Plugin < ::Proxy::Plugin
9
+ plugin 'monitoring', Proxy::Monitoring::VERSION
10
+
11
+ uses_provider
12
+ default_settings use_provider: 'monitoring_icinga2'
13
+
14
+ http_rackup_path File.expand_path('monitoring_http_config.ru', File.expand_path('../', __FILE__))
15
+ https_rackup_path File.expand_path('monitoring_http_config.ru', File.expand_path('../', __FILE__))
16
+
17
+ load_classes ::Proxy::Monitoring::ConfigurationLoader
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Proxy
2
+ module Monitoring
3
+ VERSION = '0.0.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ require 'smart_proxy_monitoring/version'
2
+ require 'smart_proxy_monitoring/configuration_loader'
3
+ require 'smart_proxy_monitoring/monitoring_plugin'
4
+ require 'smart_proxy_monitoring_icinga2'
@@ -0,0 +1,6 @@
1
+ module Proxy::Monitoring
2
+ class Error < RuntimeError; end
3
+ class NotFound < RuntimeError; end
4
+
5
+ class Provider; end
6
+ end
@@ -0,0 +1,66 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'json'
4
+
5
+ module ::Proxy::Monitoring::Icinga2
6
+ class Icinga2ApiObserver
7
+ include ::Proxy::Log
8
+ include ::Proxy::Monitoring::Icinga2::Common
9
+
10
+ attr_reader :semaphore
11
+
12
+ def initialize(queue)
13
+ @queue = queue.queue
14
+ @semaphore = Mutex.new
15
+ end
16
+
17
+ def monitor
18
+ loop do
19
+ logger.debug "Connecting to Icinga event monitoring api: #{Icinga2Client.baseurl}."
20
+
21
+ ssl_socket = Icinga2Client.events_socket('/events?queue=foreman&types=StateChange&types=AcknowledgementSet&types=AcknowledgementCleared&types=DowntimeTriggered&types=DowntimeRemoved')
22
+
23
+ logger.info 'Icinga event api monitoring started.'
24
+
25
+ while line = ssl_socket.gets
26
+ next unless line.chars.first == '{'
27
+
28
+ with_event_counter('Icinga2 Event API Monitor') do
29
+ begin
30
+ parsed = JSON.parse(line)
31
+ if @queue.size > 100_000
32
+ @queue.clear
33
+ logger.error 'Queue was full. Flushing. Events were lost.'
34
+ end
35
+ @queue.push(parsed)
36
+ rescue JSON::ParserError => e
37
+ logger.error "Icinga2 Event API Monitor: Malformed JSON: #{e.message}"
38
+ end
39
+ end
40
+
41
+ end
42
+ logger.info 'Icinga event api monitoring stopped.'
43
+ end
44
+ rescue Errno::ECONNREFUSED => e
45
+ logger.error "Icinga Event Stream: Connection refused. Retrying in 5 seconds. Reason: #{e.message}"
46
+ sleep 5
47
+ retry
48
+ rescue Exception => e
49
+ logger.error "Error while monitoring: #{e.message}\n#{e.backtrace.join("\n")}"
50
+ sleep 1
51
+ retry
52
+ ensure
53
+ ssl_socket.sysclose unless ssl_socket.nil?
54
+ end
55
+
56
+ def start
57
+ @thread = Thread.new { monitor }
58
+ @thread.abort_on_exception = true
59
+ @thread
60
+ end
61
+
62
+ def stop
63
+ @thread.terminate unless @thread.nil?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,127 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'rest-client'
4
+ require 'thread'
5
+ require 'socket'
6
+ require 'base64'
7
+
8
+ module ::Proxy::Monitoring::Icinga2
9
+ class Icinga2Client
10
+ class << self
11
+ def client(request_url)
12
+ headers = {
13
+ 'Accept' => 'application/json'
14
+ }
15
+
16
+ options = {
17
+ headers: headers,
18
+ user: user,
19
+ ssl_ca_file: cacert,
20
+ verify_ssl: ssl
21
+ }
22
+
23
+ auth_options = if certificate_request?
24
+ {
25
+ ssl_client_cert: cert,
26
+ ssl_client_key: key
27
+ }
28
+ else
29
+ {
30
+ password: password
31
+ }
32
+ end
33
+ options.merge!(auth_options)
34
+
35
+ RestClient::Resource.new(
36
+ URI.encode([baseurl, request_url].join('')),
37
+ options
38
+ )
39
+ end
40
+
41
+ def events_socket(endpoint)
42
+ uri = URI.parse([baseurl, endpoint].join(''))
43
+ socket = TCPSocket.new(uri.host, uri.port)
44
+
45
+ ssl_context = OpenSSL::SSL::SSLContext.new
46
+ ssl_context.ca_file = cacert
47
+
48
+ if ssl
49
+ ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
50
+ else
51
+ ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
52
+ end
53
+
54
+ if certificate_request?
55
+ ssl_context.cert = cert
56
+ ssl_context.key = key
57
+ end
58
+
59
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
60
+ ssl_socket.sync_close = true
61
+ ssl_socket.connect
62
+
63
+ ssl_socket.write "POST #{uri.request_uri} HTTP/1.1\r\n"
64
+ ssl_socket.write "Accept: application/json\r\n"
65
+ unless certificate_request?
66
+ auth = Base64.encode64("#{user}:#{password}")
67
+ ssl_socket.write "Authorization: Basic #{auth}"
68
+ end
69
+ ssl_socket.write "\r\n"
70
+
71
+ ssl_socket
72
+ end
73
+
74
+ def get(url)
75
+ client(url).get
76
+ end
77
+
78
+ def post(url, data)
79
+ client(url).post(data)
80
+ end
81
+
82
+ def put(url)
83
+ client(url).put
84
+ end
85
+
86
+ def delete(url)
87
+ client(url).delete
88
+ end
89
+
90
+ def cert
91
+ file = Proxy::Monitoring::Icinga2::Plugin.settings.api_usercert
92
+ return unless !file.nil? && File.file?(file)
93
+ OpenSSL::X509::Certificate.new(File.read(file))
94
+ end
95
+
96
+ def key
97
+ file = Proxy::Monitoring::Icinga2::Plugin.settings.api_userkey
98
+ return unless !file.nil? && File.file?(file)
99
+ OpenSSL::PKey::RSA.new(File.read(file))
100
+ end
101
+
102
+ def cacert
103
+ Proxy::Monitoring::Icinga2::Plugin.settings.api_cacert
104
+ end
105
+
106
+ def user
107
+ Proxy::Monitoring::Icinga2::Plugin.settings.api_user
108
+ end
109
+
110
+ def password
111
+ Proxy::Monitoring::Icinga2::Plugin.settings.api_password
112
+ end
113
+
114
+ def ssl
115
+ Proxy::Monitoring::Icinga2::Plugin.settings.verify_ssl
116
+ end
117
+
118
+ def certificate_request?
119
+ cert && key
120
+ end
121
+
122
+ def baseurl
123
+ "https://#{Proxy::Monitoring::Icinga2::Plugin.settings.server}:#{Proxy::Monitoring::Icinga2::Plugin.settings.api_port}/v1"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,109 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'json'
4
+
5
+ module ::Proxy::Monitoring::Icinga2
6
+ class Icinga2InitialImporter
7
+ include ::Proxy::Log
8
+
9
+ def initialize(queue)
10
+ @queue = queue.queue
11
+ end
12
+
13
+ def monitor
14
+ logger.debug 'Starting initial icinga import.'
15
+
16
+ around_action('Initial Host Import') do
17
+ import_hosts
18
+ end
19
+
20
+ around_action('Initial Services Import') do
21
+ import_services
22
+ end
23
+
24
+ around_action('Initial Downtimes Import') do
25
+ import_downtimes
26
+ end
27
+
28
+ logger.info 'Finished initial icinga import.'
29
+ rescue Exception => e
30
+ logger.error "Error during initial import: #{e.message}\n#{e.backtrace}"
31
+ end
32
+
33
+ def import_hosts
34
+ results = Icinga2Client.get('/objects/hosts?attrs=name&attrs=last_check_result&attrs=acknowledgement')
35
+ results = JSON.parse(results)
36
+ results['results'].each do |result|
37
+ parsed = {
38
+ host: result['attrs']['name'],
39
+ result: result['attrs']['last_check_result']['state'],
40
+ timestamp: result['attrs']['last_check_result']['schedule_end'],
41
+ acknowledged: (result['attrs']['acknowledgement'] != 0),
42
+ initial: true,
43
+ type: '_parsed'
44
+ }
45
+ @queue.push(parsed)
46
+ end
47
+ end
48
+
49
+ def import_services
50
+ results = Icinga2Client.get('/objects/services?attrs=name&attrs=last_check_result&attrs=acknowledgement&attrs=host_name')
51
+ results = JSON.parse(results)
52
+ results['results'].each do |result|
53
+ parsed = {
54
+ host: result['attrs']['host_name'],
55
+ service: result['attrs']['name'],
56
+ result: result['attrs']['last_check_result']['state'],
57
+ timestamp: result['attrs']['last_check_result']['schedule_end'],
58
+ acknowledged: (result['attrs']['acknowledgement'] != 0),
59
+ initial: true,
60
+ type: '_parsed'
61
+ }
62
+ @queue.push(parsed)
63
+ end
64
+ end
65
+
66
+ def import_downtimes
67
+ results = Icinga2Client.get('/objects/downtimes?attrs=host_name&attrs=service_name&attrs=trigger_time')
68
+ results = JSON.parse(results)
69
+ results['results'].each do |result|
70
+ next unless result['attrs']['trigger_time'] != 0
71
+ parsed = {
72
+ host: result['attrs']['host_name'],
73
+ service: result['attrs']['service_name'],
74
+ downtime: true,
75
+ initial: true,
76
+ type: '_parsed'
77
+ }
78
+ @queue.push(parsed)
79
+ end
80
+ end
81
+
82
+ def start
83
+ @thread = Thread.new { monitor }
84
+ @thread.abort_on_exception = true
85
+ @thread
86
+ end
87
+
88
+ def stop
89
+ @thread.terminate unless @thread.nil?
90
+ end
91
+
92
+ private
93
+
94
+ def around_action(task)
95
+ beginning_time = Time.now
96
+ logger.info "Starting Task: #{task}."
97
+ yield
98
+ end_time = Time.now
99
+ logger.info "Finished Task: #{task} in #{end_time - beginning_time} seconds."
100
+ rescue Errno::ECONNREFUSED => e
101
+ logger.error "Icinga Initial Importer: Connection refused in task #{task}. Reason: #{e.message}"
102
+ logger.error "Icinga Initial Importer: Restarting #{task} in 5 seconds."
103
+ sleep 5
104
+ retry
105
+ rescue JSON::ParserError => e
106
+ logger.error "Icinga Initial Importer: Failed to parse JSON: #{e.message}"
107
+ end
108
+ end
109
+ end