flapjack 0.7.35 → 0.8.0

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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -1
  3. data/Gemfile +3 -4
  4. data/Guardfile +1 -1
  5. data/README.md +38 -19
  6. data/Rakefile +1 -3
  7. data/etc/flapjack_config.yaml.example +11 -1
  8. data/features/steps/cli_steps.rb +3 -3
  9. data/features/steps/events_steps.rb +7 -6
  10. data/features/steps/flapjack-netsaint-parser_steps.rb +8 -8
  11. data/features/steps/notifications_steps.rb +10 -10
  12. data/features/steps/packaging-lintian_steps.rb +5 -9
  13. data/features/steps/time_travel_steps.rb +1 -1
  14. data/flapjack.gemspec +4 -3
  15. data/lib/flapjack/data/contact.rb +78 -6
  16. data/lib/flapjack/data/entity.rb +11 -2
  17. data/lib/flapjack/data/notification_rule.rb +67 -59
  18. data/lib/flapjack/data/semaphore.rb +44 -0
  19. data/lib/flapjack/gateways/api.rb +24 -28
  20. data/lib/flapjack/gateways/api/contact_methods.rb +1 -2
  21. data/lib/flapjack/gateways/api/entity_methods.rb +3 -3
  22. data/lib/flapjack/gateways/jsonapi.rb +249 -0
  23. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +544 -0
  24. data/lib/flapjack/gateways/jsonapi/entity_check_presenter.rb +217 -0
  25. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +350 -0
  26. data/lib/flapjack/gateways/jsonapi/entity_presenter.rb +75 -0
  27. data/lib/flapjack/gateways/jsonapi/rack/json_params_parser.rb +32 -0
  28. data/lib/flapjack/gateways/web.rb +78 -12
  29. data/lib/flapjack/gateways/web/public/css/bootstrap-theme.css +397 -0
  30. data/lib/flapjack/gateways/web/public/css/bootstrap-theme.min.css +7 -0
  31. data/lib/flapjack/gateways/web/public/css/bootstrap.css +7118 -0
  32. data/lib/flapjack/gateways/web/public/css/bootstrap.min.css +6 -8
  33. data/lib/flapjack/gateways/web/public/css/font-awesome.css +1338 -0
  34. data/lib/flapjack/gateways/web/public/css/font-awesome.min.css +4 -0
  35. data/lib/flapjack/gateways/web/public/css/screen.css +80 -0
  36. data/lib/flapjack/gateways/web/public/css/select2-bootstrap.css +87 -0
  37. data/lib/flapjack/gateways/web/public/css/select2.css +615 -0
  38. data/lib/flapjack/gateways/web/public/fonts/FontAwesome.otf +0 -0
  39. data/lib/flapjack/gateways/web/public/fonts/fontawesome-webfont.eot +0 -0
  40. data/lib/flapjack/gateways/web/public/fonts/fontawesome-webfont.svg +414 -0
  41. data/lib/flapjack/gateways/web/public/fonts/fontawesome-webfont.ttf +0 -0
  42. data/lib/flapjack/gateways/web/public/fonts/fontawesome-webfont.woff +0 -0
  43. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.eot +0 -0
  44. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.svg +229 -0
  45. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  46. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.woff +0 -0
  47. data/lib/flapjack/gateways/web/public/img/flapjack-2013-notext-transparent-300-300.png +0 -0
  48. data/lib/flapjack/gateways/web/public/img/select2.png +0 -0
  49. data/lib/flapjack/gateways/web/public/img/select2x2.png +0 -0
  50. data/lib/flapjack/gateways/web/public/js/backbone-min.js +2 -0
  51. data/lib/flapjack/gateways/web/public/js/backbone.js +1581 -0
  52. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +75 -0
  53. data/lib/flapjack/gateways/web/public/js/bootstrap.js +2276 -0
  54. data/lib/flapjack/gateways/web/public/js/contacts.js +225 -0
  55. data/lib/flapjack/gateways/web/public/js/jquery-1.10.2.js +9789 -0
  56. data/lib/flapjack/gateways/web/public/js/jquery-1.10.2.min.js +6 -0
  57. data/lib/flapjack/gateways/web/public/js/select2.js +3255 -0
  58. data/lib/flapjack/gateways/web/public/js/select2.min.js +22 -0
  59. data/lib/flapjack/gateways/web/public/js/underscore-min.js +6 -0
  60. data/lib/flapjack/gateways/web/public/js/underscore.js +1276 -0
  61. data/lib/flapjack/gateways/web/views/check.html.erb +423 -193
  62. data/lib/flapjack/gateways/web/views/checks.html.erb +51 -71
  63. data/lib/flapjack/gateways/web/views/contact.html.erb +142 -164
  64. data/lib/flapjack/gateways/web/views/contacts.html.erb +20 -40
  65. data/lib/flapjack/gateways/web/views/edit_contacts.html.erb +83 -0
  66. data/lib/flapjack/gateways/web/views/entities.html.erb +18 -37
  67. data/lib/flapjack/gateways/web/views/entity.html.erb +46 -65
  68. data/lib/flapjack/gateways/web/views/index.html.erb +6 -27
  69. data/lib/flapjack/gateways/web/views/layout.erb +95 -0
  70. data/lib/flapjack/gateways/web/views/self_stats.html.erb +100 -114
  71. data/lib/flapjack/pikelet.rb +4 -2
  72. data/lib/flapjack/version.rb +1 -1
  73. data/spec/lib/flapjack/coordinator_spec.rb +120 -120
  74. data/spec/lib/flapjack/data/contact_spec.rb +66 -58
  75. data/spec/lib/flapjack/data/entity_check_spec.rb +179 -179
  76. data/spec/lib/flapjack/data/entity_spec.rb +71 -71
  77. data/spec/lib/flapjack/data/event_spec.rb +34 -30
  78. data/spec/lib/flapjack/data/message_spec.rb +6 -6
  79. data/spec/lib/flapjack/data/notification_rule_spec.rb +24 -24
  80. data/spec/lib/flapjack/data/notification_spec.rb +19 -19
  81. data/spec/lib/flapjack/data/semaphore_spec.rb +24 -0
  82. data/spec/lib/flapjack/data/tag_spec.rb +11 -10
  83. data/spec/lib/flapjack/gateways/api/contact_methods_spec.rb +201 -201
  84. data/spec/lib/flapjack/gateways/api/entity_check_presenter_spec.rb +55 -55
  85. data/spec/lib/flapjack/gateways/api/entity_methods_spec.rb +257 -257
  86. data/spec/lib/flapjack/gateways/api/entity_presenter_spec.rb +26 -26
  87. data/spec/lib/flapjack/gateways/api_spec.rb +1 -1
  88. data/spec/lib/flapjack/gateways/email_spec.rb +4 -4
  89. data/spec/lib/flapjack/gateways/jabber_spec.rb +77 -77
  90. data/spec/lib/flapjack/gateways/jsonapi/contact_methods_spec.rb +830 -0
  91. data/spec/lib/flapjack/gateways/jsonapi/entity_check_presenter_spec.rb +211 -0
  92. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +863 -0
  93. data/spec/lib/flapjack/gateways/jsonapi/entity_presenter_spec.rb +108 -0
  94. data/spec/lib/flapjack/gateways/jsonapi_spec.rb +8 -0
  95. data/spec/lib/flapjack/gateways/oobetet_spec.rb +35 -35
  96. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +40 -40
  97. data/spec/lib/flapjack/gateways/sms_messagenet_spec.rb +3 -3
  98. data/spec/lib/flapjack/gateways/web/views/check.html.erb_spec.rb +1 -1
  99. data/spec/lib/flapjack/gateways/web/views/contact.html.erb_spec.rb +5 -5
  100. data/spec/lib/flapjack/gateways/web/views/index.html.erb_spec.rb +1 -1
  101. data/spec/lib/flapjack/gateways/web_spec.rb +73 -74
  102. data/spec/lib/flapjack/logger_spec.rb +13 -13
  103. data/spec/lib/flapjack/pikelet_spec.rb +33 -33
  104. data/spec/lib/flapjack/processor_spec.rb +22 -22
  105. data/spec/lib/flapjack/redis_pool_spec.rb +1 -1
  106. data/spec/lib/flapjack/utility_spec.rb +12 -12
  107. data/spec/spec_helper.rb +9 -9
  108. data/spec/support/erb_view_helper.rb +4 -0
  109. metadata +107 -96
  110. data/lib/flapjack/gateways/web/public/css/flapjack.css +0 -49
  111. data/lib/flapjack/gateways/web/views/_css.html.erb +0 -42
  112. data/lib/flapjack/gateways/web/views/_foot.html.erb +0 -3
  113. data/lib/flapjack/gateways/web/views/_head.html.erb +0 -5
  114. data/lib/flapjack/gateways/web/views/_nav.html.erb +0 -10
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Formats entity/check data for presentation by the API methods in Flapjack::Gateways::API.
4
+
5
+ require 'sinatra/base'
6
+
7
+ require 'flapjack/data/entity_check'
8
+
9
+ module Flapjack
10
+
11
+ module Gateways
12
+
13
+ class JSONAPI < Sinatra::Base
14
+
15
+ class EntityCheckPresenter
16
+
17
+ def initialize(entity_check)
18
+ @entity_check = entity_check
19
+ end
20
+
21
+ def status
22
+ {'name' => @entity_check.check,
23
+ 'state' => @entity_check.state,
24
+ 'enabled' => @entity_check.enabled?,
25
+ 'summary' => @entity_check.summary,
26
+ 'details' => @entity_check.details,
27
+ 'in_unscheduled_maintenance' => @entity_check.in_unscheduled_maintenance?,
28
+ 'in_scheduled_maintenance' => @entity_check.in_scheduled_maintenance?,
29
+ 'last_update' => @entity_check.last_update,
30
+ 'last_problem_notification' => @entity_check.last_notification_for_state(:problem)[:timestamp],
31
+ 'last_recovery_notification' => @entity_check.last_notification_for_state(:recovery)[:timestamp],
32
+ 'last_acknowledgement_notification' => @entity_check.last_notification_for_state(:acknowledgement)[:timestamp]}
33
+ end
34
+
35
+ def outages(start_time, end_time, options = {})
36
+ # hist_states is an array of hashes, with [state, timestamp, summary] keys
37
+ hist_states = @entity_check.historical_states(start_time, end_time)
38
+ return hist_states if hist_states.empty?
39
+
40
+ initial = @entity_check.historical_state_before(hist_states.first[:timestamp])
41
+ hist_states.unshift(initial) if initial
42
+
43
+ # TODO the following works, but isn't the neatest
44
+ num_states = hist_states.size
45
+
46
+ index = 0
47
+ result = []
48
+ obj = nil
49
+
50
+ while index < num_states do
51
+ last_obj = obj
52
+ obj = hist_states[index]
53
+ index += 1
54
+
55
+ next if obj[:state] == 'ok'
56
+
57
+ if last_obj && (last_obj[:state] == obj[:state])
58
+ # TODO maybe build up arrays of these instead, and leave calling
59
+ # classes to join them together if needed?
60
+ result.last[:summary] << " / #{obj[:summary]}"
61
+ result.last[:details] << " / #{obj[:details]}"
62
+ next
63
+ end
64
+
65
+ ts = obj[:timestamp]
66
+
67
+ obj_st = (last_obj || !start_time) ? ts : [ts, start_time].max
68
+
69
+ next_ts_obj = hist_states[index..-1].detect {|hs| hs[:state] != obj[:state] }
70
+ obj_et = next_ts_obj ? next_ts_obj[:timestamp] : end_time
71
+
72
+ obj_dur = obj_et ? obj_et - obj_st : nil
73
+
74
+ result << {:state => obj[:state],
75
+ :start_time => obj_st,
76
+ :end_time => obj_et,
77
+ :duration => obj_dur,
78
+ :summary => obj[:summary] || '',
79
+ :details => obj[:details] || ''
80
+ }
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ def unscheduled_maintenances(start_time, end_time)
87
+ # unsched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
88
+ unsched_maintenance = @entity_check.maintenances(start_time, end_time,
89
+ :scheduled => false)
90
+
91
+ # to see if we start in an unscheduled maintenance period, we must check all unscheduled
92
+ # maintenances before the period and their durations
93
+ start_in_unsched = start_time.nil? ? [] :
94
+ @entity_check.maintenances(nil, start_time, :scheduled => false).select {|pu|
95
+ pu[:end_time] >= start_time
96
+ }
97
+
98
+ start_in_unsched + unsched_maintenance
99
+ end
100
+
101
+ def scheduled_maintenances(start_time, end_time)
102
+ # sched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
103
+ sched_maintenance = @entity_check.maintenances(start_time, end_time,
104
+ :scheduled => true)
105
+
106
+ # to see if we start in a scheduled maintenance period, we must check all scheduled
107
+ # maintenances before the period and their durations
108
+ start_in_sched = start_time.nil? ? [] :
109
+ @entity_check.maintenances(nil, start_time, :scheduled => true).select {|ps|
110
+ ps[:end_time] >= start_time
111
+ }
112
+
113
+ start_in_sched + sched_maintenance
114
+ end
115
+
116
+ # TODO test whether the below overlapping logic is prone to off-by-one
117
+ # errors; the numbers may line up more neatly if we consider outages to
118
+ # start one second after the maintenance period ends.
119
+ #
120
+ # TODO test performance with larger data sets
121
+ def downtime(start_time, end_time)
122
+ sched_maintenances = scheduled_maintenances(start_time, end_time)
123
+
124
+ outs = outages(start_time, end_time)
125
+
126
+ total_secs = {}
127
+ percentages = {}
128
+
129
+ outs.collect {|obj| obj[:state]}.uniq.each do |st|
130
+ total_secs[st] = 0
131
+ percentages[st] = (start_time.nil? || end_time.nil?) ? nil : 0
132
+ end
133
+
134
+ unless outs.empty?
135
+
136
+ # Initially we need to check for cases where a scheduled
137
+ # maintenance period is fully covered by an outage period.
138
+ # We then create two new outage periods to cover the time around
139
+ # the scheduled maintenance period, and remove the original.
140
+
141
+ sched_maintenances.each do |sm|
142
+
143
+ split_outs = []
144
+
145
+ outs.each { |o|
146
+ next unless o[:end_time] && (o[:start_time] < sm[:start_time]) &&
147
+ (o[:end_time] > sm[:end_time])
148
+ o[:delete] = true
149
+ split_outs += [{:state => o[:state],
150
+ :start_time => o[:start_time],
151
+ :end_time => sm[:start_time],
152
+ :duration => sm[:start_time] - o[:start_time],
153
+ :summary => "#{o[:summary]} [split start]"},
154
+ {:state => o[:state],
155
+ :start_time => sm[:end_time],
156
+ :end_time => o[:end_time],
157
+ :duration => o[:end_time] - sm[:end_time],
158
+ :summary => "#{o[:summary]} [split finish]"}]
159
+ }
160
+
161
+ outs.reject! {|o| o[:delete]}
162
+ outs += split_outs
163
+ # not strictly necessary to keep the data sorted, but
164
+ # will make more sense while debgging
165
+ outs.sort! {|a,b| a[:start_time] <=> b[:start_time]}
166
+ end
167
+
168
+ sched_maintenances.each do |sm|
169
+
170
+ outs.each do |o|
171
+ next unless o[:end_time] && (sm[:start_time] < o[:end_time]) &&
172
+ (sm[:end_time] > o[:start_time])
173
+
174
+ if sm[:start_time] <= o[:start_time] &&
175
+ sm[:end_time] >= o[:end_time]
176
+
177
+ # outage is fully overlapped by the scheduled maintenance
178
+ o[:delete] = true
179
+
180
+ elsif sm[:start_time] <= o[:start_time]
181
+ # partially overlapping on the earlier side
182
+ o[:start_time] = sm[:end_time]
183
+ o[:duration] = o[:end_time] - o[:start_time]
184
+ elsif sm[:end_time] >= o[:end_time]
185
+ # partially overlapping on the later side
186
+ o[:end_time] = sm[:start_time]
187
+ o[:duration] = o[:end_time] - o[:start_time]
188
+ end
189
+ end
190
+
191
+ outs.reject! {|o| o[:delete]}
192
+ end
193
+
194
+ total_secs = outs.inject(total_secs) {|ret, o|
195
+ ret[o[:state]] += o[:duration] if o[:duration]
196
+ ret
197
+ }
198
+
199
+ unless (start_time.nil? || end_time.nil?)
200
+ total_secs.each_pair do |st, ts|
201
+ percentages[st] = (total_secs[st] * 100.0) / (end_time.to_f - start_time.to_f)
202
+ end
203
+ total_secs['ok'] = (end_time - start_time) - total_secs.values.reduce(:+)
204
+ percentages['ok'] = 100 - percentages.values.reduce(:+)
205
+ end
206
+ end
207
+
208
+ {:total_seconds => total_secs, :percentages => percentages, :downtime => outs}
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ end
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/entity'
6
+ require 'flapjack/data/entity_check'
7
+
8
+ require 'flapjack/gateways/jsonapi/entity_presenter'
9
+ require 'flapjack/gateways/jsonapi/entity_check_presenter'
10
+
11
+ module Flapjack
12
+
13
+ module Gateways
14
+
15
+ class JSONAPI < Sinatra::Base
16
+
17
+ module EntityMethods
18
+
19
+ module Helpers
20
+
21
+ def find_entity(entity_name)
22
+ entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis)
23
+ raise Flapjack::Gateways::JSONAPI::EntityNotFound.new(entity_name) if entity.nil?
24
+ entity
25
+ end
26
+
27
+ def find_entity_check(entity, check)
28
+ entity_check = Flapjack::Data::EntityCheck.for_entity(entity,
29
+ check, :redis => redis)
30
+ raise Flapjack::Gateways::JSONAPI::EntityCheckNotFound.new(entity, check) if entity_check.nil?
31
+ entity_check
32
+ end
33
+
34
+ def find_tags(tags)
35
+ halt err(403, "no tags") if tags.nil? || tags.empty?
36
+ tags
37
+ end
38
+
39
+ def entities_and_checks(entity_name, check)
40
+ if entity_name
41
+ # backwards-compatible, single entity or entity&check from route
42
+ entities = check ? nil : [entity_name]
43
+ checks = check ? {entity_name => check} : nil
44
+ else
45
+ # new and improved bulk API queries
46
+ entities = params[:entity]
47
+ checks = params[:check]
48
+ entities = [entities] unless entities.nil? || entities.is_a?(Array)
49
+ # TODO err if checks isn't a Hash (similar rules as in flapjack-diner)
50
+ end
51
+ [entities, checks]
52
+ end
53
+
54
+ def bulk_api_check_action(entities, entity_checks, action, params = {})
55
+ unless entities.nil? || entities.empty?
56
+ entities.each do |entity_name|
57
+ entity = find_entity(entity_name)
58
+ checks = entity.check_list.sort
59
+ checks.each do |check|
60
+ action.call( find_entity_check(entity, check) )
61
+ end
62
+ end
63
+ end
64
+
65
+ unless entity_checks.nil? || entity_checks.empty?
66
+ entity_checks.each_pair do |entity_name, checks|
67
+ entity = find_entity(entity_name)
68
+ checks = [checks] unless checks.is_a?(Array)
69
+ checks.each do |check|
70
+ action.call( find_entity_check(entity, check) )
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def present_api_results(entities, entity_checks, result_type, &block)
77
+ result = []
78
+
79
+ unless entities.nil? || entities.empty?
80
+ result += entities.collect {|entity_name|
81
+ entity = find_entity(entity_name)
82
+ yield(Flapjack::Gateways::JSONAPI::EntityPresenter.new(entity, :redis => redis))
83
+ }.flatten(1)
84
+ end
85
+
86
+ unless entity_checks.nil? || entity_checks.empty?
87
+ result += entity_checks.inject([]) {|memo, (entity_name, checks)|
88
+ checks = [checks] unless checks.is_a?(Array)
89
+ entity = find_entity(entity_name)
90
+ memo += checks.collect {|check|
91
+ entity_check = find_entity_check(entity, check)
92
+ {:entity => entity_name,
93
+ :check => check,
94
+ result_type.to_sym => yield(Flapjack::Gateways::JSONAPI::EntityCheckPresenter.new(entity_check))
95
+ }
96
+ }
97
+ }.flatten(1)
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ # NB: casts to UTC before converting to a timestamp
104
+ def validate_and_parsetime(value)
105
+ return unless value
106
+ Time.iso8601(value).getutc.to_i
107
+ rescue ArgumentError => e
108
+ logger.error "Couldn't parse time from '#{value}'"
109
+ nil
110
+ end
111
+
112
+ end
113
+
114
+ # used for backwards-compatible route matching below
115
+ ENTITY_CHECK_FRAGMENT = '(?:/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(.+))?)?'
116
+
117
+ def self.registered(app)
118
+
119
+ app.helpers Flapjack::Gateways::JSONAPI::EntityMethods::Helpers
120
+
121
+ app.get '/entities' do
122
+ content_type :json
123
+ cors_headers
124
+
125
+ entities_json = Flapjack::Data::Entity.all(:redis => redis).collect {|e|
126
+ presenter = Flapjack::Gateways::JSONAPI::EntityPresenter.new(e, :redis => redis)
127
+ id = (e.id.respond_to?(:length) && e.id.length > 0) ? e.id : e.name
128
+ {'id' => id, 'name' => e.name, 'checks' => presenter.status }.to_json
129
+ }.join(',')
130
+
131
+ '{"entities":[' + entities_json + ']}'
132
+ end
133
+
134
+ app.get '/checks/:entity' do
135
+ content_type :json
136
+ entity = find_entity(params[:entity])
137
+ entity.check_list.to_json
138
+ end
139
+
140
+ app.get %r{/status#{ENTITY_CHECK_FRAGMENT}} do
141
+ content_type :json
142
+
143
+ captures = params[:captures] || []
144
+ entity_name = captures[0]
145
+ check = captures[1]
146
+
147
+ entities, checks = entities_and_checks(entity_name, check)
148
+
149
+ results = present_api_results(entities, checks, 'status') {|presenter|
150
+ presenter.status
151
+ }
152
+
153
+ if entity_name
154
+ # compatible with previous data format
155
+ results = results.collect {|status_h| status_h[:status]}
156
+ check ? results.first.to_json : "[" + results.map {|r| r.to_json }.join(',') + "]"
157
+ else
158
+ # new and improved data format which reflects the request param structure
159
+ "[" + results.map {|r| r.to_json }.join(',') + "]"
160
+ end
161
+ end
162
+
163
+ app.get %r{/((?:outages|(?:un)?scheduled_maintenances|downtime))#{ENTITY_CHECK_FRAGMENT}} do
164
+ action = params[:captures][0].to_sym
165
+ entity_name = params[:captures][1]
166
+ check = params[:captures][2]
167
+
168
+ entities, checks = entities_and_checks(entity_name, check)
169
+
170
+ start_time = validate_and_parsetime(params[:start_time])
171
+ end_time = validate_and_parsetime(params[:end_time])
172
+
173
+ results = present_api_results(entities, checks, action) {|presenter|
174
+ presenter.send(action, start_time, end_time)
175
+ }
176
+
177
+ if check
178
+ # compatible with previous data format
179
+ results.first[action].to_json
180
+ elsif entity_name
181
+ # compatible with previous data format
182
+ rename = {:unscheduled_maintenances => :unscheduled_maintenance,
183
+ :scheduled_maintenances => :scheduled_maintenance}
184
+ drop = [:entity]
185
+ results.collect{|r|
186
+ r.inject({}) {|memo, (k, v)|
187
+ if new_k = rename[k]
188
+ memo[new_k] = v
189
+ elsif !drop.include?(k)
190
+ memo[k] = v
191
+ end
192
+ memo
193
+ }
194
+ }.to_json
195
+ else
196
+ # new and improved data format which reflects the request param structure
197
+ results.to_json
198
+ end
199
+ end
200
+
201
+ # create a scheduled maintenance period for a check on an entity
202
+ app.post %r{/scheduled_maintenances#{ENTITY_CHECK_FRAGMENT}} do
203
+
204
+ captures = params[:captures] || []
205
+ entity_name = captures[0]
206
+ check = captures[1]
207
+
208
+ entities, checks = entities_and_checks(entity_name, check)
209
+
210
+ start_time = validate_and_parsetime(params[:start_time])
211
+ halt( err(403, "start time must be provided") ) unless start_time
212
+
213
+ act_proc = proc {|entity_check|
214
+ entity_check.create_scheduled_maintenance(start_time,
215
+ params[:duration].to_i, :summary => params[:summary])
216
+ }
217
+
218
+ bulk_api_check_action(entities, checks, act_proc)
219
+ status 204
220
+ end
221
+
222
+ # create an acknowledgement for a service on an entity
223
+ # NB currently, this does not acknowledge a specific failure event, just
224
+ # the entity-check as a whole
225
+ app.post %r{/acknowledgements#{ENTITY_CHECK_FRAGMENT}} do
226
+ captures = params[:captures] || []
227
+ entity_name = captures[0]
228
+ check = captures[1]
229
+
230
+ entities, checks = entities_and_checks(entity_name, check)
231
+
232
+ dur = params[:duration] ? params[:duration].to_i : nil
233
+ duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur
234
+ summary = params[:summary]
235
+
236
+ opts = {'duration' => duration}
237
+ opts['summary'] = summary if summary
238
+
239
+ act_proc = proc {|entity_check|
240
+ Flapjack::Data::Event.create_acknowledgement(
241
+ entity_check.entity_name, entity_check.check,
242
+ :summary => params[:summary],
243
+ :duration => duration,
244
+ :redis => redis)
245
+ }
246
+
247
+ bulk_api_check_action(entities, checks, act_proc)
248
+ status 204
249
+ end
250
+
251
+ app.delete %r{/((?:un)?scheduled_maintenances)} do
252
+ action = params[:captures][0]
253
+
254
+ # no backwards-compatible mode here, it's a new method
255
+ entities, checks = entities_and_checks(nil, nil)
256
+
257
+ act_proc = case action
258
+ when 'scheduled_maintenances'
259
+ start_time = validate_and_parsetime(params[:start_time])
260
+ halt( err(403, "start time must be provided") ) unless start_time
261
+ opts = {}
262
+ proc {|entity_check| entity_check.end_scheduled_maintenance(start_time.to_i) }
263
+ when 'unscheduled_maintenances'
264
+ end_time = validate_and_parsetime(params[:end_time]) || Time.now
265
+ proc {|entity_check| entity_check.end_unscheduled_maintenance(end_time.to_i) }
266
+ end
267
+
268
+ bulk_api_check_action(entities, checks, act_proc)
269
+ status 204
270
+ end
271
+
272
+ app.post %r{/test_notifications#{ENTITY_CHECK_FRAGMENT}} do
273
+ captures = params[:captures] || []
274
+ entity_name = captures[0]
275
+ check = captures[1]
276
+
277
+ entities, checks = entities_and_checks(entity_name, check)
278
+
279
+ act_proc = proc {|entity_check|
280
+ summary = params[:summary] ||
281
+ "Testing notifications to all contacts interested in entity #{entity_check.entity.name}"
282
+ Flapjack::Data::Event.test_notifications(
283
+ entity_check.entity_name, entity_check.check,
284
+ :summary => summary,
285
+ :redis => redis)
286
+ }
287
+
288
+ bulk_api_check_action(entities, checks, act_proc)
289
+ status 204
290
+ end
291
+
292
+ app.post '/entities' do
293
+ pass unless Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(request.content_type)
294
+
295
+ errors = []
296
+ ret = nil
297
+
298
+ # FIXME should scan for invalid records before making any changes, fail early
299
+
300
+ entities = params[:entities]
301
+ unless entities
302
+ logger.debug("no entities object found in the following supplied JSON:")
303
+ logger.debug(request.body)
304
+ return err(403, "No entities object received")
305
+ end
306
+ return err(403, "The received entities object is not an Enumerable") unless entities.is_a?(Enumerable)
307
+ return err(403, "Entity with a nil id detected") unless entities.any? {|e| !e['id'].nil?}
308
+
309
+ entities.each do |entity|
310
+ unless entity['id']
311
+ errors << "Entity not imported as it has no id: #{entity.inspect}"
312
+ next
313
+ end
314
+ Flapjack::Data::Entity.add(entity, :redis => redis)
315
+ end
316
+ errors.empty? ? 204 : err(403, *errors)
317
+ end
318
+
319
+ app.post '/entities/:entity/tags' do
320
+ content_type :json
321
+
322
+ tags = find_tags(params[:tag])
323
+ entity = find_entity(params[:entity])
324
+ entity.add_tags(*tags)
325
+ entity.tags.to_json
326
+ end
327
+
328
+ app.delete '/entities/:entity/tags' do
329
+ tags = find_tags(params[:tag])
330
+ entity = find_entity(params[:entity])
331
+ entity.delete_tags(*tags)
332
+ status 204
333
+ end
334
+
335
+ app.get '/entities/:entity/tags' do
336
+ content_type :json
337
+
338
+ entity = find_entity(params[:entity])
339
+ entity.tags.to_json
340
+ end
341
+
342
+ end
343
+
344
+ end
345
+
346
+ end
347
+
348
+ end
349
+
350
+ end