flapjack 0.7.14 → 0.7.15

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 (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