flapjack 0.7.16 → 0.7.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -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