nagios-herald 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/Rakefile +9 -0
- data/bin/draw_stack_bars +76 -0
- data/bin/dump_nagios_env.sh +25 -0
- data/bin/get_ganglia_graph +82 -0
- data/bin/get_graph +50 -0
- data/bin/get_graphite_graph +58 -0
- data/bin/nagios-herald +6 -0
- data/bin/splunk_alert_frequency +54 -0
- data/contrib/nrpe-plugins/check_cpu_stats.sh +186 -0
- data/contrib/nrpe-plugins/check_disk.sh +34 -0
- data/contrib/nrpe-plugins/check_mem.pl +181 -0
- data/contrib/nrpe-plugins/nrpe-plugin-examples.md +11 -0
- data/docs/config.md +62 -0
- data/docs/example_alerts.md +48 -0
- data/docs/formatters.md +180 -0
- data/docs/helpers.md +12 -0
- data/docs/images/cpu_no_context.png +0 -0
- data/docs/images/cpu_with_context.png +0 -0
- data/docs/images/disk_space_no_context.png +0 -0
- data/docs/images/disk_space_with_context.png +0 -0
- data/docs/images/memory_high_no_context.png +0 -0
- data/docs/images/memory_high_with_context.png +0 -0
- data/docs/images/nagios-herald-formatter-content-example.png +0 -0
- data/docs/images/nagios-herald.png +0 -0
- data/docs/images/stack-bars.png +0 -0
- data/docs/images/vanilla-nagios.png +0 -0
- data/docs/messages.md +16 -0
- data/docs/nagios-config.md +74 -0
- data/docs/tools.md +79 -0
- data/etc/config.yml.example +14 -0
- data/etc/readme.md +2 -0
- data/lib/nagios-herald/config.rb +25 -0
- data/lib/nagios-herald/executor.rb +265 -0
- data/lib/nagios-herald/formatter_loader.rb +82 -0
- data/lib/nagios-herald/formatters/base.rb +524 -0
- data/lib/nagios-herald/formatters/check_cpu.rb +71 -0
- data/lib/nagios-herald/formatters/check_disk.rb +143 -0
- data/lib/nagios-herald/formatters/check_logstash.rb +155 -0
- data/lib/nagios-herald/formatters/check_memory.rb +42 -0
- data/lib/nagios-herald/formatters/example.rb +19 -0
- data/lib/nagios-herald/formatters.rb +1 -0
- data/lib/nagios-herald/helpers/ganglia_graph.rb +99 -0
- data/lib/nagios-herald/helpers/graphite_graph.rb +85 -0
- data/lib/nagios-herald/helpers/logstash_query.rb +125 -0
- data/lib/nagios-herald/helpers/splunk_alert_frequency.rb +170 -0
- data/lib/nagios-herald/helpers/splunk_query.rb +119 -0
- data/lib/nagios-herald/helpers/url_image.rb +76 -0
- data/lib/nagios-herald/helpers.rb +5 -0
- data/lib/nagios-herald/logging.rb +48 -0
- data/lib/nagios-herald/message_loader.rb +40 -0
- data/lib/nagios-herald/messages/base.rb +56 -0
- data/lib/nagios-herald/messages/email.rb +150 -0
- data/lib/nagios-herald/messages/irc.rb +58 -0
- data/lib/nagios-herald/messages/pager.rb +75 -0
- data/lib/nagios-herald/messages.rb +3 -0
- data/lib/nagios-herald/test_helpers/base_test_case.rb +82 -0
- data/lib/nagios-herald/util.rb +45 -0
- data/lib/nagios-herald/version.rb +3 -0
- data/lib/nagios-herald.rb +7 -0
- data/lib/stackbars/__init__.py +0 -0
- data/lib/stackbars/chart_utils.py +25 -0
- data/lib/stackbars/grouped_stackbars.py +97 -0
- data/lib/stackbars/pilfonts/Tahoma.ttf +0 -0
- data/lib/stackbars/pilfonts/aerial.ttf +0 -0
- data/lib/stackbars/pilfonts/arial_black.ttf +0 -0
- data/lib/stackbars/stackbar.py +100 -0
- data/nagios-herald.gemspec +33 -0
- data/test/env_files/check_cpu_idle.CRITICAL +199 -0
- data/test/env_files/check_cpu_iowait.WARNING +199 -0
- data/test/env_files/check_disk.CRITICAL +197 -0
- data/test/env_files/check_disk.CRITICAL_ICINGA +197 -0
- data/test/env_files/check_disk.RECOVERY +197 -0
- data/test/env_files/check_memory.CRITICAL +197 -0
- data/test/env_files/nagios_vars.EXAMPLE +197 -0
- data/test/unit/test_config.rb +31 -0
- data/test/unit/test_executor.rb +65 -0
- data/test/unit/test_formatter_base.rb +131 -0
- data/test/unit/test_formatter_check_cpu_idle_critical.rb +135 -0
- data/test/unit/test_formatter_check_memory.rb +135 -0
- data/test/unit/test_icinga_variables.rb +31 -0
- data/test/unit/test_logging.rb +35 -0
- data/test/unit/test_message_email.rb +69 -0
- data/test/unit/test_message_pager.rb +69 -0
- metadata +204 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# Use Splunk to search for previous occurrences of a given Nagios alert and
|
6
|
+
# report.
|
7
|
+
#
|
8
|
+
# Can search for host alerts (i.e. DOWN state) and service alerts for a
|
9
|
+
# single host (i.e. 'Disk Space' is CRITICAL).
|
10
|
+
#
|
11
|
+
# Options:
|
12
|
+
# :duration - time (in days) to search [DEFAULT: 7 days]
|
13
|
+
# :hostname - Etsy Nagios-like hostname (i.e. web0200.ny4) [REQUIRED]
|
14
|
+
# :service_name - service name to search (i.e. 'Disk Space) [OPTIONAL]
|
15
|
+
#
|
16
|
+
# If the :service_name argument is not present, this performs a search
|
17
|
+
# for host alerts.
|
18
|
+
|
19
|
+
module NagiosHerald
|
20
|
+
module Helpers
|
21
|
+
class SplunkReporter
|
22
|
+
include NagiosHerald::Logging
|
23
|
+
|
24
|
+
# Public: Initialize a new SplunkReporter object.
|
25
|
+
#
|
26
|
+
# splunk_url - The URI of the Splunk search API.
|
27
|
+
# username - A username that's authorized to perform Splunk queries.
|
28
|
+
# password - Yeah, a password.
|
29
|
+
#
|
30
|
+
# Returns a new SplunkReporter object.
|
31
|
+
def initialize(splunk_url, username, password)
|
32
|
+
uri = URI.parse( splunk_url )
|
33
|
+
@splunk_host = uri.host
|
34
|
+
@splunk_port = uri.port
|
35
|
+
@splunk_uri = uri.request_uri
|
36
|
+
|
37
|
+
@username = username
|
38
|
+
@password = password
|
39
|
+
@fields = ['hostname', 'service_name', 'state', 'date_year', 'date_month', 'date_mday', 'date_hour', 'date_minute']
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Generate the Splunk query.
|
43
|
+
#
|
44
|
+
# hostname - The name of the alerting host we want to query.
|
45
|
+
# service - Optional service name that may be alerting.
|
46
|
+
#
|
47
|
+
# Returns the generated Splunk query.
|
48
|
+
def get_splunk_alert_query(hostname, service = nil)
|
49
|
+
# query for service alerts or host alerts, depending on which args were selected
|
50
|
+
query = "search index=nagios hostname=\"#{hostname}\""
|
51
|
+
if service.nil?
|
52
|
+
query += " state=\"DOWN\""
|
53
|
+
else
|
54
|
+
query += " service_name=\"#{service}\" (state=\"WARNING\" OR state=\"CRITICAL\" OR state=\"UNKNOWN\" OR state=\"DOWN\")"
|
55
|
+
end
|
56
|
+
query += "| fields #{@fields.join(',')}"
|
57
|
+
return query
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Queries Splunk to determine how frequently an alert has fired in a given period.
|
61
|
+
#
|
62
|
+
# hostname - The name of the alerting host we want to query.
|
63
|
+
# service - Optional service name that may be alerting.
|
64
|
+
# options - The options hash containing parameters for manipulatin the query.
|
65
|
+
#
|
66
|
+
# Returns a hash containing results from the search.
|
67
|
+
def get_alert_frequency(hostname, service = nil, options = {})
|
68
|
+
duration = options[:duration] ? options[:duration] : 7
|
69
|
+
|
70
|
+
max_results = options[:max_results] ? options[:max_results] : 10000
|
71
|
+
|
72
|
+
latest_time = options[:latest_time] ? options[:latest_time] :"now"
|
73
|
+
|
74
|
+
params = {
|
75
|
+
'exec_mode' => 'oneshot',
|
76
|
+
'earliest_time' => "-#{duration}d",
|
77
|
+
'latest_time' => latest_time,
|
78
|
+
'output_mode' => 'json',
|
79
|
+
'count' => max_results
|
80
|
+
}
|
81
|
+
|
82
|
+
params['search'] = get_splunk_alert_query(hostname, service)
|
83
|
+
|
84
|
+
json_response = query_splunk(params)
|
85
|
+
|
86
|
+
return if json_response.nil?
|
87
|
+
|
88
|
+
events_count = aggregate_splunk_events(json_response)
|
89
|
+
|
90
|
+
duration.to_i > 1 ? period = "days" : period = "day"
|
91
|
+
return {
|
92
|
+
:period => "#{duration} #{period}",
|
93
|
+
:service => service,
|
94
|
+
:hostname => hostname,
|
95
|
+
:events_count => events_count
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public: Performs the Splunk query.
|
100
|
+
#
|
101
|
+
# params - The parameters of the query.
|
102
|
+
#
|
103
|
+
# Returns the JSON results from the query.
|
104
|
+
def query_splunk(params)
|
105
|
+
http = Net::HTTP.new( @splunk_host, @splunk_port )
|
106
|
+
http.use_ssl = true
|
107
|
+
http.open_timeout = 1
|
108
|
+
http.read_timeout = 2
|
109
|
+
http.ssl_timeout = 1
|
110
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # don't validate the cert
|
111
|
+
request = Net::HTTP::Post.new( @splunk_uri )
|
112
|
+
request.basic_auth( @username, @password )
|
113
|
+
request.set_form_data( params )
|
114
|
+
response = http.request( request )
|
115
|
+
unless response.code.eql?( "200" )
|
116
|
+
logger.warn "Failed to submit search to Splunk."
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
|
120
|
+
begin
|
121
|
+
json = JSON.parse( response.body )
|
122
|
+
rescue Exception => e
|
123
|
+
# just warn; our formatter should handle this gracefully
|
124
|
+
logger.warn(e.message)
|
125
|
+
logger.warn("Failed to parse response from Splunk. Perhaps we got an empty result?")
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
|
129
|
+
return json
|
130
|
+
end
|
131
|
+
|
132
|
+
# Public: Aggregate the results from Splunk.
|
133
|
+
# Nagios logs an entry for each entity that got alerted; a single alert can
|
134
|
+
# result in many log entries so we need to account for this by creating a
|
135
|
+
# unique key to ensure we don't count duplicate log lines.
|
136
|
+
# Chances are we *won't* see a duplicate except in cases where an alert fires
|
137
|
+
# on the cusp of a minute (I've seen up to 4-second skew in timestamp for
|
138
|
+
# alert results returned from Splunk; this _can_ happen).
|
139
|
+
#
|
140
|
+
# json - The Splunk query results in JSON.
|
141
|
+
#
|
142
|
+
# Returns a string containing a message useful for outputting into a notification.
|
143
|
+
def aggregate_splunk_events(json)
|
144
|
+
events = {}
|
145
|
+
json.each do |alert|
|
146
|
+
state = alert['state']
|
147
|
+
event_key = @fields.map{|f| alert[f]}.join('-')
|
148
|
+
state_els = events.fetch(state, [])
|
149
|
+
events[state] = state_els << event_key
|
150
|
+
end
|
151
|
+
|
152
|
+
# get the alert counts by state
|
153
|
+
events_count = {}
|
154
|
+
events.map {|k,v| events_count[k] = v.uniq.count}
|
155
|
+
events_count.sort_by {|k,v| v}.reverse
|
156
|
+
return events_count
|
157
|
+
end
|
158
|
+
|
159
|
+
def format_splunk_results(events_count, hostname, duration, service=nil)
|
160
|
+
# speaka da English
|
161
|
+
duration.to_i > 1 ? period = "days" : period = "day"
|
162
|
+
msg = "HOST '#{hostname}' has experienced #{events_count.join(', ')} alerts"
|
163
|
+
msg += " for SERVICE '#{service}'" unless service.nil?
|
164
|
+
msg += " in the last #{duration} #{period}."
|
165
|
+
return msg
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# Query Splunk with arbitrary search criteria
|
6
|
+
|
7
|
+
module NagiosHerald
|
8
|
+
module Helpers
|
9
|
+
class SplunkQuery
|
10
|
+
#include NagiosHerald::Logging
|
11
|
+
|
12
|
+
# Public: Initialize a new SplunkQuery object.
|
13
|
+
#
|
14
|
+
# query - A string representing the query to send to Splunk.
|
15
|
+
# index - Optional index to specify (else Splunk defaults to all indexes
|
16
|
+
# available to the authenticated user).
|
17
|
+
# output - The output format we'd like (i.e. csv, json, xml); defaults
|
18
|
+
# to json.
|
19
|
+
#
|
20
|
+
# Example:
|
21
|
+
#
|
22
|
+
# splunk_query = NagiosHerald::Helpers::SplunkQuery.new('sourcetype=perf_log page=index.html')
|
23
|
+
# splunk_query = NagiosHerald::Helpers::SplunkQuery.new('transaction_state=paid', {:index => 'get_paid'})
|
24
|
+
# splunk_query = NagiosHerald::Helpers::SplunkQuery.new('source=nagios-herald.log alert_type=host', {:output => 'csv'})
|
25
|
+
#
|
26
|
+
# Returns a new SplunkQuery object.
|
27
|
+
def initialize(query, options={})
|
28
|
+
@splunk_query = query
|
29
|
+
@splunk_index = options[:index] ? options[:index] : nil
|
30
|
+
@splunk_output = options[:output] ? options[:output] : 'json'
|
31
|
+
|
32
|
+
# Pull the Splunk URI, username, and password from the config.
|
33
|
+
splunk_url = Config.config['splunk']['url']
|
34
|
+
@splunk_username = Config.config['splunk']['username']
|
35
|
+
@splunk_password = Config.config['splunk']['password']
|
36
|
+
|
37
|
+
# Parse the URI.
|
38
|
+
uri = URI.parse(splunk_url)
|
39
|
+
@splunk_host = uri.host
|
40
|
+
@splunk_port = uri.port
|
41
|
+
@splunk_uri = uri.request_uri
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: Generate the parameters for the Splunk query.
|
45
|
+
#
|
46
|
+
# Example:
|
47
|
+
#
|
48
|
+
# parameters = splunk_query.parameters
|
49
|
+
#
|
50
|
+
# Returns the Splunk query parameters.
|
51
|
+
def parameters
|
52
|
+
# Earliest time we should look for events; defaults to 7 days ago.
|
53
|
+
earliest_time = Config.config['splunk']['earliest_time'] ?
|
54
|
+
Config.config['splunk']['earliest_time'] :
|
55
|
+
'7d'
|
56
|
+
|
57
|
+
# Latest time we should look for events; defaults to now.
|
58
|
+
latest_time = Config.config['splunk']['latest_time'] ?
|
59
|
+
Config.config['splunk']['latest_time'] :
|
60
|
+
'now'
|
61
|
+
|
62
|
+
# Maximum results returned; defaults to 100.
|
63
|
+
max_results = Config.config['splunk']['max_results'] ?
|
64
|
+
Config.config['splunk']['max_results'] :
|
65
|
+
100
|
66
|
+
|
67
|
+
params = {
|
68
|
+
'exec_mode' => 'oneshot',
|
69
|
+
'earliest_time' => "-#{earliest_time}",
|
70
|
+
'latest_time' => latest_time,
|
71
|
+
'output_mode' => @splunk_output,
|
72
|
+
'count' => max_results
|
73
|
+
}
|
74
|
+
if @splunk_index.nil?
|
75
|
+
params['search'] = "search #{@splunk_query}"
|
76
|
+
else
|
77
|
+
params['search'] = "search index=#{@splunk_index} " + @splunk_query
|
78
|
+
end
|
79
|
+
|
80
|
+
params
|
81
|
+
end
|
82
|
+
|
83
|
+
# Public: Queries Splunk.
|
84
|
+
#
|
85
|
+
# Example:
|
86
|
+
#
|
87
|
+
# results = splunk_query.query
|
88
|
+
#
|
89
|
+
# Returns the results of the query in the requested format, nil otherwise.
|
90
|
+
def query
|
91
|
+
http = Net::HTTP.new( @splunk_host, @splunk_port )
|
92
|
+
http.use_ssl = true
|
93
|
+
http.open_timeout = 1
|
94
|
+
http.read_timeout = 2
|
95
|
+
http.ssl_timeout = 1
|
96
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # don't validate the cert
|
97
|
+
request = Net::HTTP::Post.new( @splunk_uri )
|
98
|
+
request.basic_auth( @splunk_username, @splunk_password )
|
99
|
+
request.set_form_data( parameters )
|
100
|
+
begin
|
101
|
+
response = http.request( request )
|
102
|
+
rescue Exception => e
|
103
|
+
logger.warn "Failed to send request: #{e.message}"
|
104
|
+
return nil
|
105
|
+
end
|
106
|
+
|
107
|
+
if response.code.eql?( "200" )
|
108
|
+
response.body
|
109
|
+
else
|
110
|
+
logger.warn "Splunk query failed with HTTP #{response.code}: #{response.message}"
|
111
|
+
logger.warn response.body
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
# TODO: don't assume the MIME type is image/png; provide a mechanism for ensuring a standard image size
|
5
|
+
module NagiosHerald
|
6
|
+
module Helpers
|
7
|
+
class UrlImage
|
8
|
+
|
9
|
+
# Public: Requests an image by its URI.
|
10
|
+
#
|
11
|
+
# uri - The URI of the image resource.
|
12
|
+
#
|
13
|
+
# Returns the content of the image.
|
14
|
+
def self.get_image( uri )
|
15
|
+
graph = Net::HTTP.get( URI.parse( uri ) )
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Writes te given content to a file name.
|
19
|
+
#
|
20
|
+
# file_name - The name of the file to write.
|
21
|
+
# content - Arbitrary content to write into the file.
|
22
|
+
#
|
23
|
+
# Returns true if successful, false otherwise.
|
24
|
+
def self.write_image( file_name, content )
|
25
|
+
File.delete( file_name ) if File.exists?( file_name ) # remove any pre-existing versions
|
26
|
+
written_size = File.open( file_name, 'w' ) { |f| f.write( content ) }
|
27
|
+
if written_size == content.size
|
28
|
+
return written_size > 0 # why aren't we just returning true?
|
29
|
+
else
|
30
|
+
false # oops...
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Convert the URI to a useful name to be used in the image file name.
|
35
|
+
# Removes the transport type and query characters.
|
36
|
+
#
|
37
|
+
# uri - The URI to be converted.
|
38
|
+
#
|
39
|
+
# Returns the converted URI.
|
40
|
+
# FIXME: This doesn't account for HTTPS URIs.
|
41
|
+
def self.convert_uri(uri)
|
42
|
+
converted_uri = uri.gsub("http:\/\/", "")
|
43
|
+
converted_uri.gsub!(/(\/|=|\?|&)/, "_") # such a hack...
|
44
|
+
converted_uri.gsub!(/_+/, "_") # de-dupe underscores
|
45
|
+
return converted_uri
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Downloads an image and writes it to a file.
|
49
|
+
#
|
50
|
+
# uri - The URI of the image resource.
|
51
|
+
# path - The destination file for the image content.
|
52
|
+
#
|
53
|
+
# Returns nothing.
|
54
|
+
def self.download_image(uri, path)
|
55
|
+
graph = get_image( uri )
|
56
|
+
# only push the image path into the array if we successfully create it
|
57
|
+
write_image( path, graph ) ? path : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# To be honest, I don't recall this method's purpose.
|
61
|
+
# It's only called in the ``bin/get_graph`` script. May be dead code.
|
62
|
+
# TODO: Determine if this is still necessary.
|
63
|
+
def self.download_images( uris, path )
|
64
|
+
path = path.sub(/\/$/, "") # strip the trailing slash (if it exists) so the components of image_name are clear
|
65
|
+
image_paths = []
|
66
|
+
uris.each do |uri|
|
67
|
+
converted_uri = convert_uri(uri)
|
68
|
+
image_path = "#{path}/#{converted_uri}.png"
|
69
|
+
# only push the image path into the array if we successfully create it
|
70
|
+
image_paths.push(image_path) if download_image(uri, image_path)
|
71
|
+
end
|
72
|
+
image_paths
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# looks like this code came from
|
2
|
+
# http://stackoverflow.com/questions/917566/ruby-share-logger-instance-among-module-classes
|
3
|
+
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
module NagiosHerald
|
7
|
+
module Logging
|
8
|
+
|
9
|
+
def logger
|
10
|
+
@logger ||= Logging.logger_for(self.class.name)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Use a hash class-ivar to cache a unique Logger per class:
|
14
|
+
# "ivar" = fancy term for "instance variable"
|
15
|
+
# do we want this? or a global logger?
|
16
|
+
@loggers = {}
|
17
|
+
|
18
|
+
extend self
|
19
|
+
|
20
|
+
# effectively sets the progname
|
21
|
+
def logger_for(classname)
|
22
|
+
@loggers[classname] ||= configure_logger_for(classname)
|
23
|
+
end
|
24
|
+
|
25
|
+
# instantiates a Logger instance
|
26
|
+
# default to STDOUT
|
27
|
+
def configure_logger_for(classname)
|
28
|
+
logfile = Config.config['logfile'] ? Config.config['logfile'] : STDOUT
|
29
|
+
if logfile.eql?("STDOUT")
|
30
|
+
logfile = STDOUT
|
31
|
+
end
|
32
|
+
begin
|
33
|
+
logger = Logger.new(logfile)
|
34
|
+
rescue Exception => e
|
35
|
+
puts "Failed to open #{logfile} for writing: #{e.message}"
|
36
|
+
puts "Defaulting to STDOUT"
|
37
|
+
logger = Logger.new(STDOUT)
|
38
|
+
end
|
39
|
+
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
|
40
|
+
logger.progname = "#{File.basename $0} (#{classname})"
|
41
|
+
logger.formatter = proc { |severity, datetime, progname, msg|
|
42
|
+
"[#{datetime}] #{severity} -- #{progname}: #{msg}\n"
|
43
|
+
}
|
44
|
+
logger
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Load all Message classes similar to how messageLoader does its thang.
|
2
|
+
module NagiosHerald
|
3
|
+
class MessageLoader
|
4
|
+
include NagiosHerald::Logging
|
5
|
+
|
6
|
+
attr_accessor :message_path
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
# TODO: add support for @options.message_path
|
10
|
+
@message_path = File.expand_path("messages", File.dirname(__FILE__))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: Enumerate the available message class files.
|
14
|
+
#
|
15
|
+
# Returns an array of the class files' absolute paths
|
16
|
+
def enum_message_class_files(message_path)
|
17
|
+
message_class_files = Dir.glob(File.expand_path("*.rb", message_path))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Return an array of class files' paths.
|
21
|
+
def message_class_files
|
22
|
+
@message_class_files ||= enum_message_class_files(@message_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Load the messages into the namespace.
|
26
|
+
# A message can then easily be instantiated later.
|
27
|
+
def load_messages
|
28
|
+
if message_class_files.empty?
|
29
|
+
logger.fatal "#{$0}: No messages were found in '#{@message_path}'"
|
30
|
+
exit 1
|
31
|
+
else
|
32
|
+
message_class_files.each do |message_class_file|
|
33
|
+
Kernel.load message_class_file
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'nagios-herald/message_loader'
|
2
|
+
require 'nagios-herald/logging'
|
3
|
+
require 'nagios-herald/util'
|
4
|
+
|
5
|
+
# Message objects know best about how to generate and send messages
|
6
|
+
# The Base class defines @content and @recipients variables as all messages probably
|
7
|
+
# have some notion of these constructs.
|
8
|
+
module NagiosHerald
|
9
|
+
class Message
|
10
|
+
include NagiosHerald::Logging
|
11
|
+
include NagiosHerald::Util
|
12
|
+
|
13
|
+
attr_accessor :content
|
14
|
+
attr_accessor :recipients
|
15
|
+
|
16
|
+
def initialize(recipients, options)
|
17
|
+
@no_send = options[:no_send]
|
18
|
+
# TODO: instead of passing this in via the subclass, let's set it via message.recipients
|
19
|
+
@recipients = recipients
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Defines what is required to send a message.
|
23
|
+
# The message type knows best how this is done. Override the #send method
|
24
|
+
# in your message subclass.
|
25
|
+
def send
|
26
|
+
raise Exception, "#{self.to_s}: You must override #send"
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.message_types
|
30
|
+
@@message_types ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: When subclassed message types are instantiated, add them to the @@message_types hash.
|
34
|
+
# The key is the downcased and snake_cased name of the class file (i.e. email);
|
35
|
+
# the value is the actual class (i.e. Email) so that we can easily
|
36
|
+
# instantiate message types when we know the message type name.
|
37
|
+
# Learned this pattern thanks to the folks at Chef and @jonlives.
|
38
|
+
# See https://github.com/opscode/chef/blob/11-stable/lib/chef/knife.rb#L79#L83
|
39
|
+
#
|
40
|
+
# Returns the message_types hash.
|
41
|
+
def self.inherited(subclass)
|
42
|
+
subclass_base_name = subclass.name.split('::').last
|
43
|
+
if subclass_base_name == subclass_base_name.upcase
|
44
|
+
# we've got an all upper case class name (probably an acronym like IRC); just downcase the whole thing
|
45
|
+
subclass_base_name.downcase!
|
46
|
+
message_types[subclass_base_name] = subclass
|
47
|
+
else
|
48
|
+
subclass_base_name.gsub!(/[A-Z]/) { |s| "_" + s } # replace uppercase with underscore and lowercase
|
49
|
+
subclass_base_name.downcase!
|
50
|
+
subclass_base_name.sub!(/^_/, "") # strip the leading underscore
|
51
|
+
message_types[subclass_base_name] = subclass
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'nagios-herald/messages/base'
|
2
|
+
require 'mail'
|
3
|
+
|
4
|
+
module NagiosHerald
|
5
|
+
class Message
|
6
|
+
class Email < Message
|
7
|
+
|
8
|
+
attr_accessor :attachments
|
9
|
+
attr_accessor :html
|
10
|
+
attr_accessor :subject
|
11
|
+
attr_accessor :text
|
12
|
+
|
13
|
+
# Public: Initializes a new Message::Email object.
|
14
|
+
#
|
15
|
+
# recipients - A list of recipients for this message.
|
16
|
+
# options - The options hash from Executor.
|
17
|
+
# FIXME: Is that ^^ necessary now with Config.config available?
|
18
|
+
#
|
19
|
+
# Returns a new Message::Email object.
|
20
|
+
def initialize(recipients, options = {})
|
21
|
+
@replyto = options[:replyto]
|
22
|
+
@subject = ""
|
23
|
+
@text = ""
|
24
|
+
@html = ""
|
25
|
+
@attachments = []
|
26
|
+
super(recipients, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# this is a list of Mail::Part
|
30
|
+
# => #<Mail::Part:19564000, Multipart: false, Headers: <Content-Type: ; filename="Rakefile">, <Content-Transfer-Encoding: binary>, <Content-Disposition: attachment; filename="Rakefile">, <Content-ID: <530e1814464a9_3305aaef88979a2@blahblahbl.blah.blah.blah.mail>>>
|
31
|
+
# Public: Updates HTML content so that attachments are referenced via the 'cid:' URI scheme and neatly embedded in the body
|
32
|
+
# of a(n email) message.
|
33
|
+
#
|
34
|
+
# attachments - The list of attachments defined in the formatter content hash.
|
35
|
+
#
|
36
|
+
# Returns the updated HTML content.
|
37
|
+
def inline_body_with_attachments(attachments)
|
38
|
+
inline_html = @html
|
39
|
+
attachments.each do |attachment|
|
40
|
+
if (inline_html =~ /#{attachment.filename}/)
|
41
|
+
inline_html = inline_html.sub(attachment.filename, "cid:#{attachment.cid}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
inline_html
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Generates the text portion of the content hash.
|
48
|
+
#
|
49
|
+
# Returns the full text portion of the content hash.
|
50
|
+
def curate_text
|
51
|
+
@text += self.content[:text][:host_info] unless self.content[:text][:host_info].empty?
|
52
|
+
@text += self.content[:text][:state_info] unless self.content[:text][:state_info].empty?
|
53
|
+
@text += self.content[:text][:additional_info] unless self.content[:text][:additional_info].empty?
|
54
|
+
@text += self.content[:text][:action_url] unless self.content[:text][:action_url].empty?
|
55
|
+
@text += self.content[:text][:notes] unless self.content[:text][:notes].empty?
|
56
|
+
@text += self.content[:text][:additional_details] unless self.content[:text][:additional_details].empty?
|
57
|
+
@text += self.content[:text][:recipients_email_link] unless self.content[:text][:recipients_email_link].empty?
|
58
|
+
@text += self.content[:text][:notification_info] unless self.content[:text][:notification_info].empty?
|
59
|
+
@text += self.content[:text][:alert_ack_url] unless self.content[:text][:alert_ack_url].empty?
|
60
|
+
# Hack to ensure we get ack info, if it's populated.
|
61
|
+
@text += self.content[:text][:ack_info] if self.content[:text][:ack_info] and !self.content[:text][:ack_info].empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: Generates the HTML portion of the content hash.
|
65
|
+
#
|
66
|
+
# Returns the full HTML portion of the content hash.
|
67
|
+
def curate_html
|
68
|
+
@html += self.content[:html][:host_info] unless self.content[:html][:host_info].empty?
|
69
|
+
@html += self.content[:html][:state_info] unless self.content[:html][:state_info].empty?
|
70
|
+
@html += self.content[:html][:additional_info] unless self.content[:html][:additional_info].empty?
|
71
|
+
@html += self.content[:html][:action_url] unless self.content[:html][:action_url].empty?
|
72
|
+
@html += self.content[:html][:notes] unless self.content[:html][:notes].empty?
|
73
|
+
@html += self.content[:html][:additional_details] unless self.content[:html][:additional_details].empty?
|
74
|
+
@html += self.content[:html][:recipients_email_link] unless self.content[:html][:recipients_email_link].empty?
|
75
|
+
@html += self.content[:html][:notification_info] unless self.content[:html][:notification_info].empty?
|
76
|
+
@html += self.content[:html][:alert_ack_url] unless self.content[:html][:alert_ack_url].empty?
|
77
|
+
# Hack to ensure we get ack info, if it's populated.
|
78
|
+
@html += self.content[:html][:ack_info] if self.content[:html][:ack_info] and !self.content[:html][:ack_info].empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Public: Prints the subject, text and HTML content to the terminal.
|
82
|
+
# Useful for debugging.
|
83
|
+
#
|
84
|
+
# Returns nothing.
|
85
|
+
def print
|
86
|
+
puts "------------------"
|
87
|
+
puts "Subject : #{@subject}"
|
88
|
+
puts "------------------"
|
89
|
+
puts @text if !@text.empty?
|
90
|
+
puts @html if !@html.empty?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Public: Builds the email message.
|
94
|
+
#
|
95
|
+
# Returns the mail object.
|
96
|
+
def build_message
|
97
|
+
curate_text
|
98
|
+
curate_html
|
99
|
+
if @no_send
|
100
|
+
self.print
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
@subject = self.content[:subject]
|
105
|
+
mail = Mail.new({
|
106
|
+
:from => @replyto,
|
107
|
+
:to => @recipients,
|
108
|
+
:subject => @subject,
|
109
|
+
:content_type => 'multipart/alternative'
|
110
|
+
})
|
111
|
+
|
112
|
+
text_content = @text
|
113
|
+
text_part = Mail::Part.new do
|
114
|
+
body text_content
|
115
|
+
end
|
116
|
+
|
117
|
+
mail.add_part(text_part)
|
118
|
+
|
119
|
+
html_part = Mail::Part.new do
|
120
|
+
content_type 'multipart/related;'
|
121
|
+
end
|
122
|
+
|
123
|
+
# Load the attachments
|
124
|
+
@attachments = self.content[:attachments]
|
125
|
+
@attachments.each do |attachment|
|
126
|
+
html_part.attachments[attachment] = File.read(attachment)
|
127
|
+
end
|
128
|
+
|
129
|
+
if @html != ""
|
130
|
+
# Inline the attachment if need be
|
131
|
+
inline_html = inline_body_with_attachments(html_part.attachments)
|
132
|
+
html_content_part = Mail::Part.new do
|
133
|
+
content_type 'text/html; charset=UTF-8'
|
134
|
+
body inline_html
|
135
|
+
end
|
136
|
+
html_part.add_part(html_content_part)
|
137
|
+
end
|
138
|
+
|
139
|
+
mail.add_part(html_part)
|
140
|
+
mail
|
141
|
+
end
|
142
|
+
|
143
|
+
def send
|
144
|
+
mail = self.build_message
|
145
|
+
mail.deliver!
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|