smart_proxy_monitoring 0.0.1

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