flapjack 1.0.0rc3 → 1.0.0rc5
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 +4 -4
- data/.gitignore +1 -2
- data/.ruby-version +1 -0
- data/CHANGELOG.md +20 -0
- data/CONTRIBUTING.md +2 -2
- data/Gemfile +1 -1
- data/README.md +6 -16
- data/build.sh +13 -1
- data/etc/flapjack_config.yaml.example +98 -12
- data/features/cli.feature +8 -8
- data/features/cli_flapjack-nagios-receiver.feature +29 -37
- data/features/cli_flapper.feature +24 -12
- data/features/cli_simulate-failed-check.feature +2 -2
- data/features/notifications.feature +18 -1
- data/features/steps/cli_steps.rb +2 -2
- data/features/steps/notifications_steps.rb +71 -0
- data/features/support/env.rb +7 -6
- data/flapjack.gemspec +3 -1
- data/lib/flapjack/cli/flapper.rb +74 -25
- data/lib/flapjack/cli/import.rb +3 -4
- data/lib/flapjack/cli/maintenance.rb +182 -0
- data/lib/flapjack/cli/receiver.rb +110 -121
- data/lib/flapjack/cli/server.rb +30 -26
- data/lib/flapjack/cli/simulate.rb +2 -3
- data/lib/flapjack/data/contact.rb +1 -1
- data/lib/flapjack/data/entity.rb +425 -32
- data/lib/flapjack/data/entity_check.rb +212 -14
- data/lib/flapjack/data/event.rb +1 -1
- data/lib/flapjack/gateways/aws_sns.rb +134 -0
- data/lib/flapjack/gateways/aws_sns/alert.text.erb +5 -0
- data/lib/flapjack/gateways/aws_sns/rollup.text.erb +2 -0
- data/lib/flapjack/gateways/jabber.rb +2 -2
- data/lib/flapjack/gateways/jsonapi/check_methods.rb +1 -1
- data/lib/flapjack/gateways/jsonapi/contact_methods.rb +1 -1
- data/lib/flapjack/gateways/jsonapi/entity_methods.rb +15 -1
- data/lib/flapjack/gateways/jsonapi/metrics_methods.rb +4 -3
- data/lib/flapjack/gateways/jsonapi/report_methods.rb +1 -1
- data/lib/flapjack/gateways/web.rb +35 -16
- data/lib/flapjack/gateways/web/public/css/tablesort.css +0 -16
- data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +1 -1
- data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.widgets.js +0 -45
- data/lib/flapjack/gateways/web/public/js/modules/contact.js +2 -2
- data/lib/flapjack/gateways/web/public/js/modules/entity.js +2 -2
- data/lib/flapjack/gateways/web/public/js/modules/medium.js +4 -4
- data/lib/flapjack/gateways/web/public/js/self_stats.js +1 -1
- data/lib/flapjack/gateways/web/views/check.html.erb +10 -10
- data/lib/flapjack/gateways/web/views/checks.html.erb +1 -1
- data/lib/flapjack/gateways/web/views/contact.html.erb +5 -1
- data/lib/flapjack/gateways/web/views/edit_contacts.html.erb +3 -4
- data/lib/flapjack/gateways/web/views/entities.html.erb +1 -1
- data/lib/flapjack/gateways/web/views/index.html.erb +2 -2
- data/lib/flapjack/gateways/web/views/layout.erb +3 -3
- data/lib/flapjack/gateways/web/views/self_stats.html.erb +5 -6
- data/lib/flapjack/notifier.rb +4 -1
- data/lib/flapjack/patches.rb +8 -2
- data/lib/flapjack/pikelet.rb +3 -1
- data/lib/flapjack/version.rb +1 -1
- data/libexec/httpbroker.go +1 -1
- data/spec/lib/flapjack/coordinator_spec.rb +3 -3
- data/spec/lib/flapjack/data/contact_spec.rb +2 -2
- data/spec/lib/flapjack/data/entity_check_spec.rb +805 -53
- data/spec/lib/flapjack/data/entity_spec.rb +661 -0
- data/spec/lib/flapjack/gateways/aws_sns_spec.rb +123 -0
- data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -1
- data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +1 -1
- data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +2 -2
- data/spec/lib/flapjack/gateways/pagerduty_spec.rb +1 -1
- data/spec/lib/flapjack/gateways/web_spec.rb +11 -11
- data/spec/support/profile_all_formatter.rb +10 -10
- data/spec/support/uncolored_doc_formatter.rb +66 -4
- data/src/flapjack/event.go +1 -1
- data/tasks/benchmarks.rake +24 -20
- data/tasks/entities.rake +148 -0
- data/tmp/dummy_contacts.json +43 -0
- data/tmp/dummy_entities.json +37 -1
- metadata +43 -7
- data/tmp/test_entities.json +0 -1
@@ -7,6 +7,8 @@ require 'flapjack/patches'
|
|
7
7
|
require 'flapjack/data/contact'
|
8
8
|
require 'flapjack/data/event'
|
9
9
|
require 'flapjack/data/entity'
|
10
|
+
#FIXME: Require chronic_duration in the correct place
|
11
|
+
require 'chronic_duration'
|
10
12
|
|
11
13
|
# TODO might want to split the class methods out to a separate class, DAO pattern
|
12
14
|
# ( http://en.wikipedia.org/wiki/Data_access_object ).
|
@@ -61,17 +63,17 @@ module Flapjack
|
|
61
63
|
self.new(entity, check, :logger => logger, :redis => redis)
|
62
64
|
end
|
63
65
|
|
64
|
-
def self.
|
66
|
+
def self.find_current_for_entity_name(entity_name, options = {})
|
65
67
|
raise "Redis connection not set" unless redis = options[:redis]
|
66
68
|
redis.zrange("current_checks:#{entity_name}", 0, -1)
|
67
69
|
end
|
68
70
|
|
69
|
-
def self.
|
71
|
+
def self.find_current(options = {})
|
70
72
|
raise "Redis connection not set" unless redis = options[:redis]
|
71
|
-
self.conflate_to_keys(self.
|
73
|
+
self.conflate_to_keys(self.find_current_by_entity(:redis => redis))
|
72
74
|
end
|
73
75
|
|
74
|
-
def self.
|
76
|
+
def self.find_current_by_entity(options = {})
|
75
77
|
raise "Redis connection not set" unless redis = options[:redis]
|
76
78
|
d = {}
|
77
79
|
redis.zrange("current_entities", 0, -1).each {|entity|
|
@@ -80,19 +82,19 @@ module Flapjack
|
|
80
82
|
d
|
81
83
|
end
|
82
84
|
|
83
|
-
def self.
|
85
|
+
def self.count_current(options = {})
|
84
86
|
raise "Redis connection not set" unless redis = options[:redis]
|
85
87
|
redis.zrange("current_entities", 0, -1).inject(0) {|memo, entity|
|
86
88
|
memo + redis.zcount("current_checks:#{entity}", '-inf', '+inf')
|
87
89
|
}
|
88
90
|
end
|
89
91
|
|
90
|
-
def self.
|
92
|
+
def self.find_current_failing(options = {})
|
91
93
|
raise "Redis connection not set" unless redis = options[:redis]
|
92
|
-
self.conflate_to_keys(self.
|
94
|
+
self.conflate_to_keys(self.find_current_failing_by_entity(:redis => redis))
|
93
95
|
end
|
94
96
|
|
95
|
-
def self.
|
97
|
+
def self.find_current_failing_by_entity(options = {})
|
96
98
|
raise "Redis connection not set" unless redis = options[:redis]
|
97
99
|
redis.zrange("failed_checks", 0, -1).inject({}) do |memo, key|
|
98
100
|
entity, check = key.split(':', 2)
|
@@ -104,7 +106,7 @@ module Flapjack
|
|
104
106
|
end
|
105
107
|
end
|
106
108
|
|
107
|
-
def self.
|
109
|
+
def self.count_current_failing(options = {})
|
108
110
|
raise "Redis connection not set" unless redis = options[:redis]
|
109
111
|
redis.zrange("failed_checks", 0, -1).count do |key|
|
110
112
|
entity, check = key.split(':', 2)
|
@@ -132,6 +134,201 @@ module Flapjack
|
|
132
134
|
result
|
133
135
|
end
|
134
136
|
|
137
|
+
def self.find_maintenance(options = {})
|
138
|
+
raise "Redis connection not set" unless redis = options[:redis]
|
139
|
+
type = options[:type]
|
140
|
+
keys = redis.keys("*:#{type}_maintenances")
|
141
|
+
keys.flat_map { |k|
|
142
|
+
entity = k.split(':')[0]
|
143
|
+
check = k.split(':')[1]
|
144
|
+
ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
|
145
|
+
|
146
|
+
# Only return entries which match what was passed in
|
147
|
+
case
|
148
|
+
when options[:state] && options[:state] != ec.state
|
149
|
+
nil
|
150
|
+
when options[:entity] && !Regexp.new(options[:entity]).match(entity)
|
151
|
+
nil
|
152
|
+
when options[:check] && !Regexp.new(options[:check]).match(check)
|
153
|
+
nil
|
154
|
+
else
|
155
|
+
windows = ec.maintenances(nil, nil, type.to_sym => true)
|
156
|
+
windows.map { |window|
|
157
|
+
entry = { :entity => entity,
|
158
|
+
:check => check,
|
159
|
+
:state => ec.state
|
160
|
+
}
|
161
|
+
if (options[:reason].nil? || Regexp.new(options[:reason]).match(window[:summary])) &&
|
162
|
+
check_maintenance_timestamp(options[:started], window[:start_time]) &&
|
163
|
+
check_maintenance_timestamp(options[:finishing], window[:end_time]) &&
|
164
|
+
check_maintenance_interval(options[:duration], window[:duration])
|
165
|
+
entry.merge!(window)
|
166
|
+
end
|
167
|
+
}.compact
|
168
|
+
end
|
169
|
+
}.compact
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.delete_maintenance(options = {})
|
173
|
+
raise "Redis connection not set" unless redis = options[:redis]
|
174
|
+
entries = find_maintenance(options)
|
175
|
+
# Try to delete all entries passed in, but return false if any entries failed
|
176
|
+
errors = {}
|
177
|
+
entries.each do |entry|
|
178
|
+
identifier = "#{entry[:entity]}:#{entry[:check]}:#{entry[:start_time]}"
|
179
|
+
if entry[:end_time] < Time.now.to_i
|
180
|
+
errors[identifier] = "Maintenance can't be deleted as it finished in the past"
|
181
|
+
else
|
182
|
+
entity = entry[:entity]
|
183
|
+
check = entry[:check]
|
184
|
+
|
185
|
+
ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
|
186
|
+
success = case options[:type]
|
187
|
+
when 'scheduled'
|
188
|
+
ec.end_scheduled_maintenance(entry[:start_time])
|
189
|
+
when 'unscheduled'
|
190
|
+
ec.end_unscheduled_maintenance(entry[:end_time])
|
191
|
+
end
|
192
|
+
errors[identifier] = "The following entry failed to delete: #{entry}" unless success
|
193
|
+
end
|
194
|
+
end
|
195
|
+
errors
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.create_maintenance(options = {})
|
199
|
+
raise "Redis connection not set" unless redis = options[:redis]
|
200
|
+
errors = {}
|
201
|
+
entities = options[:entity].is_a?(String) ? options[:entity].split(',') : options[:entity]
|
202
|
+
checks = options[:check].is_a?(String) ? options[:check].split(',') : options[:check]
|
203
|
+
entities.each do |entity|
|
204
|
+
# Create the entity if it doesn't exist, so we can schedule maintenance against it
|
205
|
+
Flapjack::Data::Entity.find_by_name(entity, :redis => redis, :create => true)
|
206
|
+
checks.each do |check|
|
207
|
+
ec = Flapjack::Data::EntityCheck.for_entity_name(entity, check, :redis => redis)
|
208
|
+
started = Chronic.parse(options[:started]).to_i
|
209
|
+
duration = ChronicDuration.parse(options[:duration]).to_i
|
210
|
+
raise "Failed to parse start time #{options[:started]}" if started == 0
|
211
|
+
raise"Failed to parse duration #{options[:duration]}" if duration == 0
|
212
|
+
|
213
|
+
success = case options[:type]
|
214
|
+
when 'scheduled'
|
215
|
+
ec.create_scheduled_maintenance(started, duration, :summary => options[:reason])
|
216
|
+
when 'unscheduled'
|
217
|
+
ec.create_unscheduled_maintenance(started, duration, :summary => options[:reason])
|
218
|
+
end
|
219
|
+
identifier = "#{entity}:#{check}:#{started}"
|
220
|
+
errors[identifier] = "The following check failed to create: #{identifier}" unless success
|
221
|
+
end
|
222
|
+
end
|
223
|
+
errors
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
def self.check_maintenance_interval(input, maintenance_duration)
|
228
|
+
# If no duration was specified, give back all results
|
229
|
+
return true unless input
|
230
|
+
inp = input.downcase
|
231
|
+
|
232
|
+
if inp.start_with?('between')
|
233
|
+
# Between 3 hours and 4 hours translates to more than 3 hours, less than 4 hours
|
234
|
+
first, last = inp.match(/between (.*) and (.*)/).captures
|
235
|
+
suffix = last.match(/\w (.*)/) ? last.match(/\w (.*)/).captures.first : ''
|
236
|
+
|
237
|
+
# If the first duration only contains only a single word, the unit is
|
238
|
+
# most likely directly after the first word of the the second duration
|
239
|
+
# eg between 3 and 4 hours
|
240
|
+
first = "#{first} #{suffix}" unless / /.match(first)
|
241
|
+
raise "Failed to parse #{first}" unless ChronicDuration.parse(first)
|
242
|
+
raise "Failed to parse #{last}" unless ChronicDuration.parse(last)
|
243
|
+
|
244
|
+
(first, last = last, first) if ChronicDuration.parse(first) > ChronicDuration.parse(last)
|
245
|
+
return check_maintenance_interval("more than #{first}", maintenance_duration) && check_maintenance_interval("less than #{last}", maintenance_duration)
|
246
|
+
end
|
247
|
+
|
248
|
+
# ChronicDuration can't parse timestamps for strings starting with before or after.
|
249
|
+
# Strip the before or after for the conversion only, but use it for the comparison later
|
250
|
+
ctime = inp.gsub(/^(more than|less than|before|after)/, '')
|
251
|
+
input_duration = ChronicDuration.parse(ctime, :keep_zero => true)
|
252
|
+
|
253
|
+
raise "Failed to parse time: #{input}" if input_duration.nil?
|
254
|
+
|
255
|
+
case inp
|
256
|
+
when /^(less than|before)/
|
257
|
+
maintenance_duration < input_duration
|
258
|
+
when /^(more than|after)/
|
259
|
+
maintenance_duration > input_duration
|
260
|
+
else
|
261
|
+
maintenance_duration == input_duration
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.check_maintenance_timestamp(input, maintenance_timestamp)
|
266
|
+
# If no time was specified, give back all results
|
267
|
+
return true unless input
|
268
|
+
inp = input.downcase
|
269
|
+
|
270
|
+
# Chronic can't parse timestamps for strings starting with before, after or in some cases, on.
|
271
|
+
# Strip the before or after for the conversion only, but use it for the comparison later
|
272
|
+
ctime = inp.gsub(/^(on|before|after)/, '')
|
273
|
+
|
274
|
+
base_time = Time.now
|
275
|
+
|
276
|
+
case inp
|
277
|
+
# Between 3 and 4 hours ago translates to more than 3 hours ago, less than 4 hours ago
|
278
|
+
when /^between/
|
279
|
+
first, last = inp.match(/between (.*) and (.*)/).captures
|
280
|
+
|
281
|
+
# If the first time only contains only a single word, the unit (and past/future) is
|
282
|
+
# most likely directly after the first word of the the second time
|
283
|
+
# eg between 3 and 4 hours ago
|
284
|
+
suffix = last.match(/\w (.*)/) ? last.match(/\w (.*)/).captures.first : ''
|
285
|
+
first = "#{first} #{suffix}" unless / /.match(first)
|
286
|
+
|
287
|
+
first += ' from now' unless Chronic.parse(first, :now => base_time)
|
288
|
+
last += ' from now' unless Chronic.parse(last, :now => base_time)
|
289
|
+
raise "Failed to parse #{first}" unless ChronicDuration.parse(first)
|
290
|
+
raise "Failed to parse #{last}" unless ChronicDuration.parse(last)
|
291
|
+
|
292
|
+
(first, last = last, first) if Chronic.parse(first, :now => base_time) > Chronic.parse(last, :now => base_time)
|
293
|
+
(check_maintenance_timestamp("after #{first}", maintenance_timestamp) &&
|
294
|
+
check_maintenance_timestamp("before #{last}", maintenance_timestamp))
|
295
|
+
# On 1/1/15. We use Chronic to work out the minimum and maximum timestamp, and use the same behaviour as between.
|
296
|
+
when /^on/
|
297
|
+
first = Chronic.parse(ctime, :guess => false, :now => base_time).first
|
298
|
+
last = Chronic.parse(ctime, :guess => false, :now => base_time).last
|
299
|
+
(check_maintenance_timestamp("after #{first}", maintenance_timestamp) &&
|
300
|
+
check_maintenance_timestamp("before #{last}", maintenance_timestamp))
|
301
|
+
else
|
302
|
+
# We assume timestamps are rooted against the current time.
|
303
|
+
# Chronic doesn't always handle this correctly, so we need to handhold it a little
|
304
|
+
input_timestamp = Chronic.parse(ctime, :keep_zero => true, :now => base_time).to_i
|
305
|
+
input_timestamp = Chronic.parse(ctime + ' from now', :keep_zero => true, :now => base_time).to_i if input_timestamp == 0
|
306
|
+
|
307
|
+
raise "Failed to parse time: #{input}" if input_timestamp == 0
|
308
|
+
|
309
|
+
case inp
|
310
|
+
when /^less than/
|
311
|
+
if input_timestamp < base_time.to_i
|
312
|
+
maintenance_timestamp > input_timestamp
|
313
|
+
else
|
314
|
+
maintenance_timestamp < input_timestamp
|
315
|
+
end
|
316
|
+
when /^more than/
|
317
|
+
# FIXME: and here is the race condition. input timestamp could be in the previous second
|
318
|
+
# to Time.now due to code execution time:
|
319
|
+
if input_timestamp < base_time.to_i
|
320
|
+
maintenance_timestamp < input_timestamp
|
321
|
+
else
|
322
|
+
maintenance_timestamp > input_timestamp
|
323
|
+
end
|
324
|
+
when /^before/
|
325
|
+
maintenance_timestamp < input_timestamp
|
326
|
+
when /^after/
|
327
|
+
maintenance_timestamp > input_timestamp
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
135
332
|
def self.in_unscheduled_maintenance_for_event_id?(event_id, options)
|
136
333
|
raise "Redis connection not set" unless redis = options[:redis]
|
137
334
|
redis.exists("#{event_id}:unscheduled_maintenance")
|
@@ -184,10 +381,9 @@ module Flapjack
|
|
184
381
|
|
185
382
|
checks = []
|
186
383
|
# get all the current checks, with last update time
|
187
|
-
Flapjack::Data::Entity.
|
188
|
-
redis.zrange("current_checks:#{entity}", 0, -1,
|
189
|
-
|
190
|
-
checks << check
|
384
|
+
Flapjack::Data::Entity.all(:enabled => true, :redis => redis).each do |entity|
|
385
|
+
redis.zrange("current_checks:#{entity}", 0, -1, :withscores => true).each do |check, score|
|
386
|
+
checks << ["#{entity}:#{check}", score]
|
191
387
|
end
|
192
388
|
end
|
193
389
|
|
@@ -270,10 +466,11 @@ module Flapjack
|
|
270
466
|
if (um_start = @redis.get("#{@key}:unscheduled_maintenance"))
|
271
467
|
duration = end_time - um_start.to_i
|
272
468
|
@logger.debug("ending unscheduled downtime for #{@key} at #{Time.at(end_time).to_s}") if @logger
|
273
|
-
@redis.del("#{@key}:unscheduled_maintenance")
|
274
469
|
@redis.zadd("#{@key}:unscheduled_maintenances", duration, um_start) # updates existing UM 'score'
|
470
|
+
@redis.del("#{@key}:unscheduled_maintenance") == 1
|
275
471
|
else
|
276
472
|
@logger.debug("end_unscheduled_maintenance called for #{@key} but none found") if @logger
|
473
|
+
true
|
277
474
|
end
|
278
475
|
end
|
279
476
|
|
@@ -392,6 +589,7 @@ module Flapjack
|
|
392
589
|
end
|
393
590
|
|
394
591
|
# Retain event data for entity:check pair
|
592
|
+
# NB (appending to tail as far as Redis is concerned)
|
395
593
|
@redis.rpush("#{@key}:states", timestamp)
|
396
594
|
@redis.set("#{@key}:#{timestamp}:state", new_state)
|
397
595
|
@redis.set("#{@key}:#{timestamp}:summary", summary) if summary
|
data/lib/flapjack/data/event.rb
CHANGED
@@ -124,7 +124,7 @@ module Flapjack
|
|
124
124
|
if parsed.is_a?(Hash)
|
125
125
|
errors = validation_errors_for_hash(parsed, opts)
|
126
126
|
else
|
127
|
-
errors << "Event must be a JSON hash, see
|
127
|
+
errors << "Event must be a JSON hash, see http://flapjack.io/docs/1.0/development/DATA_STRUCTURES#event-queue"
|
128
128
|
end
|
129
129
|
return parsed if errors.empty?
|
130
130
|
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
|
7
|
+
require 'flapjack/data/alert'
|
8
|
+
require 'flapjack/utility'
|
9
|
+
|
10
|
+
module Flapjack
|
11
|
+
module Gateways
|
12
|
+
class AwsSns
|
13
|
+
|
14
|
+
SNS_DEFAULT_REGION_NAME = 'us-east-1'
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
include Flapjack::Utility
|
19
|
+
|
20
|
+
def start
|
21
|
+
@sent = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def perform(contents)
|
25
|
+
@logger.debug "Received a notification: #{contents.inspect}"
|
26
|
+
alert = Flapjack::Data::Alert.new(contents, :logger => @logger)
|
27
|
+
|
28
|
+
region_name = @config["region_name"] || SNS_DEFAULT_REGION_NAME
|
29
|
+
hostname = "sns.#{region_name}.amazonaws.com"
|
30
|
+
endpoint = "http://#{hostname}/"
|
31
|
+
access_key = @config["access_key"]
|
32
|
+
secret_key = @config["secret_key"]
|
33
|
+
timestamp = @config["timestamp"] || DateTime.now.iso8601
|
34
|
+
|
35
|
+
address = alert.address
|
36
|
+
notification_id = alert.notification_id
|
37
|
+
message_type = alert.rollup ? 'rollup' : 'alert'
|
38
|
+
|
39
|
+
my_dir = File.dirname(__FILE__)
|
40
|
+
sms_template_path = case
|
41
|
+
when @config.has_key?('templates') && @config['templates']["#{message_type}.text"]
|
42
|
+
@config['templates']["#{message_type}.text"]
|
43
|
+
else
|
44
|
+
my_dir + "/aws_sns/#{message_type}.text.erb"
|
45
|
+
end
|
46
|
+
sms_template = ERB.new(File.read(sms_template_path), nil, '-')
|
47
|
+
|
48
|
+
@alert = alert
|
49
|
+
bnd = binding
|
50
|
+
|
51
|
+
begin
|
52
|
+
message = sms_template.result(bnd).chomp
|
53
|
+
rescue => e
|
54
|
+
@logger.error "Error while excuting the ERB for an sms: " +
|
55
|
+
"ERB being executed: #{sms_template_path}"
|
56
|
+
raise
|
57
|
+
end
|
58
|
+
|
59
|
+
if @config.nil? || (@config.respond_to?(:empty?) && @config.empty?)
|
60
|
+
@logger.error "AWS SNS config is missing"
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
errors = []
|
65
|
+
|
66
|
+
[[address, "AWS SNS topic ARN is missing"],
|
67
|
+
[access_key, "AWS SNS access key is missing"],
|
68
|
+
[secret_key, "AWS SNS secret key is missing"],
|
69
|
+
[notification_id, "Notification id is missing"]].each do |val_err|
|
70
|
+
|
71
|
+
next unless val_err.first.nil? || (val_err.first.respond_to?(:empty?) && val_err.first.empty?)
|
72
|
+
errors << val_err.last
|
73
|
+
end
|
74
|
+
|
75
|
+
unless errors.empty?
|
76
|
+
errors.each {|err| @logger.error err }
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
query = {'Subject' => message,
|
82
|
+
'TopicArn' => address,
|
83
|
+
'Message' => message,
|
84
|
+
'Action' => 'Publish',
|
85
|
+
'SignatureVersion' => 2,
|
86
|
+
'SignatureMethod' => 'HmacSHA256',
|
87
|
+
'Timestamp' => timestamp,
|
88
|
+
'AWSAccessKeyId' => access_key}
|
89
|
+
|
90
|
+
string_to_sign = string_to_sign('POST', hostname, "/", query)
|
91
|
+
|
92
|
+
query['Signature'] = get_signature(secret_key, string_to_sign)
|
93
|
+
|
94
|
+
http = EM::HttpRequest.new(endpoint).post(:query => query)
|
95
|
+
|
96
|
+
@logger.debug "server response: #{http.response}"
|
97
|
+
|
98
|
+
status = (http.nil? || http.response_header.nil?) ? nil : http.response_header.status
|
99
|
+
if (status >= 200) && (status <= 206)
|
100
|
+
@sent += 1
|
101
|
+
alert.record_send_success!
|
102
|
+
@logger.debug "Sent notification via SNS, response status is #{status}, " +
|
103
|
+
"notification_id: #{notification_id}"
|
104
|
+
else
|
105
|
+
@logger.error "Failed to send notification via SNS, response status is #{status}, " +
|
106
|
+
"notification_id: #{notification_id}"
|
107
|
+
end
|
108
|
+
rescue => e
|
109
|
+
@logger.error "Error generating or delivering notification to #{address}: #{e.class}: #{e.message}"
|
110
|
+
@logger.error e.backtrace.join("\n")
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_signature(secret_key, string_to_sign)
|
115
|
+
signature = OpenSSL::HMAC.digest('sha256', secret_key, string_to_sign)
|
116
|
+
|
117
|
+
Base64.encode64(signature).strip
|
118
|
+
end
|
119
|
+
|
120
|
+
def string_to_sign(method, host, uri, query)
|
121
|
+
query = query.sort_by { |key, value| key }
|
122
|
+
|
123
|
+
[method.upcase,
|
124
|
+
host.downcase,
|
125
|
+
uri,
|
126
|
+
URI.encode_www_form(query)
|
127
|
+
].join("\n")
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<%= @alert.type_sentence_case %>: '<%= @alert.check %>' on <%= @alert.entity -%>
|
2
|
+
<% unless ['acknowledgement', 'test'].include?(@alert.notification_type) -%>
|
3
|
+
is <%= @alert.state_title_case -%>
|
4
|
+
<% end -%>
|
5
|
+
at <%= Time.at(@alert.time).strftime('%-d %b %H:%M') %>, <%= @alert.summary -%>
|