flapjack 1.0.0rc3 → 1.0.0rc5
Sign up to get free protection for your applications and to get access to all the features.
- 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 -%>
|