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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG.md +11 -0
  5. data/CONTRIBUTING.md +28 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE +21 -0
  8. data/README.md +94 -0
  9. data/Rakefile +9 -0
  10. data/bin/draw_stack_bars +76 -0
  11. data/bin/dump_nagios_env.sh +25 -0
  12. data/bin/get_ganglia_graph +82 -0
  13. data/bin/get_graph +50 -0
  14. data/bin/get_graphite_graph +58 -0
  15. data/bin/nagios-herald +6 -0
  16. data/bin/splunk_alert_frequency +54 -0
  17. data/contrib/nrpe-plugins/check_cpu_stats.sh +186 -0
  18. data/contrib/nrpe-plugins/check_disk.sh +34 -0
  19. data/contrib/nrpe-plugins/check_mem.pl +181 -0
  20. data/contrib/nrpe-plugins/nrpe-plugin-examples.md +11 -0
  21. data/docs/config.md +62 -0
  22. data/docs/example_alerts.md +48 -0
  23. data/docs/formatters.md +180 -0
  24. data/docs/helpers.md +12 -0
  25. data/docs/images/cpu_no_context.png +0 -0
  26. data/docs/images/cpu_with_context.png +0 -0
  27. data/docs/images/disk_space_no_context.png +0 -0
  28. data/docs/images/disk_space_with_context.png +0 -0
  29. data/docs/images/memory_high_no_context.png +0 -0
  30. data/docs/images/memory_high_with_context.png +0 -0
  31. data/docs/images/nagios-herald-formatter-content-example.png +0 -0
  32. data/docs/images/nagios-herald.png +0 -0
  33. data/docs/images/stack-bars.png +0 -0
  34. data/docs/images/vanilla-nagios.png +0 -0
  35. data/docs/messages.md +16 -0
  36. data/docs/nagios-config.md +74 -0
  37. data/docs/tools.md +79 -0
  38. data/etc/config.yml.example +14 -0
  39. data/etc/readme.md +2 -0
  40. data/lib/nagios-herald/config.rb +25 -0
  41. data/lib/nagios-herald/executor.rb +265 -0
  42. data/lib/nagios-herald/formatter_loader.rb +82 -0
  43. data/lib/nagios-herald/formatters/base.rb +524 -0
  44. data/lib/nagios-herald/formatters/check_cpu.rb +71 -0
  45. data/lib/nagios-herald/formatters/check_disk.rb +143 -0
  46. data/lib/nagios-herald/formatters/check_logstash.rb +155 -0
  47. data/lib/nagios-herald/formatters/check_memory.rb +42 -0
  48. data/lib/nagios-herald/formatters/example.rb +19 -0
  49. data/lib/nagios-herald/formatters.rb +1 -0
  50. data/lib/nagios-herald/helpers/ganglia_graph.rb +99 -0
  51. data/lib/nagios-herald/helpers/graphite_graph.rb +85 -0
  52. data/lib/nagios-herald/helpers/logstash_query.rb +125 -0
  53. data/lib/nagios-herald/helpers/splunk_alert_frequency.rb +170 -0
  54. data/lib/nagios-herald/helpers/splunk_query.rb +119 -0
  55. data/lib/nagios-herald/helpers/url_image.rb +76 -0
  56. data/lib/nagios-herald/helpers.rb +5 -0
  57. data/lib/nagios-herald/logging.rb +48 -0
  58. data/lib/nagios-herald/message_loader.rb +40 -0
  59. data/lib/nagios-herald/messages/base.rb +56 -0
  60. data/lib/nagios-herald/messages/email.rb +150 -0
  61. data/lib/nagios-herald/messages/irc.rb +58 -0
  62. data/lib/nagios-herald/messages/pager.rb +75 -0
  63. data/lib/nagios-herald/messages.rb +3 -0
  64. data/lib/nagios-herald/test_helpers/base_test_case.rb +82 -0
  65. data/lib/nagios-herald/util.rb +45 -0
  66. data/lib/nagios-herald/version.rb +3 -0
  67. data/lib/nagios-herald.rb +7 -0
  68. data/lib/stackbars/__init__.py +0 -0
  69. data/lib/stackbars/chart_utils.py +25 -0
  70. data/lib/stackbars/grouped_stackbars.py +97 -0
  71. data/lib/stackbars/pilfonts/Tahoma.ttf +0 -0
  72. data/lib/stackbars/pilfonts/aerial.ttf +0 -0
  73. data/lib/stackbars/pilfonts/arial_black.ttf +0 -0
  74. data/lib/stackbars/stackbar.py +100 -0
  75. data/nagios-herald.gemspec +33 -0
  76. data/test/env_files/check_cpu_idle.CRITICAL +199 -0
  77. data/test/env_files/check_cpu_iowait.WARNING +199 -0
  78. data/test/env_files/check_disk.CRITICAL +197 -0
  79. data/test/env_files/check_disk.CRITICAL_ICINGA +197 -0
  80. data/test/env_files/check_disk.RECOVERY +197 -0
  81. data/test/env_files/check_memory.CRITICAL +197 -0
  82. data/test/env_files/nagios_vars.EXAMPLE +197 -0
  83. data/test/unit/test_config.rb +31 -0
  84. data/test/unit/test_executor.rb +65 -0
  85. data/test/unit/test_formatter_base.rb +131 -0
  86. data/test/unit/test_formatter_check_cpu_idle_critical.rb +135 -0
  87. data/test/unit/test_formatter_check_memory.rb +135 -0
  88. data/test/unit/test_icinga_variables.rb +31 -0
  89. data/test/unit/test_logging.rb +35 -0
  90. data/test/unit/test_message_email.rb +69 -0
  91. data/test/unit/test_message_pager.rb +69 -0
  92. 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,5 @@
1
+ require 'nagios-herald/helpers/ganglia_graph'
2
+ require 'nagios-herald/helpers/graphite_graph'
3
+ require 'nagios-herald/helpers/url_image'
4
+ require 'nagios-herald/helpers/splunk_alert_frequency'
5
+ require 'nagios-herald/helpers/logstash_query'
@@ -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