flapjack 0.7.16 → 0.7.17

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.
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/contact'
6
+ require 'flapjack/data/notification_rule'
7
+
8
+ module Flapjack
9
+
10
+ module Gateways
11
+
12
+ class API < Sinatra::Base
13
+
14
+ class ContactNotFound < RuntimeError
15
+ attr_reader :contact_id
16
+ def initialize(contact_id)
17
+ @contact_id = contact_id
18
+ end
19
+ end
20
+
21
+ class NotificationRuleNotFound < RuntimeError
22
+ attr_reader :rule_id
23
+ def initialize(rule_id)
24
+ @rule_id = rule_id
25
+ end
26
+ end
27
+
28
+ module ContactMethods
29
+
30
+ module Helpers
31
+
32
+ def find_contact(contact_id)
33
+ contact = Flapjack::Data::Contact.find_by_id(contact_id, :logger => logger, :redis => redis)
34
+ raise Flapjack::Gateways::API::ContactNotFound.new(contact_id) if contact.nil?
35
+ contact
36
+ end
37
+
38
+ def find_rule(rule_id)
39
+ rule = Flapjack::Data::NotificationRule.find_by_id(rule_id, :logger => logger, :redis => redis)
40
+ raise Flapjack::Gateways::API::NotificationRuleNotFound.new(rule_id) if rule.nil?
41
+ rule
42
+ end
43
+
44
+ def find_tags(tags)
45
+ halt err(403, "no tags") if tags.nil? || tags.empty?
46
+ tags
47
+ end
48
+
49
+ end
50
+
51
+ def self.registered(app)
52
+
53
+ app.helpers Flapjack::Gateways::API::ContactMethods::Helpers
54
+
55
+ app.post '/contacts' do
56
+ pass unless 'application/json'.eql?(request.content_type)
57
+ content_type :json
58
+
59
+ errors = []
60
+
61
+ contacts_data = params[:contacts]
62
+ if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
63
+ errors << "No valid contacts were submitted"
64
+ else
65
+ # stringifying as integer string params are automatically integered,
66
+ # but our redis ids are strings
67
+ contacts_data_ids = contacts_data.reject {|c| c['id'].nil? }.
68
+ map {|co| co['id'].to_s }
69
+
70
+ if contacts_data_ids.empty?
71
+ errors << "No contacts with IDs were submitted"
72
+ else
73
+ contacts = Flapjack::Data::Contact.all(:redis => redis)
74
+ contacts_h = hashify(*contacts) {|c| [c.id, c] }
75
+ contacts_ids = contacts_h.keys
76
+
77
+ # delete contacts not found in the bulk list
78
+ (contacts_ids - contacts_data_ids).each do |contact_to_delete_id|
79
+ contact_to_delete = contacts.detect {|c| c.id == contact_to_delete_id }
80
+ contact_to_delete.delete!
81
+ end
82
+
83
+ # add or update contacts found in the bulk list
84
+ contacts_data.reject {|cd| cd['id'].nil? }.each do |contact_data|
85
+ if contacts_ids.include?(contact_data['id'].to_s)
86
+ contacts_h[contact_data['id'].to_s].update(contact_data)
87
+ else
88
+ Flapjack::Data::Contact.add(contact_data, :redis => redis)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ errors.empty? ? 204 : err(403, *errors)
94
+ end
95
+
96
+ # Returns all the contacts
97
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts
98
+ app.get '/contacts' do
99
+ content_type :json
100
+
101
+ Flapjack::Data::Contact.all(:redis => redis).to_json
102
+ end
103
+
104
+ # Returns the core information about the specified contact
105
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id
106
+ app.get '/contacts/:contact_id' do
107
+ content_type :json
108
+
109
+ contact = find_contact(params[:contact_id])
110
+ contact.to_json
111
+ end
112
+
113
+ # Lists this contact's notification rules
114
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules
115
+ app.get '/contacts/:contact_id/notification_rules' do
116
+ content_type :json
117
+
118
+ contact = find_contact(params[:contact_id])
119
+ contact.notification_rules.to_json
120
+ end
121
+
122
+ # Get the specified notification rule for this user
123
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules_id
124
+ app.get '/notification_rules/:id' do
125
+ content_type :json
126
+
127
+ rule = find_rule(params[:id])
128
+ rule.to_json
129
+ end
130
+
131
+ # Creates a notification rule for a contact
132
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-post_contacts_id_notification_rules
133
+ app.post '/notification_rules' do
134
+ content_type :json
135
+
136
+ if params[:id]
137
+ halt err(403, "post cannot be used for update, do a put instead")
138
+ end
139
+
140
+ logger.debug("post /notification_rules data: ")
141
+ logger.debug(params.inspect)
142
+
143
+ contact = find_contact(params[:contact_id])
144
+
145
+ rule_data = hashify(:entities, :entity_tags,
146
+ :warning_media, :critical_media, :time_restrictions,
147
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
148
+
149
+ rule_or_errors = contact.add_notification_rule(rule_data, :logger => logger)
150
+
151
+ unless rule_or_errors.respond_to?(:critical_media)
152
+ halt err(403, *rule_or_errors)
153
+ end
154
+ rule_or_errors.to_json
155
+ end
156
+
157
+ # Updates a notification rule
158
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
159
+ app.put('/notification_rules/:id') do
160
+ content_type :json
161
+
162
+ logger.debug("put /notification_rules/#{params[:id]} data: ")
163
+ logger.debug(params.inspect)
164
+
165
+ rule = find_rule(params[:id])
166
+ contact = find_contact(rule.contact_id)
167
+
168
+ rule_data = hashify(:entities, :entity_tags,
169
+ :warning_media, :critical_media, :time_restrictions,
170
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
171
+
172
+ errors = rule.update(rule_data, :logger => logger)
173
+
174
+ unless errors.nil? || errors.empty?
175
+ halt err(403, *errors)
176
+ end
177
+ rule.to_json
178
+ end
179
+
180
+ # Deletes a notification rule
181
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
182
+ app.delete('/notification_rules/:id') do
183
+ logger.debug("delete /notification_rules/#{params[:id]}")
184
+ rule = find_rule(params[:id])
185
+ logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
186
+ contact = find_contact(rule.contact_id)
187
+ contact.delete_notification_rule(rule)
188
+ status 204
189
+ end
190
+
191
+ # Returns the media of a contact
192
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media
193
+ app.get '/contacts/:contact_id/media' do
194
+ content_type :json
195
+
196
+ contact = find_contact(params[:contact_id])
197
+
198
+ media = contact.media
199
+ media_intervals = contact.media_intervals
200
+ media_addr_int = hashify(*media.keys) {|k|
201
+ [k, {'address' => media[k],
202
+ 'interval' => media_intervals[k] }]
203
+ }
204
+ media_addr_int.to_json
205
+ end
206
+
207
+ # Returns the specified media of a contact
208
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media_media
209
+ app.get('/contacts/:contact_id/media/:id') do
210
+ content_type :json
211
+
212
+ contact = find_contact(params[:contact_id])
213
+ media = contact.media[params[:id]]
214
+ if media.nil?
215
+ halt err(403, "no #{params[:id]} for contact '#{params[:contact_id]}'")
216
+ end
217
+ interval = contact.media_intervals[params[:id]]
218
+ if interval.nil?
219
+ halt err(403, "no #{params[:id]} interval for contact '#{params[:contact_id]}'")
220
+ end
221
+ {'address' => media,
222
+ 'interval' => interval}.to_json
223
+ end
224
+
225
+ # Creates or updates a media of a contact
226
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_media_media
227
+ app.put('/contacts/:contact_id/media/:id') do
228
+ content_type :json
229
+
230
+ contact = find_contact(params[:contact_id])
231
+ errors = []
232
+ if params[:address].nil?
233
+ errors << "no address for '#{params[:id]}' media"
234
+ end
235
+
236
+ halt err(403, *errors) unless errors.empty?
237
+
238
+ contact.set_address_for_media(params[:id], params[:address])
239
+ contact.set_interval_for_media(params[:id], params[:interval])
240
+
241
+ {'address' => contact.media[params[:id]],
242
+ 'interval' => contact.media_intervals[params[:id]]}.to_json
243
+ end
244
+
245
+ # delete a media of a contact
246
+ app.delete('/contacts/:contact_id/media/:id') do
247
+ contact = find_contact(params[:contact_id])
248
+ contact.remove_media(params[:id])
249
+ status 204
250
+ end
251
+
252
+ # Returns the timezone of a contact
253
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_timezone
254
+ app.get('/contacts/:contact_id/timezone') do
255
+ content_type :json
256
+
257
+ contact = find_contact(params[:contact_id])
258
+ contact.timezone.name.to_json
259
+ end
260
+
261
+ # Sets the timezone of a contact
262
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
263
+ app.put('/contacts/:contact_id/timezone') do
264
+ content_type :json
265
+
266
+ contact = find_contact(params[:contact_id])
267
+ contact.timezone = params[:timezone]
268
+ contact.timezone.name.to_json
269
+ end
270
+
271
+ # Removes the timezone of a contact
272
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
273
+ app.delete('/contacts/:contact_id/timezone') do
274
+ contact = find_contact(params[:contact_id])
275
+ contact.timezone = nil
276
+ status 204
277
+ end
278
+
279
+ app.post '/contacts/:contact_id/tags' do
280
+ content_type :json
281
+
282
+ tags = find_tags(params[:tag])
283
+ contact = find_contact(params[:contact_id])
284
+ contact.add_tags(*tags)
285
+ contact.tags.to_json
286
+ end
287
+
288
+ app.post '/contacts/:contact_id/entity_tags' do
289
+ content_type :json
290
+ contact = find_contact(params[:contact_id])
291
+ contact.entities.map {|e| e[:entity]}.each do |entity|
292
+ next unless tags = params[:entity][entity.name]
293
+ entity.add_tags(*tags)
294
+ end
295
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
296
+ [et[:entity].name, et[:tags]]
297
+ }
298
+ contact_ent_tag.to_json
299
+ end
300
+
301
+ app.delete '/contacts/:contact_id/tags' do
302
+ tags = find_tags(params[:tag])
303
+ contact = find_contact(params[:contact_id])
304
+ contact.delete_tags(*tags)
305
+ status 204
306
+ end
307
+
308
+ app.delete '/contacts/:contact_id/entity_tags' do
309
+ contact = find_contact(params[:contact_id])
310
+ contact.entities.map {|e| e[:entity]}.each do |entity|
311
+ next unless tags = params[:entity][entity.name]
312
+ entity.delete_tags(*tags)
313
+ end
314
+ status 204
315
+ end
316
+
317
+ app.get '/contacts/:contact_id/tags' do
318
+ content_type :json
319
+
320
+ contact = find_contact(params[:contact_id])
321
+ contact.tags.to_json
322
+ end
323
+
324
+ app.get '/contacts/:contact_id/entity_tags' do
325
+ content_type :json
326
+
327
+ contact = find_contact(params[:contact_id])
328
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
329
+ [et[:entity].name, et[:tags]]
330
+ }
331
+ contact_ent_tag.to_json
332
+ end
333
+
334
+ end
335
+
336
+ end
337
+
338
+ end
339
+
340
+ end
341
+
342
+ end
@@ -0,0 +1,364 @@
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/api/entity_presenter'
9
+ require 'flapjack/gateways/api/entity_check_presenter'
10
+
11
+ module Flapjack
12
+
13
+ module Gateways
14
+
15
+ class API < Sinatra::Base
16
+
17
+ class EntityCheckNotFound < RuntimeError
18
+ attr_reader :entity, :check
19
+ def initialize(entity, check)
20
+ @entity = entity
21
+ @check = check
22
+ end
23
+ end
24
+
25
+ class EntityNotFound < RuntimeError
26
+ attr_reader :entity
27
+ def initialize(entity)
28
+ @entity = entity
29
+ end
30
+ end
31
+
32
+ module EntityMethods
33
+
34
+ module Helpers
35
+
36
+ def find_entity(entity_name)
37
+ entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis)
38
+ raise Flapjack::Gateways::API::EntityNotFound.new(entity_name) if entity.nil?
39
+ entity
40
+ end
41
+
42
+ def find_entity_check(entity, check)
43
+ entity_check = Flapjack::Data::EntityCheck.for_entity(entity,
44
+ check, :redis => redis)
45
+ raise Flapjack::Gateways::API::EntityCheckNotFound.new(entity, check) if entity_check.nil?
46
+ entity_check
47
+ end
48
+
49
+ def find_tags(tags)
50
+ halt err(403, "no tags") if tags.nil? || tags.empty?
51
+ tags
52
+ end
53
+
54
+ def entities_and_checks(entity_name, check)
55
+ if entity_name
56
+ # backwards-compatible, single entity or entity&check from route
57
+ entities = check ? nil : [entity_name]
58
+ checks = check ? {entity_name => check} : nil
59
+ else
60
+ # new and improved bulk API queries
61
+ entities = params[:entity]
62
+ checks = params[:check]
63
+ entities = [entities] unless entities.nil? || entities.is_a?(Array)
64
+ # TODO err if checks isn't a Hash (similar rules as in flapjack-diner)
65
+ end
66
+ [entities, checks]
67
+ end
68
+
69
+ def bulk_api_check_action(entities, entity_checks, action, params = {})
70
+ unless entities.nil? || entities.empty?
71
+ entities.each do |entity_name|
72
+ entity = find_entity(entity_name)
73
+ checks = entity.check_list.sort
74
+ checks.each do |check|
75
+ action.call( find_entity_check(entity, check) )
76
+ end
77
+ end
78
+ end
79
+
80
+ unless entity_checks.nil? || entity_checks.empty?
81
+ entity_checks.each_pair do |entity_name, checks|
82
+ entity = find_entity(entity_name)
83
+ checks = [checks] unless checks.is_a?(Array)
84
+ checks.each do |check|
85
+ action.call( find_entity_check(entity, check) )
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def present_api_results(entities, entity_checks, result_type, &block)
92
+ result = []
93
+
94
+ unless entities.nil? || entities.empty?
95
+ result += entities.collect {|entity_name|
96
+ entity = find_entity(entity_name)
97
+ yield(Flapjack::Gateways::API::EntityPresenter.new(entity, :redis => redis))
98
+ }.flatten(1)
99
+ end
100
+
101
+ unless entity_checks.nil? || entity_checks.empty?
102
+ result += entity_checks.inject([]) {|memo, (entity_name, checks)|
103
+ checks = [checks] unless checks.is_a?(Array)
104
+ entity = find_entity(entity_name)
105
+ memo += checks.collect {|check|
106
+ entity_check = find_entity_check(entity, check)
107
+ {:entity => entity_name,
108
+ :check => check,
109
+ result_type.to_sym => yield(Flapjack::Gateways::API::EntityCheckPresenter.new(entity_check))
110
+ }
111
+ }
112
+ }.flatten(1)
113
+ end
114
+
115
+ result
116
+ end
117
+
118
+ # NB: casts to UTC before converting to a timestamp
119
+ def validate_and_parsetime(value)
120
+ return unless value
121
+ Time.iso8601(value).getutc.to_i
122
+ rescue ArgumentError => e
123
+ logger.error "Couldn't parse time from '#{value}'"
124
+ nil
125
+ end
126
+
127
+ end
128
+
129
+ # used for backwards-compatible route matching below
130
+ ENTITY_CHECK_FRAGMENT = '(?:/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(.+))?)?'
131
+
132
+ def self.registered(app)
133
+
134
+ app.helpers Flapjack::Gateways::API::EntityMethods::Helpers
135
+
136
+ app.get '/entities' do
137
+ content_type :json
138
+ ret = Flapjack::Data::Entity.all(:redis => redis).sort_by(&:name).collect {|e|
139
+ presenter = Flapjack::Gateways::API::EntityPresenter.new(e, :redis => redis)
140
+ {'id' => e.id, 'name' => e.name, 'checks' => presenter.status }
141
+ }
142
+ ret.to_json
143
+ end
144
+
145
+ app.get '/checks/:entity' do
146
+ content_type :json
147
+ entity = find_entity(params[:entity])
148
+ entity.check_list.to_json
149
+ end
150
+
151
+ app.get %r{/status#{ENTITY_CHECK_FRAGMENT}} do
152
+ content_type :json
153
+
154
+ captures = params[:captures] || []
155
+ entity_name = captures[0]
156
+ check = captures[1]
157
+
158
+ entities, checks = entities_and_checks(entity_name, check)
159
+
160
+ results = present_api_results(entities, checks, 'status') {|presenter|
161
+ presenter.status
162
+ }
163
+
164
+ if entity_name
165
+ # compatible with previous data format
166
+ results = results.collect {|status_h| status_h[:status]}
167
+ (check ? results.first : results).to_json
168
+ else
169
+ # new and improved data format which reflects the request param structure
170
+ results.to_json
171
+ end
172
+ end
173
+
174
+ app.get %r{/((?:outages|(?:un)?scheduled_maintenances|downtime))#{ENTITY_CHECK_FRAGMENT}} do
175
+ action = params[:captures][0].to_sym
176
+ entity_name = params[:captures][1]
177
+ check = params[:captures][2]
178
+
179
+ entities, checks = entities_and_checks(entity_name, check)
180
+
181
+ start_time = validate_and_parsetime(params[:start_time])
182
+ end_time = validate_and_parsetime(params[:end_time])
183
+
184
+ results = present_api_results(entities, checks, action) {|presenter|
185
+ presenter.send(action, start_time, end_time)
186
+ }
187
+
188
+ if check
189
+ # compatible with previous data format
190
+ results.first[action].to_json
191
+ elsif entity_name
192
+ # compatible with previous data format
193
+ rename = {:unscheduled_maintenances => :unscheduled_maintenance,
194
+ :scheduled_maintenances => :scheduled_maintenance}
195
+ drop = [:entity]
196
+ results.collect{|r|
197
+ r.inject({}) {|memo, (k, v)|
198
+ if new_k = rename[k]
199
+ memo[new_k] = v
200
+ elsif !drop.include?(k)
201
+ memo[k] = v
202
+ end
203
+ memo
204
+ }
205
+ }.to_json
206
+ else
207
+ # new and improved data format which reflects the request param structure
208
+ results.to_json
209
+ end
210
+ end
211
+
212
+ # create a scheduled maintenance period for a check on an entity
213
+ app.post %r{/scheduled_maintenances#{ENTITY_CHECK_FRAGMENT}} do
214
+
215
+ captures = params[:captures] || []
216
+ entity_name = captures[0]
217
+ check = captures[1]
218
+
219
+ entities, checks = entities_and_checks(entity_name, check)
220
+
221
+ start_time = validate_and_parsetime(params[:start_time])
222
+ halt( err(403, "start time must be provided") ) unless start_time
223
+
224
+ act_proc = proc {|entity_check|
225
+ entity_check.create_scheduled_maintenance(:start_time => start_time,
226
+ :duration => params[:duration].to_i, :summary => params[:summary])
227
+ }
228
+
229
+ bulk_api_check_action(entities, checks, act_proc)
230
+ status 204
231
+ end
232
+
233
+ # create an acknowledgement for a service on an entity
234
+ # NB currently, this does not acknowledge a specific failure event, just
235
+ # the entity-check as a whole
236
+ app.post %r{/acknowledgements#{ENTITY_CHECK_FRAGMENT}} do
237
+ captures = params[:captures] || []
238
+ entity_name = captures[0]
239
+ check = captures[1]
240
+
241
+ entities, checks = entities_and_checks(entity_name, check)
242
+
243
+ dur = params[:duration] ? params[:duration].to_i : nil
244
+ duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur
245
+ summary = params[:summary]
246
+
247
+ opts = {'duration' => duration}
248
+ opts['summary'] = summary if summary
249
+
250
+ act_proc = proc {|entity_check|
251
+ Flapjack::Data::Event.create_acknowledgement(
252
+ entity_check.entity_name, entity_check.check,
253
+ :summary => params[:summary],
254
+ :duration => duration,
255
+ :redis => redis)
256
+ }
257
+
258
+ bulk_api_check_action(entities, checks, act_proc)
259
+ status 204
260
+ end
261
+
262
+ app.delete %r{/((?:un)?scheduled_maintenances)} do
263
+ action = params[:captures][0]
264
+
265
+ # no backwards-compatible mode here, it's a new method
266
+ entities, checks = entities_and_checks(nil, nil)
267
+
268
+ act_proc = case action
269
+ when 'scheduled_maintenances'
270
+ start_time = validate_and_parsetime(params[:start_time])
271
+ halt( err(403, "start time must be provided") ) unless start_time
272
+ opts = {}
273
+ opts[:start_time] = start_time.to_i
274
+ proc {|entity_check| entity_check.delete_scheduled_maintenance(opts) }
275
+ when 'unscheduled_maintenances'
276
+ end_time = validate_and_parsetime(params[:end_time])
277
+ opts = {}
278
+ opts[:end_time] = end_time.to_i if end_time
279
+ proc {|entity_check| entity_check.end_unscheduled_maintenance(opts) }
280
+ end
281
+
282
+ bulk_api_check_action(entities, checks, act_proc)
283
+ status 204
284
+ end
285
+
286
+ app.post %r{/test_notifications#{ENTITY_CHECK_FRAGMENT}} do
287
+ captures = params[:captures] || []
288
+ entity_name = captures[0]
289
+ check = captures[1]
290
+
291
+ entities, checks = entities_and_checks(entity_name, check)
292
+
293
+ act_proc = proc {|entity_check|
294
+ summary = params[:summary] ||
295
+ "Testing notifications to all contacts interested in entity #{entity_check.entity.name}"
296
+ Flapjack::Data::Event.test_notifications(
297
+ entity_check.entity_name, entity_check.check,
298
+ :summary => summary,
299
+ :redis => redis)
300
+ }
301
+
302
+ bulk_api_check_action(entities, checks, act_proc)
303
+ status 204
304
+ end
305
+
306
+ app.post '/entities' do
307
+ pass unless 'application/json'.eql?(request.content_type)
308
+
309
+ errors = []
310
+ ret = nil
311
+
312
+ # FIXME should scan for invalid records before making any changes, fail early
313
+
314
+ entities = params[:entities]
315
+ unless entities
316
+ logger.debug("no entities object found in the following supplied JSON:")
317
+ logger.debug(request.body)
318
+ return err(403, "No entities object received")
319
+ end
320
+ return err(403, "The received entities object is not an Enumerable") unless entities.is_a?(Enumerable)
321
+ return err(403, "Entity with a nil id detected") unless entities.any? {|e| !e['id'].nil?}
322
+
323
+ entities.each do |entity|
324
+ unless entity['id']
325
+ errors << "Entity not imported as it has no id: #{entity.inspect}"
326
+ next
327
+ end
328
+ Flapjack::Data::Entity.add(entity, :redis => redis)
329
+ end
330
+ errors.empty? ? 204 : err(403, *errors)
331
+ end
332
+
333
+ app.post '/entities/:entity/tags' do
334
+ content_type :json
335
+
336
+ tags = find_tags(params[:tag])
337
+ entity = find_entity(params[:entity])
338
+ entity.add_tags(*tags)
339
+ entity.tags.to_json
340
+ end
341
+
342
+ app.delete '/entities/:entity/tags' do
343
+ tags = find_tags(params[:tag])
344
+ entity = find_entity(params[:entity])
345
+ entity.delete_tags(*tags)
346
+ status 204
347
+ end
348
+
349
+ app.get '/entities/:entity/tags' do
350
+ content_type :json
351
+
352
+ entity = find_entity(params[:entity])
353
+ entity.tags.to_json
354
+ end
355
+
356
+ end
357
+
358
+ end
359
+
360
+ end
361
+
362
+ end
363
+
364
+ end