flapjack 0.7.14 → 0.7.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG.md +10 -0
  2. data/etc/flapjack_config.yaml.example +1 -0
  3. data/features/events.feature +5 -0
  4. data/features/notification_rules.feature +1 -1
  5. data/features/steps/events_steps.rb +28 -13
  6. data/features/steps/notifications_steps.rb +1 -1
  7. data/lib/flapjack/coordinator.rb +3 -1
  8. data/lib/flapjack/data/contact.rb +8 -6
  9. data/lib/flapjack/data/entity_check.rb +78 -113
  10. data/lib/flapjack/data/event.rb +54 -65
  11. data/lib/flapjack/data/notification.rb +5 -1
  12. data/lib/flapjack/executive.rb +42 -38
  13. data/lib/flapjack/filters/acknowledgement.rb +5 -5
  14. data/lib/flapjack/filters/base.rb +2 -2
  15. data/lib/flapjack/filters/delays.rb +11 -11
  16. data/lib/flapjack/filters/detect_mass_client_failures.rb +8 -8
  17. data/lib/flapjack/filters/ok.rb +6 -6
  18. data/lib/flapjack/filters/scheduled_maintenance.rb +2 -2
  19. data/lib/flapjack/filters/unscheduled_maintenance.rb +3 -2
  20. data/lib/flapjack/gateways/api.rb +374 -277
  21. data/lib/flapjack/gateways/api/entity_check_presenter.rb +52 -21
  22. data/lib/flapjack/gateways/api/entity_presenter.rb +14 -9
  23. data/lib/flapjack/gateways/email.rb +7 -0
  24. data/lib/flapjack/gateways/email/alert.html.haml +13 -1
  25. data/lib/flapjack/gateways/email/alert.text.erb +5 -4
  26. data/lib/flapjack/gateways/jabber.rb +90 -34
  27. data/lib/flapjack/gateways/pagerduty.rb +6 -2
  28. data/lib/flapjack/gateways/web.rb +13 -8
  29. data/lib/flapjack/gateways/web/views/check.haml +70 -45
  30. data/lib/flapjack/gateways/web/views/checks.haml +1 -1
  31. data/lib/flapjack/gateways/web/views/entity.haml +1 -1
  32. data/lib/flapjack/patches.rb +9 -2
  33. data/lib/flapjack/pikelet.rb +14 -10
  34. data/lib/flapjack/utility.rb +10 -4
  35. data/lib/flapjack/version.rb +1 -1
  36. data/spec/lib/flapjack/coordinator_spec.rb +19 -5
  37. data/spec/lib/flapjack/data/entity_check_spec.rb +3 -30
  38. data/spec/lib/flapjack/data/event_spec.rb +96 -1
  39. data/spec/lib/flapjack/executive_spec.rb +5 -11
  40. data/spec/lib/flapjack/gateways/api/entity_check_presenter_spec.rb +22 -3
  41. data/spec/lib/flapjack/gateways/api/entity_presenter_spec.rb +30 -15
  42. data/spec/lib/flapjack/gateways/api_spec.rb +552 -186
  43. data/spec/lib/flapjack/gateways/email_spec.rb +2 -0
  44. data/spec/lib/flapjack/gateways/jabber_spec.rb +5 -4
  45. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +3 -2
  46. data/spec/lib/flapjack/gateways/web_spec.rb +17 -12
  47. data/spec/lib/flapjack/pikelet_spec.rb +5 -2
  48. metadata +4 -5
  49. data/config.ru +0 -11
@@ -18,25 +18,25 @@ module Flapjack
18
18
  client_mass_fail_threshold = 10
19
19
  timestamp = Time.now.to_i
20
20
 
21
- if event.type == 'service'
22
- client_fail_count = @persistence.zcount("failed_checks:#{event.client}", '-inf', '+inf')
21
+ if event.service?
22
+ client_fail_count = @redis.zcount("failed_checks:#{event.client}", '-inf', '+inf')
23
23
 
24
24
  if client_fail_count >= client_mass_fail_threshold
25
25
  # set the flag
26
26
  # FIXME: perhaps implement this with tagging
27
- @persistence.add("mass_failed_client:#{event.client}", timestamp)
28
- @persistence.zadd("mass_failure_events_client:#{event.client}", 0, timestamp)
27
+ @redis.add("mass_failed_client:#{event.client}", timestamp)
28
+ @redis.zadd("mass_failure_events_client:#{event.client}", 0, timestamp)
29
29
  else
30
30
  # unset the flag
31
- start_mf = @persistence.get("mass_failed_client:#{event.client}")
31
+ start_mf = @redis.get("mass_failed_client:#{event.client}")
32
32
  duration = Time.now.to_i - start_mf.to_i
33
- @persistence.del("mass_failed_client:#{event.client}")
34
- @persistence.zadd("mass_failure_events_client:#{event.client}", duration, start_mf)
33
+ @redis.del("mass_failed_client:#{event.client}")
34
+ @redis.zadd("mass_failure_events_client:#{event.client}", duration, start_mf)
35
35
  end
36
36
  end
37
37
 
38
38
  result = false
39
- @log.debug("Filter: DetectMassClientFailures: #{result ? "block" : "pass"}")
39
+ @logger.debug("Filter: DetectMassClientFailures: #{result ? "block" : "pass"}")
40
40
  result
41
41
  end
42
42
  end
@@ -19,22 +19,22 @@ module Flapjack
19
19
 
20
20
  if event.ok?
21
21
  if event.previous_state == 'ok'
22
- @log.debug("Filter: Ok: existing state was ok, and the previous state was ok, so blocking")
22
+ @logger.debug("Filter: Ok: existing state was ok, and the previous state was ok, so blocking")
23
23
  result = true
24
24
  end
25
25
 
26
- entity_check = Flapjack::Data::EntityCheck.for_event_id(event.id, :redis => @persistence)
26
+ entity_check = Flapjack::Data::EntityCheck.for_event_id(event.id, :redis => @redis)
27
27
 
28
28
  last_notification = entity_check.last_notification
29
- @log.debug("Filter: Ok: last notification: #{last_notification.inspect}")
29
+ @logger.debug("Filter: Ok: last notification: #{last_notification.inspect}")
30
30
  if last_notification[:type] == 'recovery'
31
- @log.debug("Filter: Ok: last notification was a recovery, so blocking")
31
+ @logger.debug("Filter: Ok: last notification was a recovery, so blocking")
32
32
  result = true
33
33
  end
34
34
 
35
35
  if event.previous_state != 'ok'
36
36
  if event.previous_state_duration < 30
37
- @log.debug("Filter: Ok: previous non ok state was for less than 30 seconds, so blocking")
37
+ @logger.debug("Filter: Ok: previous non ok state was for less than 30 seconds, so blocking")
38
38
  result = true
39
39
  end
40
40
  end
@@ -43,7 +43,7 @@ module Flapjack
43
43
  entity_check.end_unscheduled_maintenance
44
44
  end
45
45
 
46
- @log.debug("Filter: Ok: #{result ? "block" : "pass"}")
46
+ @logger.debug("Filter: Ok: #{result ? "block" : "pass"}")
47
47
  result
48
48
  end
49
49
  end
@@ -8,8 +8,8 @@ module Flapjack
8
8
  include Base
9
9
 
10
10
  def block?(event)
11
- result = @persistence.exists("#{event.id}:scheduled_maintenance")
12
- @log.debug("Filter: Scheduled Maintenance: #{result ? "block" : "pass"}")
11
+ result = @redis.exists("#{event.id}:scheduled_maintenance")
12
+ @logger.debug("Filter: Scheduled Maintenance: #{result ? "block" : "pass"}")
13
13
  result
14
14
  end
15
15
  end
@@ -8,8 +8,9 @@ module Flapjack
8
8
  include Base
9
9
 
10
10
  def block?(event)
11
- result = @persistence.exists("#{event.id}:unscheduled_maintenance")
12
- @log.debug("Filter: Unscheduled Maintenance: #{result ? "block" : "pass"}")
11
+ result = @redis.exists("#{event.id}:unscheduled_maintenance") &&
12
+ !event.acknowledgement?
13
+ @logger.debug("Filter: Unscheduled Maintenance: #{result ? "block" : "pass"}")
13
14
  result
14
15
  end
15
16
  end
@@ -16,6 +16,7 @@ require 'flapjack/data/entity'
16
16
  require 'flapjack/data/entity_check'
17
17
 
18
18
  require 'flapjack/gateways/api/entity_presenter'
19
+ require 'flapjack/gateways/api/entity_check_presenter'
19
20
  require 'flapjack/rack_logger'
20
21
  require 'flapjack/redis_pool'
21
22
 
@@ -50,6 +51,38 @@ module Flapjack
50
51
 
51
52
  class API < Sinatra::Base
52
53
 
54
+ # used for backwards-compatible route matching below
55
+ ENTITY_CHECK_FRAGMENT = '(?:/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(.+))?)?'
56
+
57
+ class EntityCheckNotFound < RuntimeError
58
+ attr_reader :entity, :check
59
+ def initialize(entity, check)
60
+ @entity = entity
61
+ @check = check
62
+ end
63
+ end
64
+
65
+ class EntityNotFound < RuntimeError
66
+ attr_reader :entity
67
+ def initialize(entity)
68
+ @entity = entity
69
+ end
70
+ end
71
+
72
+ class ContactNotFound < RuntimeError
73
+ attr_reader :contact_id
74
+ def initialize(contact_id)
75
+ @contact_id = contact_id
76
+ end
77
+ end
78
+
79
+ class NotificationRuleNotFound < RuntimeError
80
+ attr_reader :rule_id
81
+ def initialize(rule_id)
82
+ @rule_id = rule_id
83
+ end
84
+ end
85
+
53
86
  include Flapjack::Utility
54
87
 
55
88
  set :show_exceptions, false
@@ -88,147 +121,171 @@ module Flapjack
88
121
  get '/entities' do
89
122
  content_type :json
90
123
  ret = Flapjack::Data::Entity.all(:redis => redis).sort_by(&:name).collect {|e|
91
- {'id' => e.id, 'name' => e.name,
92
- 'checks' => e.check_list.sort.collect {|c|
93
- entity_check_status(e, c)
94
- }
95
- }
124
+ presenter = Flapjack::Gateways::API::EntityPresenter.new(e, :redis => redis)
125
+ {'id' => e.id, 'name' => e.name, 'checks' => presenter.status }
96
126
  }
97
127
  ret.to_json
98
128
  end
99
129
 
100
130
  get '/checks/:entity' do
101
131
  content_type :json
102
- find_entity(params[:entity]) do |entity|
103
- entity.check_list.to_json
104
- end
132
+ entity = find_entity(params[:entity])
133
+ entity.check_list.to_json
105
134
  end
106
135
 
107
- get %r{/status/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(.+))?} do
136
+ get %r{/status#{ENTITY_CHECK_FRAGMENT}} do
108
137
  content_type :json
109
138
 
110
- entity_name = params[:captures][0]
111
- check = params[:captures][1]
139
+ captures = params[:captures] || []
140
+ entity_name = captures[0]
141
+ check = captures[1]
112
142
 
113
- find_entity(entity_name) do |entity|
114
- ret = if check
115
- entity_check_status(entity, check)
116
- else
117
- entity.check_list.sort.collect {|c|
118
- entity_check_status(entity, c)
119
- }
120
- end
121
- return error(404, "could not find entity check '#{entity_name}:#{check}'") if ret.nil?
122
- ret.to_json
123
- end
124
- end
143
+ entities, checks = entities_and_checks(entity_name, check)
125
144
 
126
- # the first capture group in the regex checks for acceptable
127
- # characters in a domain name -- this will also match strings
128
- # that aren't acceptable domain names as well, of course.
129
- get %r{/outages/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(\w+))?} do
130
- content_type :json
131
-
132
- entity_name = params[:captures][0]
133
- check = params[:captures][1]
134
-
135
- start_time = validate_and_parsetime(params[:start_time])
136
- end_time = validate_and_parsetime(params[:end_time])
145
+ results = present_api_results(entities, checks, 'status') {|presenter|
146
+ presenter.status
147
+ }
137
148
 
138
- find_api_presenter(entity_name, check) do |presenter|
139
- presenter.outages(start_time, end_time).to_json
149
+ if entity_name
150
+ # compatible with previous data format
151
+ results = results.collect {|status_h| status_h[:status]}
152
+ (check ? results.first : results).to_json
153
+ else
154
+ # new and improved data format which reflects the request param structure
155
+ results.to_json
140
156
  end
141
157
  end
142
158
 
143
- get %r{/unscheduled_maintenances/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(\w+))?} do
144
- content_type :json
159
+ get %r{/((?:outages|(?:un)?scheduled_maintenances|downtime))#{ENTITY_CHECK_FRAGMENT}} do
160
+ action = params[:captures][0].to_sym
161
+ entity_name = params[:captures][1]
162
+ check = params[:captures][2]
145
163
 
146
- entity_name = params[:captures][0]
147
- check = params[:captures][1]
164
+ entities, checks = entities_and_checks(entity_name, check)
148
165
 
149
166
  start_time = validate_and_parsetime(params[:start_time])
150
167
  end_time = validate_and_parsetime(params[:end_time])
151
168
 
152
- find_api_presenter(entity_name, check) do |presenter|
153
- presenter.unscheduled_maintenance(start_time, end_time).to_json
169
+ results = present_api_results(entities, checks, action) {|presenter|
170
+ presenter.send(action, start_time, end_time)
171
+ }
172
+
173
+ if check
174
+ # compatible with previous data format
175
+ results.first[action].to_json
176
+ elsif entity_name
177
+ # compatible with previous data format
178
+ rename = {:unscheduled_maintenances => :unscheduled_maintenance,
179
+ :scheduled_maintenances => :scheduled_maintenance}
180
+ drop = [:entity]
181
+ results.collect{|r|
182
+ r.inject({}) {|memo, (k, v)|
183
+ if new_k = rename[k]
184
+ memo[new_k] = v
185
+ elsif !drop.include?(k)
186
+ memo[k] = v
187
+ end
188
+ memo
189
+ }
190
+ }.to_json
191
+ else
192
+ # new and improved data format which reflects the request param structure
193
+ results.to_json
154
194
  end
155
195
  end
156
196
 
157
- get %r{/scheduled_maintenances/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(\w+))?} do
158
- content_type :json
159
-
197
+ # create a scheduled maintenance period for a check on an entity
198
+ post %r{/scheduled_maintenances#{ENTITY_CHECK_FRAGMENT}} do
160
199
  entity_name = params[:captures][0]
161
- check = params[:captures][1]
200
+ check = params[:captures][1]
201
+
202
+ entities, checks = entities_and_checks(entity_name, check)
162
203
 
163
204
  start_time = validate_and_parsetime(params[:start_time])
164
- end_time = validate_and_parsetime(params[:end_time])
165
205
 
166
- find_api_presenter(entity_name, check) do |presenter|
167
- presenter.scheduled_maintenance(start_time, end_time).to_json
168
- end
206
+ act_proc = proc {|entity_check|
207
+ entity_check.create_scheduled_maintenance(:start_time => start_time,
208
+ :duration => params[:duration].to_i, :summary => params[:summary])
209
+ }
210
+
211
+ bulk_api_check_action(entities, checks, act_proc)
212
+ status 204
169
213
  end
170
214
 
171
- get %r{/downtime/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(\w+))?} do
172
- content_type :json
215
+ # create an acknowledgement for a service on an entity
216
+ # NB currently, this does not acknowledge a specific failure event, just
217
+ # the entity-check as a whole
218
+ post %r{/acknowledgements#{ENTITY_CHECK_FRAGMENT}} do
219
+ captures = params[:captures] || []
220
+ entity_name = captures[0]
221
+ check = captures[1]
173
222
 
174
- entity_name = params[:captures][0]
175
- check = params[:captures][1]
223
+ entities, checks = entities_and_checks(entity_name, check)
176
224
 
177
- start_time = validate_and_parsetime(params[:start_time])
178
- end_time = validate_and_parsetime(params[:end_time])
225
+ dur = params[:duration] ? params[:duration].to_i : nil
226
+ duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur
227
+ summary = params[:summary]
179
228
 
180
- find_api_presenter(entity_name, check) do |presenter|
181
- presenter.downtime(start_time, end_time).to_json
182
- end
229
+ opts = {'duration' => duration}
230
+ opts['summary'] = summary if summary
231
+
232
+ act_proc = proc {|entity_check|
233
+ Flapjack::Data::Event.create_acknowledgement(
234
+ entity_check.entity_name, entity_check.check,
235
+ :summary => params[:summary],
236
+ :duration => duration,
237
+ :redis => redis)
238
+ }
239
+
240
+ bulk_api_check_action(entities, checks, act_proc)
241
+ status 204
183
242
  end
184
243
 
185
- # create a scheduled maintenance period for a service on an entity
186
- post '/scheduled_maintenances/:entity/:check' do
187
- content_type :json
244
+ delete %r{/((?:un)?scheduled_maintenances)} do
245
+ action = params[:captures][0]
188
246
 
189
- start_time = validate_and_parsetime(params[:start_time])
247
+ # no backwards-compatible mode here, it's a new method
248
+ entities, checks = entities_and_checks(nil, nil)
190
249
 
191
- find_entity(params[:entity]) do |entity|
192
- find_entity_check(entity, params[:check]) do |entity_check|
193
- entity_check.create_scheduled_maintenance(:start_time => start_time,
194
- :duration => params[:duration].to_i, :summary => params[:summary])
195
- status 204
196
- end
250
+ act_proc = case action
251
+ when 'scheduled_maintenances'
252
+ start_time = validate_and_parsetime(params[:start_time])
253
+ opts = {}
254
+ opts[:start_time] = start_time.to_i if start_time
255
+ proc {|entity_check| entity_check.delete_scheduled_maintenance(opts) }
256
+ when 'unscheduled_maintenances'
257
+ end_time = validate_and_parsetime(params[:end_time])
258
+ opts = {}
259
+ opts[:end_time] = end_time.to_i if end_time
260
+ proc {|entity_check| entity_check.end_unscheduled_maintenance(opts) }
197
261
  end
262
+
263
+ bulk_api_check_action(entities, checks, act_proc)
264
+ status 204
198
265
  end
199
266
 
200
- # create an acknowledgement for a service on an entity
201
- # NB currently, this does not acknowledge a specific failure event, just
202
- # the entity-check as a whole
203
- post '/acknowledgements/:entity/:check' do
204
- content_type :json
267
+ post %r{/test_notifications#{ENTITY_CHECK_FRAGMENT}} do
268
+ captures = params[:captures] || []
269
+ entity_name = captures[0]
270
+ check = captures[1]
205
271
 
206
- dur = params[:duration] ? params[:duration].to_i : nil
207
- duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur
272
+ entities, checks = entities_and_checks(entity_name, check)
208
273
 
209
- find_entity(params[:entity]) do |entity|
210
- find_entity_check(entity, params[:check]) do |entity_check|
211
- entity_check.create_acknowledgement('summary' => params[:summary],
212
- 'duration' => duration)
213
- status 204
214
- end
215
- end
216
- end
274
+ act_proc = proc {|entity_check|
275
+ summary = params[:summary] ||
276
+ "Testing notifications to all contacts interested in entity #{entity_check.entity.name}"
277
+ Flapjack::Data::Event.test_notifications(
278
+ entity_check.entity_name, entity_check.check,
279
+ :summary => summary,
280
+ :redis => redis)
281
+ }
217
282
 
218
- post '/test_notifications/:entity/:check' do
219
- content_type :json
220
- find_entity(params[:entity]) do |entity|
221
- find_entity_check(entity, params[:check]) do |entity_check|
222
- summary = params[:summary] || "Testing notifications to all contacts interested in entity #{entity.name}"
223
- entity_check.test_notifications('summary' => summary)
224
- status 204
225
- end
226
- end
283
+ bulk_api_check_action(entities, checks, act_proc)
284
+ status 204
227
285
  end
228
286
 
229
287
  post '/entities' do
230
288
  pass unless 'application/json'.eql?(request.content_type)
231
- content_type :json
232
289
 
233
290
  errors = []
234
291
  ret = nil
@@ -239,10 +296,10 @@ module Flapjack
239
296
  unless entities
240
297
  logger.debug("no entities object found in the following supplied JSON:")
241
298
  logger.debug(request.body)
242
- return error(403, "No entities object received")
299
+ return err(403, "No entities object received")
243
300
  end
244
- return error(403, "The received entities object is not an Enumerable") unless entities.is_a?(Enumerable)
245
- return error(403, "Entity with a nil id detected") unless entities.any? {|e| !e['id'].nil?}
301
+ return err(403, "The received entities object is not an Enumerable") unless entities.is_a?(Enumerable)
302
+ return err(403, "Entity with a nil id detected") unless entities.any? {|e| !e['id'].nil?}
246
303
 
247
304
  entities.each do |entity|
248
305
  unless entity['id']
@@ -251,8 +308,7 @@ module Flapjack
251
308
  end
252
309
  Flapjack::Data::Entity.add(entity, :redis => redis)
253
310
  end
254
-
255
- errors.empty? ? 204 : error(403, *errors)
311
+ errors.empty? ? 204 : err(403, *errors)
256
312
  end
257
313
 
258
314
  post '/contacts' do
@@ -293,13 +349,14 @@ module Flapjack
293
349
  end
294
350
  end
295
351
  end
296
- errors.empty? ? 204 : error(403, *errors)
352
+ errors.empty? ? 204 : err(403, *errors)
297
353
  end
298
354
 
299
355
  # Returns all the contacts
300
356
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts
301
357
  get '/contacts' do
302
358
  content_type :json
359
+
303
360
  Flapjack::Data::Contact.all(:redis => redis).to_json
304
361
  end
305
362
 
@@ -307,18 +364,18 @@ module Flapjack
307
364
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id
308
365
  get '/contacts/:contact_id' do
309
366
  content_type :json
310
- find_contact(params[:contact_id]) do |contact|
311
- contact.to_json
312
- end
367
+
368
+ contact = find_contact(params[:contact_id])
369
+ contact.to_json
313
370
  end
314
371
 
315
372
  # Lists this contact's notification rules
316
373
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules
317
374
  get '/contacts/:contact_id/notification_rules' do
318
375
  content_type :json
319
- find_contact(params[:contact_id]) do |contact|
320
- contact.notification_rules.to_json
321
- end
376
+
377
+ contact = find_contact(params[:contact_id])
378
+ contact.notification_rules.to_json
322
379
  end
323
380
 
324
381
  # Get the specified notification rule for this user
@@ -326,317 +383,357 @@ module Flapjack
326
383
  get '/notification_rules/:id' do
327
384
  content_type :json
328
385
 
329
- find_rule(params[:id]) do |rule|
330
- rule.to_json
331
- end
386
+ rule = find_rule(params[:id])
387
+ rule.to_json
332
388
  end
333
389
 
334
390
  # Creates a notification rule for a contact
335
391
  # https://github.com/flpjck/flapjack/wiki/API#wiki-post_contacts_id_notification_rules
336
392
  post '/notification_rules' do
337
393
  content_type :json
394
+
338
395
  if params[:id]
339
- return error(403, "post cannot be used for update, do a put instead")
396
+ halt err(403, "post cannot be used for update, do a put instead")
340
397
  end
341
398
 
342
399
  logger.debug("post /notification_rules data: ")
343
400
  logger.debug(params.inspect)
344
401
 
345
- find_contact(params[:contact_id]) do |contact|
402
+ contact = find_contact(params[:contact_id])
346
403
 
347
- rule_data = hashify(:entities, :entity_tags,
348
- :warning_media, :critical_media, :time_restrictions,
349
- :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
404
+ rule_data = hashify(:entities, :entity_tags,
405
+ :warning_media, :critical_media, :time_restrictions,
406
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
350
407
 
351
- unless rule = contact.add_notification_rule(rule_data, :logger => logger)
352
- return error(403, "invalid notification rule data")
353
- end
354
- rule.to_json
408
+ unless rule = contact.add_notification_rule(rule_data, :logger => logger)
409
+ halt err(403, "invalid notification rule data")
355
410
  end
411
+ rule.to_json
356
412
  end
357
413
 
358
414
  # Updates a notification rule
359
415
  # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
360
416
  put('/notification_rules/:id') do
361
417
  content_type :json
418
+
362
419
  logger.debug("put /notification_rules/#{params[:id]} data: ")
363
420
  logger.debug(params.inspect)
364
421
 
365
- find_rule(params[:id]) do |rule|
366
- find_contact(rule.contact_id) do |contact|
422
+ rule = find_rule(params[:id])
423
+ contact = find_contact(rule.contact_id)
367
424
 
368
- rule_data = hashify(:entities, :entity_tags,
369
- :warning_media, :critical_media, :time_restrictions,
370
- :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
425
+ rule_data = hashify(:entities, :entity_tags,
426
+ :warning_media, :critical_media, :time_restrictions,
427
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
371
428
 
372
- unless rule.update(rule_data, :logger => logger)
373
- return error(403, "invalid notification rule data")
374
- end
375
- rule.to_json
376
- end
429
+ unless rule.update(rule_data, :logger => logger)
430
+ halt err(403, "invalid notification rule data")
377
431
  end
432
+ rule.to_json
378
433
  end
379
434
 
380
435
  # Deletes a notification rule
381
436
  # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
382
437
  delete('/notification_rules/:id') do
383
438
  logger.debug("delete /notification_rules/#{params[:id]}")
384
- find_rule(params[:id]) do |rule|
385
- logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
386
- find_contact(rule.contact_id) do |contact|
387
- contact.delete_notification_rule(rule)
388
- status 204
389
- end
390
- end
439
+ rule = find_rule(params[:id])
440
+ logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
441
+ contact = find_contact(rule.contact_id)
442
+ contact.delete_notification_rule(rule)
443
+ status 204
391
444
  end
392
445
 
393
446
  # Returns the media of a contact
394
447
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media
395
448
  get '/contacts/:contact_id/media' do
396
449
  content_type :json
397
- find_contact(params[:contact_id]) do |contact|
398
- media = contact.media
399
- media_intervals = contact.media_intervals
400
- media_addr_int = hashify(*media.keys) {|k|
401
- [k, {'address' => media[k],
402
- 'interval' => media_intervals[k] }]
403
- }
404
- media_addr_int.to_json
405
- end
450
+
451
+ contact = find_contact(params[:contact_id])
452
+
453
+ media = contact.media
454
+ media_intervals = contact.media_intervals
455
+ media_addr_int = hashify(*media.keys) {|k|
456
+ [k, {'address' => media[k],
457
+ 'interval' => media_intervals[k] }]
458
+ }
459
+ media_addr_int.to_json
406
460
  end
407
461
 
408
462
  # Returns the specified media of a contact
409
463
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media_media
410
464
  get('/contacts/:contact_id/media/:id') do
411
465
  content_type :json
412
- find_contact(params[:contact_id]) do |contact|
413
- if contact.media[params[:id]].nil?
414
- status 404
415
- return
416
- end
417
- {'address' => contact.media[params[:id]],
418
- 'interval' => contact.media_intervals[params[:id]]}.to_json
466
+
467
+ contact = find_contact(params[:contact_id])
468
+ media = contact.media[params[:id]]
469
+ if media.nil?
470
+ halt err(403, "no #{params[:id]} for contact '#{params[:contact_id]}'")
419
471
  end
472
+ interval = contact.media_intervals[params[:id]]
473
+ if interval.nil?
474
+ halt err(403, "no #{params[:id]} interval for contact '#{params[:contact_id]}'")
475
+ end
476
+ {'address' => media,
477
+ 'interval' => interval}.to_json
420
478
  end
421
479
 
422
480
  # Creates or updates a media of a contact
423
481
  # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_media_media
424
482
  put('/contacts/:contact_id/media/:id') do
425
483
  content_type :json
426
- find_contact(params[:contact_id]) do |contact|
427
- errors = []
428
- if params[:address].nil?
429
- errors << "no address for '#{params[:id]}' media"
430
- end
431
- if params[:interval].nil?
432
- errors << "no interval for '#{params[:id]}' media"
433
- end
434
484
 
435
- return error(403, *errors) unless errors.empty?
485
+ contact = find_contact(params[:contact_id])
486
+ errors = []
487
+ if params[:address].nil?
488
+ errors << "no address for '#{params[:id]}' media"
489
+ end
436
490
 
437
- contact.set_address_for_media(params[:id], params[:address])
438
- contact.set_interval_for_media(params[:id], params[:interval])
491
+ halt err(403, *errors) unless errors.empty?
439
492
 
440
- {'address' => contact.media[params[:id]],
441
- 'interval' => contact.media_intervals[params[:id]]}.to_json
442
- end
493
+ contact.set_address_for_media(params[:id], params[:address])
494
+ contact.set_interval_for_media(params[:id], params[:interval])
495
+
496
+ {'address' => contact.media[params[:id]],
497
+ 'interval' => contact.media_intervals[params[:id]]}.to_json
443
498
  end
444
499
 
445
500
  # delete a media of a contact
446
501
  delete('/contacts/:contact_id/media/:id') do
447
- find_contact(params[:contact_id]) do |contact|
448
- contact.remove_media(params[:id])
449
- status 204
450
- end
502
+ contact = find_contact(params[:contact_id])
503
+ contact.remove_media(params[:id])
504
+ status 204
451
505
  end
452
506
 
453
507
  # Returns the timezone of a contact
454
508
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_timezone
455
509
  get('/contacts/:contact_id/timezone') do
456
510
  content_type :json
457
- find_contact(params[:contact_id]) do |contact|
458
- contact.timezone.name.to_json
459
- end
511
+
512
+ contact = find_contact(params[:contact_id])
513
+ contact.timezone.name.to_json
460
514
  end
461
515
 
462
516
  # Sets the timezone of a contact
463
517
  # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
464
518
  put('/contacts/:contact_id/timezone') do
465
519
  content_type :json
466
- find_contact(params[:contact_id]) do |contact|
467
- contact.timezone = params[:timezone]
468
- contact.timezone.name.to_json
469
- end
520
+
521
+ contact = find_contact(params[:contact_id])
522
+ contact.timezone = params[:timezone]
523
+ contact.timezone.name.to_json
470
524
  end
471
525
 
472
526
  # Removes the timezone of a contact
473
527
  # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
474
528
  delete('/contacts/:contact_id/timezone') do
475
- find_contact(params[:contact_id]) do |contact|
476
- contact.timezone = nil
477
- status 204
478
- end
529
+ contact = find_contact(params[:contact_id])
530
+ contact.timezone = nil
531
+ status 204
479
532
  end
480
533
 
481
534
  post '/contacts/:contact_id/tags' do
482
535
  content_type :json
483
- check_tags(params[:tag]) do |tags|
484
- find_contact(params[:contact_id]) do |contact|
485
- contact.add_tags(*tags)
486
- contact.tags.to_json
487
- end
488
- end
536
+
537
+ tags = find_tags(params[:tag])
538
+ contact = find_contact(params[:contact_id])
539
+ contact.add_tags(*tags)
540
+ contact.tags.to_json
489
541
  end
490
542
 
491
543
  post '/contacts/:contact_id/entity_tags' do
492
544
  content_type :json
493
- find_contact(params[:contact_id]) do |contact|
494
- contact.entities.map {|e| e[:entity]}.each do |entity|
495
- next unless tags = params[:entity][entity.name]
496
- entity.add_tags(*tags)
497
- end
498
- contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
499
- [et[:entity].name, et[:tags]]
500
- }
501
- contact_ent_tag.to_json
545
+ contact = find_contact(params[:contact_id])
546
+ contact.entities.map {|e| e[:entity]}.each do |entity|
547
+ next unless tags = params[:entity][entity.name]
548
+ entity.add_tags(*tags)
502
549
  end
550
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
551
+ [et[:entity].name, et[:tags]]
552
+ }
553
+ contact_ent_tag.to_json
503
554
  end
504
555
 
505
556
  delete '/contacts/:contact_id/tags' do
506
- content_type :json
507
- check_tags(params[:tag]) do |tags|
508
- find_contact(params[:contact_id]) do |contact|
509
- contact.delete_tags(*tags)
510
- status 204
511
- end
512
- end
557
+ tags = find_tags(params[:tag])
558
+ contact = find_contact(params[:contact_id])
559
+ contact.delete_tags(*tags)
560
+ status 204
513
561
  end
514
562
 
515
563
  delete '/contacts/:contact_id/entity_tags' do
516
- content_type :json
517
- find_contact(params[:contact_id]) do |contact|
518
- contact.entities.map {|e| e[:entity]}.each do |entity|
519
- next unless tags = params[:entity][entity.name]
520
- entity.delete_tags(*tags)
521
- end
522
- status 204
564
+ contact = find_contact(params[:contact_id])
565
+ contact.entities.map {|e| e[:entity]}.each do |entity|
566
+ next unless tags = params[:entity][entity.name]
567
+ entity.delete_tags(*tags)
523
568
  end
569
+ status 204
524
570
  end
525
571
 
526
572
  get '/contacts/:contact_id/tags' do
527
573
  content_type :json
528
- find_contact(params[:contact_id]) do |contact|
529
- contact.tags.to_json
530
- end
574
+
575
+ contact = find_contact(params[:contact_id])
576
+ contact.tags.to_json
531
577
  end
532
578
 
533
579
  get '/contacts/:contact_id/entity_tags' do
534
580
  content_type :json
535
- find_contact(params[:contact_id]) do |contact|
536
- contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
537
- [et[:entity].name, et[:tags]]
538
- }
539
- contact_ent_tag.to_json
540
- end
581
+
582
+ contact = find_contact(params[:contact_id])
583
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
584
+ [et[:entity].name, et[:tags]]
585
+ }
586
+ contact_ent_tag.to_json
541
587
  end
542
588
 
543
589
  post '/entities/:entity/tags' do
544
590
  content_type :json
545
- check_tags(params[:tag]) do |tags|
546
- find_entity(params[:entity]) do |entity|
547
- entity.add_tags(*tags)
548
- entity.tags.to_json
549
- end
550
- end
591
+
592
+ tags = find_tags(params[:tag])
593
+ entity = find_entity(params[:entity])
594
+ entity.add_tags(*tags)
595
+ entity.tags.to_json
551
596
  end
552
597
 
553
598
  delete '/entities/:entity/tags' do
554
- content_type :json
555
- check_tags(params[:tag]) do |tags|
556
- find_entity(params[:entity]) do |entity|
557
- entity.delete_tags(*tags)
558
- status 204
559
- end
560
- end
599
+ tags = find_tags(params[:tag])
600
+ entity = find_entity(params[:entity])
601
+ entity.delete_tags(*tags)
602
+ status 204
561
603
  end
562
604
 
563
605
  get '/entities/:entity/tags' do
564
606
  content_type :json
565
- find_entity(params[:entity]) do |entity|
566
- entity.tags.to_json
567
- end
607
+
608
+ entity = find_entity(params[:entity])
609
+ entity.tags.to_json
568
610
  end
569
611
 
570
612
  not_found do
571
613
  logger.debug("in not_found :-(")
572
- error(404, "not routable")
614
+ err(404, "not routable")
615
+ end
616
+
617
+ error Flapjack::Gateways::API::ContactNotFound do
618
+ e = env['sinatra.error']
619
+ err(403, "could not find contact '#{e.contact_id}'")
620
+ end
621
+
622
+ error Flapjack::Gateways::API::NotificationRuleNotFound do
623
+ e = env['sinatra.error']
624
+ err(403, "could not find notification rule '#{e.rule_id}'")
625
+ end
626
+
627
+ error Flapjack::Gateways::API::EntityNotFound do
628
+ e = env['sinatra.error']
629
+ err(403, "could not find entity '#{e.entity}'")
630
+ end
631
+
632
+ error Flapjack::Gateways::API::EntityCheckNotFound do
633
+ e = env['sinatra.error']
634
+ err(403, "could not find entity check '#{e.check}'")
573
635
  end
574
636
 
575
637
  private
576
638
 
577
- def error(status, *msg)
639
+ def err(status, *msg)
578
640
  msg_str = msg.join(", ")
579
641
  logger.info "Error: #{msg_str}"
580
642
  [status, {}, {:errors => msg}.to_json]
581
643
  end
582
644
 
583
- def entity_check_status(entity, check)
584
- entity_check = Flapjack::Data::EntityCheck.for_entity(entity,
585
- check, :redis => redis)
586
- return if entity_check.nil?
587
- {'name' => check,
588
- 'state' => entity_check.state,
589
- 'summary' => entity_check.summary,
590
- 'details' => entity_check.details,
591
- 'in_unscheduled_maintenance' => entity_check.in_unscheduled_maintenance?,
592
- 'in_scheduled_maintenance' => entity_check.in_scheduled_maintenance?,
593
- 'last_update' => entity_check.last_update,
594
- 'last_problem_notification' => entity_check.last_problem_notification,
595
- 'last_recovery_notification' => entity_check.last_recovery_notification,
596
- 'last_acknowledgement_notification' => entity_check.last_acknowledgement_notification}
597
- end
598
-
599
- # following a callback-heavy pattern -- feels like nodejs :)
600
- def find_contact(contact_id, &block)
601
- contact = Flapjack::Data::Contact.find_by_id(contact_id.to_s, :redis => redis, :logger => logger)
602
- return(yield(contact)) if contact
603
- error(404, "could not find contact with id '#{contact_id}'")
645
+ def find_contact(contact_id)
646
+ contact = Flapjack::Data::Contact.find_by_id(contact_id, :logger => logger, :redis => redis)
647
+ raise Flapjack::Gateways::API::ContactNotFound.new(contact_id) if contact.nil?
648
+ contact
604
649
  end
605
650
 
606
- def find_rule(rule_id, &block)
607
- rule = Flapjack::Data::NotificationRule.find_by_id(rule_id, :redis => redis, :logger => logger)
608
- return(yield(rule)) if rule
609
- error(404, "could not find notification rule with id '#{rule_id}'")
651
+ def find_rule(rule_id)
652
+ rule = Flapjack::Data::NotificationRule.find_by_id(rule_id, :logger => logger, :redis => redis)
653
+ raise Flapjack::Gateways::API::NotificationRuleNotFound.new(rule_id) if rule.nil?
654
+ rule
610
655
  end
611
656
 
612
- def find_entity(entity_name, &block)
657
+ def find_entity(entity_name)
613
658
  entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis)
614
- return(yield(entity)) if entity
615
- error(404, "could not find entity '#{entity_name}'")
659
+ raise Flapjack::Gateways::API::EntityNotFound.new(entity_name) if entity.nil?
660
+ entity
616
661
  end
617
662
 
618
- def find_entity_check(entity, check, &block)
663
+ def find_entity_check(entity, check)
619
664
  entity_check = Flapjack::Data::EntityCheck.for_entity(entity,
620
665
  check, :redis => redis)
621
- return(yield(entity_check)) if entity_check
622
- error(404, "could not find entity check '#{entity.name}:#{check}'")
666
+ raise Flapjack::Gateways::API::EntityCheckNotFound.new(entity, check) if entity_check.nil?
667
+ entity_check
623
668
  end
624
669
 
625
- def find_api_presenter(entity_name, check, &block)
626
- find_entity(entity_name) do |entity|
627
- if check
628
- find_entity_check(entity, check) do |entity_check|
629
- yield(Flapjack::Gateways::API::EntityCheckPresenter.new(entity_check))
670
+ def entities_and_checks(entity_name, check)
671
+ if entity_name
672
+ # backwards-compatible, single entity or entity&check from route
673
+ entities = check ? nil : [entity_name]
674
+ checks = check ? {entity_name => check} : nil
675
+ else
676
+ # new and improved bulk API queries
677
+ entities = params[:entity]
678
+ checks = params[:check]
679
+ entities = [entities] unless entities.nil? || entities.is_a?(Array)
680
+ # TODO err if checks isn't a Hash (similar rules as in flapjack-diner)
681
+ end
682
+ [entities, checks]
683
+ end
684
+
685
+ def bulk_api_check_action(entities, entity_checks, action, params = {})
686
+ unless entities.nil? || entities.empty?
687
+ entities.each do |entity_name|
688
+ entity = find_entity(entity_name)
689
+ checks = entity.check_list.sort
690
+ checks.each do |check|
691
+ action.call( find_entity_check(entity, check) )
630
692
  end
631
- else
632
- yield(Flapjack::Gateways::API::EntityPresenter.new(entity, :redis => redis))
633
693
  end
634
694
  end
695
+
696
+ unless entity_checks.nil? || entity_checks.empty?
697
+ entity_checks.each_pair do |entity_name, checks|
698
+ entity = find_entity(entity_name)
699
+ checks = [checks] unless checks.is_a?(Array)
700
+ checks.each do |check|
701
+ action.call( find_entity_check(entity, check) )
702
+ end
703
+ end
704
+ end
705
+ end
706
+
707
+ def present_api_results(entities, entity_checks, result_type, &block)
708
+ result = []
709
+
710
+ unless entities.nil? || entities.empty?
711
+ result += entities.collect {|entity_name|
712
+ entity = find_entity(entity_name)
713
+ yield(Flapjack::Gateways::API::EntityPresenter.new(entity, :redis => redis))
714
+ }.flatten(1)
715
+ end
716
+
717
+ unless entity_checks.nil? || entity_checks.empty?
718
+ result += entity_checks.inject([]) {|memo, (entity_name, checks)|
719
+ checks = [checks] unless checks.is_a?(Array)
720
+ entity = find_entity(entity_name)
721
+ memo += checks.collect {|check|
722
+ entity_check = find_entity_check(entity, check)
723
+ {:entity => entity_name,
724
+ :check => check,
725
+ result_type.to_sym => yield(Flapjack::Gateways::API::EntityCheckPresenter.new(entity_check, :redis => redis))
726
+ }
727
+ }
728
+ }.flatten(1)
729
+ end
730
+
731
+ result
635
732
  end
636
733
 
637
- def check_tags(tags)
638
- return(yield(tags)) unless tags.nil? || tags.empty?
639
- error(403, "no tag params passed")
734
+ def find_tags(tags)
735
+ halt err(403, "no tags") if tags.nil? || tags.empty?
736
+ tags
640
737
  end
641
738
 
642
739
  # NB: casts to UTC before converting to a timestamp