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
@@ -118,8 +118,7 @@ module Flapjack
118
118
  app.get '/contacts/:contact_id/notification_rules' do
119
119
  content_type :json
120
120
 
121
- contact = find_contact(params[:contact_id])
122
- contact.notification_rules.to_json
121
+ "[" + find_contact(params[:contact_id]).notification_rules.map {|r| r.to_json }.join(',') + "]"
123
122
  end
124
123
 
125
124
  # Get the specified notification rule for this user
@@ -135,7 +135,7 @@ module Flapjack
135
135
 
136
136
  app.get '/entities' do
137
137
  content_type :json
138
- ret = Flapjack::Data::Entity.all(:redis => redis).sort_by(&:name).collect {|e|
138
+ ret = Flapjack::Data::Entity.all(:redis => redis).collect {|e|
139
139
  presenter = Flapjack::Gateways::API::EntityPresenter.new(e, :redis => redis)
140
140
  {'id' => e.id, 'name' => e.name, 'checks' => presenter.status }
141
141
  }
@@ -164,10 +164,10 @@ module Flapjack
164
164
  if entity_name
165
165
  # compatible with previous data format
166
166
  results = results.collect {|status_h| status_h[:status]}
167
- (check ? results.first : results).to_json
167
+ check ? results.first.to_json : "[" + results.map {|r| r.to_json }.join(',') + "]"
168
168
  else
169
169
  # new and improved data format which reflects the request param structure
170
- results.to_json
170
+ "[" + results.map {|r| r.to_json }.join(',') + "]"
171
171
  end
172
172
  end
173
173
 
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A HTTP-based API server, which provides queries to determine the status of
4
+ # entities and the checks that are reported against them.
5
+ #
6
+ # There's a matching flapjack-diner gem at https://github.com/flpjck/flapjack-diner
7
+ # which consumes data from this API.
8
+
9
+ require 'time'
10
+
11
+ require 'rack/fiber_pool'
12
+ require 'sinatra/base'
13
+
14
+ require 'flapjack/rack_logger'
15
+ require 'flapjack/redis_pool'
16
+
17
+ require 'flapjack/gateways/jsonapi/rack/json_params_parser'
18
+
19
+ require 'flapjack/gateways/jsonapi/contact_methods'
20
+ require 'flapjack/gateways/jsonapi/entity_methods'
21
+
22
+ module Flapjack
23
+
24
+ module Gateways
25
+
26
+ class JSONAPI < Sinatra::Base
27
+
28
+ include Flapjack::Utility
29
+
30
+ JSON_REQUEST_MIME_TYPES = ['application/vnd.api+json', 'application/json']
31
+
32
+ class ContactNotFound < RuntimeError
33
+ attr_reader :contact_id
34
+ def initialize(contact_id)
35
+ @contact_id = contact_id
36
+ end
37
+ end
38
+
39
+ class NotificationRuleNotFound < RuntimeError
40
+ attr_reader :rule_id
41
+ def initialize(rule_id)
42
+ @rule_id = rule_id
43
+ end
44
+ end
45
+
46
+ class EntityNotFound < RuntimeError
47
+ attr_reader :entity
48
+ def initialize(entity)
49
+ @entity = entity
50
+ end
51
+ end
52
+
53
+ class EntityCheckNotFound < RuntimeError
54
+ attr_reader :entity, :check
55
+ def initialize(entity, check)
56
+ @entity = entity
57
+ @check = check
58
+ end
59
+ end
60
+
61
+ class ResourceLocked < RuntimeError
62
+ attr_reader :resource
63
+ def initialize(resource)
64
+ @resource = resource
65
+ end
66
+ end
67
+
68
+ set :dump_errors, false
69
+
70
+ rescue_error = Proc.new {|status, exception, request_info, *msg|
71
+ if !msg || msg.empty?
72
+ trace = exception.backtrace.join("\n")
73
+ msg = "#{exception.class} - #{exception.message}"
74
+ msg_str = "#{msg}\n#{trace}"
75
+ else
76
+ msg_str = msg.join(", ")
77
+ end
78
+ case
79
+ when status < 500
80
+ @logger.warn "Error: #{msg_str}"
81
+ else
82
+ @logger.error "Error: #{msg_str}"
83
+ end
84
+
85
+ response_body = {:errors => msg}.to_json
86
+
87
+ query_string = (request_info[:query_string].respond_to?(:length) &&
88
+ request_info[:query_string].length > 0) ? "?#{request_info[:query_string]}" : ""
89
+ if @logger.debug?
90
+ @logger.debug("Returning #{status} for #{request_info[:request_method]} " +
91
+ "#{request_info[:path_info]}#{query_string}, body: #{response_body}")
92
+ elsif @logger.info?
93
+ @logger.info("Returning #{status} for #{request_info[:request_method]} " +
94
+ "#{request_info[:path_info]}#{query_string}")
95
+ end
96
+
97
+ [status, {}, response_body]
98
+ }
99
+
100
+ rescue_exception = Proc.new {|env, e|
101
+ request_info = {
102
+ :path_info => env['REQUEST_PATH'],
103
+ :request_method => env['REQUEST_METHOD'],
104
+ :query_string => env['QUERY_STRING']
105
+ }
106
+ case e
107
+ when Flapjack::Gateways::JSONAPI::ContactNotFound
108
+ rescue_error.call(404, e, request_info, "could not find contact '#{e.contact_id}'")
109
+ when Flapjack::Gateways::JSONAPI::NotificationRuleNotFound
110
+ rescue_error.call(404, e, request_info,"could not find notification rule '#{e.rule_id}'")
111
+ when Flapjack::Gateways::JSONAPI::EntityNotFound
112
+ rescue_error.call(404, e, request_info, "could not find entity '#{e.entity}'")
113
+ when Flapjack::Gateways::JSONAPI::EntityCheckNotFound
114
+ rescue_error.call(404, e, request_info, "could not find entity check '#{e.check}'")
115
+ when Flapjack::Gateways::JSONAPI::ResourceLocked
116
+ rescue_error.call(423, e, request_info, "unable to obtain lock for resource '#{e.resource}'")
117
+ else
118
+ rescue_error.call(500, e, request_info)
119
+ end
120
+ }
121
+ use ::Rack::FiberPool, :size => 25, :rescue_exception => rescue_exception
122
+
123
+ use ::Rack::MethodOverride
124
+ use Flapjack::Gateways::JSONAPI::Rack::JsonParamsParser
125
+
126
+ class << self
127
+ def start
128
+ @redis = Flapjack::RedisPool.new(:config => @redis_config, :size => 2)
129
+
130
+ @logger.info "starting jsonapi - class"
131
+
132
+ if @config && @config['access_log']
133
+ access_logger = Flapjack::AsyncLogger.new(@config['access_log'])
134
+ use Flapjack::CommonLogger, access_logger
135
+ end
136
+
137
+ @base_url = @config['base_url']
138
+ dummy_url = "http://api.example.com"
139
+ if @base_url
140
+ @base_url = $1 if @base_url.match(/^(.+)\/$/)
141
+ else
142
+ @logger.error "base_url must be a valid http or https URI (not configured), setting to dummy value (#{dummy_url})"
143
+ # FIXME: at this point I'd like to stop this pikelet without bringing down the whole
144
+ @base_url = dummy_url
145
+ end
146
+ if (@base_url =~ /^#{URI::regexp(%w(http https))}$/).nil?
147
+ @logger.error "base_url must be a valid http or https URI (#{@base_url}), setting to dummy value (#{dummy_url})"
148
+ # FIXME: at this point I'd like to stop this pikelet without bringing down the whole
149
+ # flapjack process
150
+ # For now, set a dummy value
151
+ @base_url = dummy_url
152
+ end
153
+ end
154
+ end
155
+
156
+ def redis
157
+ self.class.instance_variable_get('@redis')
158
+ end
159
+
160
+ def logger
161
+ self.class.instance_variable_get('@logger')
162
+ end
163
+
164
+ def base_url
165
+ self.class.instance_variable_get('@base_url')
166
+ end
167
+
168
+ before do
169
+ input = nil
170
+ query_string = (request.query_string.respond_to?(:length) &&
171
+ request.query_string.length > 0) ? "?#{request.query_string}" : ""
172
+ if logger.debug?
173
+ input = env['rack.input'].read
174
+ logger.debug("#{request.request_method} #{request.path_info}#{query_string} #{input}")
175
+ elsif logger.info?
176
+ input = env['rack.input'].read
177
+ input_short = input.gsub(/\n/, '').gsub(/\s+/, ' ')
178
+ logger.info("#{request.request_method} #{request.path_info}#{query_string} #{input_short[0..80]}")
179
+ end
180
+ env['rack.input'].rewind unless input.nil?
181
+ end
182
+
183
+ after do
184
+ return if response.status == 500
185
+
186
+ query_string = (request.query_string.respond_to?(:length) &&
187
+ request.query_string.length > 0) ? "?#{request.query_string}" : ""
188
+ if logger.debug?
189
+ logger.debug("Returning #{response.status} for #{request.request_method} " +
190
+ "#{request.path_info}#{query_string}, body: #{response.body.join(', ')}")
191
+ elsif logger.info?
192
+ logger.info("Returning #{response.status} for #{request.request_method} " +
193
+ "#{request.path_info}#{query_string}")
194
+ end
195
+ end
196
+
197
+ register Flapjack::Gateways::JSONAPI::EntityMethods
198
+
199
+ register Flapjack::Gateways::JSONAPI::ContactMethods
200
+
201
+ # the following should add the cors headers to every request, but is no work
202
+ #register Sinatra::CrossOrigin
203
+ #
204
+ #configure do
205
+ # enable :cross_origin
206
+ #end
207
+ #set :allow_origin, :any
208
+ #set :allow_methods, [:get, :post, :put, :patch, :delete, :options]
209
+
210
+ options '*' do
211
+ cors_headers
212
+ 204
213
+ end
214
+
215
+ not_found do
216
+ err(404, "not routable")
217
+ end
218
+
219
+ def cors_headers
220
+ allow_headers = %w(* Content-Type Accept AUTHORIZATION Cache-Control)
221
+ allow_methods = %w(GET POST PUT PATCH DELETE OPTIONS)
222
+ expose_headers = %w(Cache-Control Content-Language Content-Type Expires Last-Modified Pragma)
223
+ cors_headers = {
224
+ 'Access-Control-Allow-Origin' => '*',
225
+ 'Access-Control-Allow-Methods' => allow_methods.join(', '),
226
+ 'Access-Control-Allow-Headers' => allow_headers.join(', '),
227
+ 'Access-Control-Expose-Headers' => expose_headers.join(', '),
228
+ 'Access-Control-Max-Age' => '1728000'
229
+ }
230
+ headers(cors_headers)
231
+ end
232
+
233
+ def location(ids)
234
+ location = "#{base_url}#{request.path_info}#{ids.length == 1 ? '/' + ids.first : '?ids=' + ids.join(',')}"
235
+ headers({'Location' => location})
236
+ end
237
+
238
+ private
239
+
240
+ def err(status, *msg)
241
+ msg_str = msg.join(", ")
242
+ logger.info "Error: #{msg_str}"
243
+ [status, {}, {:errors => msg}.to_json]
244
+ end
245
+ end
246
+
247
+ end
248
+
249
+ end
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/contact'
6
+ require 'flapjack/data/notification_rule'
7
+ require 'flapjack/data/semaphore'
8
+
9
+ module Flapjack
10
+
11
+ module Gateways
12
+
13
+ class JSONAPI < Sinatra::Base
14
+
15
+ module ContactMethods
16
+
17
+ SEMAPHORE_CONTACT_MASS_UPDATE = 'contact_mass_update'
18
+
19
+ module Helpers
20
+
21
+ def find_contact(contact_id)
22
+ contact = Flapjack::Data::Contact.find_by_id(contact_id, :logger => logger, :redis => redis)
23
+ raise Flapjack::Gateways::JSONAPI::ContactNotFound.new(contact_id) if contact.nil?
24
+ contact
25
+ end
26
+
27
+ def find_rule(rule_id)
28
+ rule = Flapjack::Data::NotificationRule.find_by_id(rule_id, :logger => logger, :redis => redis)
29
+ raise Flapjack::Gateways::JSONAPI::NotificationRuleNotFound.new(rule_id) if rule.nil?
30
+ rule
31
+ end
32
+
33
+ def find_tags(tags)
34
+ halt err(400, "no tags given") if tags.nil? || tags.empty?
35
+ tags
36
+ end
37
+
38
+ def obtain_semaphore(resource)
39
+ semaphore = nil
40
+ strikes = 0
41
+ begin
42
+ semaphore = Flapjack::Data::Semaphore.new(resource, {:redis => redis, :expiry => 30})
43
+ rescue Flapjack::Data::Semaphore::ResourceLocked
44
+ strikes += 1
45
+ raise Flapjack::Gateways::JSONAPI::ResourceLocked.new(resource) unless strikes < 3
46
+ sleep 1
47
+ retry
48
+ end
49
+ raise Flapjack::Gateways::JSONAPI::ResourceLocked.new(resource) unless semaphore
50
+ semaphore
51
+ end
52
+ end
53
+
54
+ def self.registered(app)
55
+
56
+ app.helpers Flapjack::Gateways::JSONAPI::ContactMethods::Helpers
57
+
58
+ app.post '/contacts' do
59
+ pass unless Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(request.content_type)
60
+ content_type :json
61
+ cors_headers
62
+
63
+ contacts_data = params[:contacts]
64
+
65
+ if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
66
+ halt err(422, "No valid contacts were submitted")
67
+ end
68
+
69
+ contacts_ids = contacts_data.reject {|c| c['id'].nil? }.
70
+ map {|co| co['id'].to_s }
71
+
72
+ semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
73
+
74
+ conflicted_ids = contacts_ids.find_all {|id|
75
+ Flapjack::Data::Contact.exists_with_id?(id, :redis => redis)
76
+ }
77
+
78
+ unless conflicted_ids.empty?
79
+ semaphore.release
80
+ halt err(409, "Contacts already exist with the following IDs: " +
81
+ conflicted_ids.join(', '))
82
+ end
83
+
84
+ contacts_data.each do |contact_data|
85
+ unless contact_data['id']
86
+ contact_data['id'] = SecureRandom.uuid
87
+ end
88
+ Flapjack::Data::Contact.add(contact_data, :redis => redis)
89
+ end
90
+
91
+ semaphore.release
92
+
93
+ ids = contacts_data.map {|c| c['id']}
94
+ location(ids)
95
+
96
+ contacts_data.map {|cd| cd['id']}.to_json
97
+ end
98
+
99
+ app.post '/contacts_atomic' do
100
+ pass unless Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(request.content_type)
101
+ content_type :json
102
+
103
+ contacts_data = params[:contacts]
104
+ if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
105
+ halt err(422, "No valid contacts were submitted")
106
+ end
107
+
108
+ # stringifying as integer string params are automatically integered,
109
+ # but our redis ids are strings
110
+ contacts_data_ids = contacts_data.reject {|c| c['id'].nil? }.
111
+ map {|co| co['id'].to_s }
112
+
113
+ if contacts_data_ids.empty?
114
+ halt err(422, "No contacts with IDs were submitted")
115
+ end
116
+
117
+ semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
118
+
119
+ contacts = Flapjack::Data::Contact.all(:redis => redis)
120
+ contacts_h = hashify(*contacts) {|c| [c.id, c] }
121
+ contacts_ids = contacts_h.keys
122
+
123
+ # delete contacts not found in the bulk list
124
+ (contacts_ids - contacts_data_ids).each do |contact_to_delete_id|
125
+ contact_to_delete = contacts.detect {|c| c.id == contact_to_delete_id }
126
+ contact_to_delete.delete!
127
+ end
128
+
129
+ # add or update contacts found in the bulk list
130
+ contacts_data.reject {|cd| cd['id'].nil? }.each do |contact_data|
131
+ if contacts_ids.include?(contact_data['id'].to_s)
132
+ contacts_h[contact_data['id'].to_s].update(contact_data)
133
+ else
134
+ Flapjack::Data::Contact.add(contact_data, :redis => redis)
135
+ end
136
+ end
137
+
138
+ semaphore.release
139
+ 204
140
+ end
141
+
142
+ # Returns all the contacts
143
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts
144
+ app.get '/contacts' do
145
+ content_type :json
146
+ cors_headers
147
+
148
+ contacts = if params[:ids]
149
+ Flapjack::Data::Contact.find_by_ids(params[:ids].split(',').uniq, :redis => redis)
150
+ else
151
+ Flapjack::Data::Contact.all(:redis => redis)
152
+ end
153
+ contacts.compact!
154
+
155
+ linked_entity_data, linked_entity_ids = if contacts.empty?
156
+ [[], []]
157
+ else
158
+ Flapjack::Data::Contact.entities_jsonapi(contacts.map(&:id), :redis => redis)
159
+ end
160
+
161
+ contacts_json = contacts.collect {|contact|
162
+ contact.linked_entity_ids = linked_entity_ids[contact.id]
163
+ contact.to_json
164
+ }.join(", ")
165
+
166
+ '{"contacts":[' + contacts_json + ']' +
167
+ ( linked_entity_data.empty? ? '}' :
168
+ ', "linked": {"entities":' + linked_entity_data.to_json + '}}')
169
+ end
170
+
171
+ # Returns the core information about the specified contact
172
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id
173
+ app.get '/contacts/:contact_id' do
174
+ content_type :json
175
+ cors_headers
176
+ contact = find_contact(params[:contact_id])
177
+
178
+ entities = contact.entities.map {|e| e[:entity] }
179
+
180
+ '{"contacts":[' + contact.to_json + ']' +
181
+ ( entities.empty? ? '}' :
182
+ ', "linked": {"entities":' + entities.values.to_json + '}}')
183
+ end
184
+
185
+ # Updates a contact
186
+ app.put '/contacts/:contact_id' do
187
+ content_type :json
188
+ cors_headers
189
+
190
+ contacts_data = params[:contacts]
191
+
192
+ if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
193
+ halt err(422, "No valid contacts were submitted")
194
+ end
195
+
196
+ unless contacts_data.length == 1
197
+ halt err(422, "Exactly one contact hash must be supplied.")
198
+ end
199
+
200
+ contact_data = contacts_data.first
201
+
202
+ if contact_data['id'] && contact_data['id'].to_s != params[:contact_id]
203
+ halt err(422, "ID, if supplied, must match URL")
204
+ end
205
+
206
+ contact = find_contact(params[:contact_id])
207
+ #contact_data = hashify('first_name', 'last_name', 'email', 'media', 'tags') {|k| [k, params[k]]}
208
+ logger.debug("contact_data: #{contact_data}")
209
+ contact.update(contact_data)
210
+
211
+ contact.to_json
212
+ end
213
+
214
+ # Deletes a contact
215
+ app.delete '/contacts/:contact_id' do
216
+ cors_headers
217
+ semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
218
+ contact = find_contact(params[:contact_id])
219
+ contact.delete!
220
+ semaphore.release
221
+ status 204
222
+ end
223
+
224
+ # Lists this contact's notification rules
225
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules
226
+ app.get '/contacts/:contact_id/notification_rules' do
227
+ content_type :json
228
+ cors_headers
229
+
230
+ "[" + find_contact(params[:contact_id]).notification_rules.map {|r| r.to_json }.join(',') + "]"
231
+ end
232
+
233
+ # Get the specified notification rule for this user
234
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules_id
235
+ app.get '/notification_rules/:id' do
236
+ content_type :json
237
+ cors_headers
238
+
239
+ '{"notification_rules":[' +
240
+ find_rule(params[:id]).to_json +
241
+ ']}'
242
+ end
243
+
244
+ # Creates a notification rule or rules for a contact
245
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-post_contacts_id_notification_rules
246
+ app.post '/notification_rules' do
247
+ content_type :json
248
+ cors_headers
249
+
250
+ rules_data = params[:notification_rules]
251
+
252
+ if rules_data.nil? || !rules_data.is_a?(Enumerable)
253
+ halt err(422, "No valid notification rules were submitted")
254
+ end
255
+
256
+ if rules_data.any? {|rule| rule['id']}
257
+ halt err(422, "ID fields may not be generated by you. Remove IDs and POST again")
258
+ end
259
+
260
+ errors = []
261
+ rules_data.each do |rule_data|
262
+ errors << Flapjack::Data::NotificationRule.prevalidate_data(symbolize(rule_data), {:logger => logger})
263
+ end
264
+ errors.compact!
265
+
266
+ unless errors.nil? || errors.empty?
267
+ halt err(422, *errors)
268
+ end
269
+
270
+ rules = []
271
+ errors = []
272
+ rules_data.each do |rule_data|
273
+ rule_data = symbolize(rule_data)
274
+ contact = find_contact(rule_data.delete(:contact_id))
275
+ rule_or_errors = contact.add_notification_rule(rule_data, :logger => logger)
276
+ if rule_or_errors.respond_to?(:critical_media)
277
+ rules << rule_or_errors
278
+ else
279
+ errors << rule_or_errors
280
+ end
281
+ end
282
+
283
+ if rules.empty?
284
+ halt err(422, *errors)
285
+ else
286
+ if errors.empty?
287
+ status 201
288
+ else
289
+ logger.warn("Errors during bulk notification rules creation: " + errors.join(', '))
290
+ status 200
291
+ end
292
+ end
293
+ ids = rules.map {|r| r.id}
294
+ location(ids)
295
+ '{"notification_rules":[' +
296
+ rules.map {|r| r.to_json}.join(',') +
297
+ ']}'
298
+ end
299
+
300
+ # Updates a notification rule
301
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
302
+ app.put('/notification_rules/:id') do
303
+ content_type :json
304
+ cors_headers
305
+
306
+ rules_data = params[:notification_rules]
307
+
308
+ if rules_data.nil? || !rules_data.is_a?(Enumerable)
309
+ halt err(422, "No valid notification rules were submitted")
310
+ end
311
+
312
+ unless rules_data.length == 1
313
+ halt err(422, "Exactly one notification rules hash must be supplied.")
314
+ end
315
+
316
+ rule_data = rules_data.first
317
+
318
+ if rule_data['id'] && rule_data['id'].to_s != params[:id]
319
+ halt err(422, "ID, if supplied, must match URL")
320
+ end
321
+
322
+ rule = find_rule(params[:id])
323
+ contact = find_contact(rule.contact_id)
324
+
325
+ supplied_contact = rule_data.delete('contact_id')
326
+ if supplied_contact && supplied_contact != contact.id
327
+ halt err(422, "contact_id cannot be modified")
328
+ end
329
+
330
+ errors = rule.update(symbolize(rule_data), :logger => logger)
331
+
332
+ unless errors.nil? || errors.empty?
333
+ halt err(422, *errors)
334
+ end
335
+ '{"notification_rules":[' +
336
+ rule.to_json +
337
+ ']}'
338
+ end
339
+
340
+ # Deletes a notification rule
341
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
342
+ app.delete('/notification_rules/:id') do
343
+ cors_headers
344
+ rule = find_rule(params[:id])
345
+ logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
346
+ contact = find_contact(rule.contact_id)
347
+ contact.delete_notification_rule(rule)
348
+ status 204
349
+ end
350
+
351
+ # Returns the media of a contact
352
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media
353
+ app.get '/contacts/:contact_id/media' do
354
+ content_type :json
355
+ cors_headers
356
+
357
+ contact = find_contact(params[:contact_id])
358
+
359
+ media = contact.media
360
+ media_intervals = contact.media_intervals
361
+ media_rollup_thresholds = contact.media_rollup_thresholds
362
+ media_addr_int = hashify(*media.keys) {|k|
363
+ [k, {'address' => media[k],
364
+ 'interval' => media_intervals[k],
365
+ 'rollup_threshold' => media_rollup_thresholds[k] }]
366
+ }
367
+ media_addr_int.to_json
368
+ end
369
+
370
+ # Returns the specified media of a contact
371
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media_media
372
+ app.get('/contacts/:contact_id/media/:id') do
373
+ content_type :json
374
+ cors_headers
375
+
376
+ contact = find_contact(params[:contact_id])
377
+ media = contact.media[params[:id]]
378
+ if media.nil?
379
+ halt err(404, "no #{params[:id]} for contact '#{params[:contact_id]}'")
380
+ end
381
+ interval = contact.media_intervals[params[:id]]
382
+ # FIXME: does erroring when no interval found make sense?
383
+ if interval.nil?
384
+ halt err(403, "no #{params[:id]} interval for contact '#{params[:contact_id]}'")
385
+ end
386
+ rollup_threshold = contact.media_rollup_thresholds[params[:id]]
387
+ {'address' => media,
388
+ 'interval' => interval,
389
+ 'rollup_threshold' => rollup_threshold }.to_json
390
+ end
391
+
392
+ # Creates or updates a media of a contact
393
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_media_media
394
+ app.put('/contacts/:contact_id/media/:id') do
395
+ content_type :json
396
+ cors_headers
397
+
398
+ contact = find_contact(params[:contact_id])
399
+ errors = []
400
+
401
+ if 'pagerduty'.eql?(params[:id])
402
+ errors = [:service_key, :subdomain, :username, :password].inject([]) do |memo, pdp|
403
+ memo << "no #{pdp.to_s} for 'pagerduty' media" if params[pdp].nil?
404
+ memo
405
+ end
406
+
407
+ halt err(422, *errors) unless errors.empty?
408
+
409
+ contact.set_pagerduty_credentials('service_key' => params[:service_key],
410
+ 'subdomain' => params[:subdomain],
411
+ 'username' => params[:username],
412
+ 'password' => params[:password])
413
+
414
+ contact.pagerduty_credentials.to_json
415
+ else
416
+ if params[:address].nil?
417
+ errors << "no address for '#{params[:id]}' media"
418
+ end
419
+
420
+ halt err(422, *errors) unless errors.empty?
421
+
422
+ contact.set_address_for_media(params[:id], params[:address])
423
+ contact.set_interval_for_media(params[:id], params[:interval])
424
+ contact.set_rollup_threshold_for_media(params[:id], params[:rollup_threshold])
425
+
426
+ {'address' => contact.media[params[:id]],
427
+ 'interval' => contact.media_intervals[params[:id]],
428
+ 'rollup_threshold' => contact.media_rollup_thresholds[params[:id]]}.to_json
429
+ end
430
+ end
431
+
432
+ # delete a media of a contact
433
+ app.delete('/contacts/:contact_id/media/:id') do
434
+ cors_headers
435
+ contact = find_contact(params[:contact_id])
436
+ contact.remove_media(params[:id])
437
+ status 204
438
+ end
439
+
440
+ # Returns the timezone of a contact
441
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_timezone
442
+ app.get('/contacts/:contact_id/timezone') do
443
+ content_type :json
444
+ cors_headers
445
+
446
+ contact = find_contact(params[:contact_id])
447
+ contact.timezone.name.to_json
448
+ end
449
+
450
+ # Sets the timezone of a contact
451
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
452
+ app.put('/contacts/:contact_id/timezone') do
453
+ content_type :json
454
+ cors_headers
455
+
456
+ contact = find_contact(params[:contact_id])
457
+ contact.timezone = params[:timezone]
458
+ contact.timezone.name.to_json
459
+ end
460
+
461
+ # Removes the timezone of a contact
462
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
463
+ app.delete('/contacts/:contact_id/timezone') do
464
+ cors_headers
465
+ contact = find_contact(params[:contact_id])
466
+ contact.timezone = nil
467
+ status 204
468
+ end
469
+
470
+ app.post '/contacts/:contact_id/tags' do
471
+ content_type :json
472
+ cors_headers
473
+
474
+ tags = find_tags(params[:tags])
475
+ contact = find_contact(params[:contact_id])
476
+ contact.add_tags(*tags)
477
+ '{"tags":' +
478
+ contact.tags.to_json +
479
+ '}'
480
+ end
481
+
482
+ app.post '/contacts/:contact_id/entity_tags' do
483
+ content_type :json
484
+ cors_headers
485
+ contact = find_contact(params[:contact_id])
486
+ contact.entities.map {|e| e[:entity]}.each do |entity|
487
+ next unless tags = params[:entity][entity.name]
488
+ entity.add_tags(*tags)
489
+ end
490
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
491
+ [et[:entity].name, et[:tags]]
492
+ }
493
+ contact_ent_tag.to_json
494
+ end
495
+
496
+ app.delete '/contacts/:contact_id/tags' do
497
+ cors_headers
498
+ tags = find_tags(params[:tags])
499
+ contact = find_contact(params[:contact_id])
500
+ contact.delete_tags(*tags)
501
+ status 204
502
+ end
503
+
504
+ app.delete '/contacts/:contact_id/entity_tags' do
505
+ cors_headers
506
+ contact = find_contact(params[:contact_id])
507
+ contact.entities.map {|e| e[:entity]}.each do |entity|
508
+ next unless tags = params[:entity][entity.name]
509
+ entity.delete_tags(*tags)
510
+ end
511
+ status 204
512
+ end
513
+
514
+ app.get '/contacts/:contact_id/tags' do
515
+ content_type :json
516
+ cors_headers
517
+
518
+ contact = find_contact(params[:contact_id])
519
+ '{"tags":' +
520
+ contact.tags.to_json +
521
+ '}'
522
+
523
+ end
524
+
525
+ app.get '/contacts/:contact_id/entity_tags' do
526
+ content_type :json
527
+ cors_headers
528
+
529
+ contact = find_contact(params[:contact_id])
530
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
531
+ [et[:entity].name, et[:tags]]
532
+ }
533
+ contact_ent_tag.to_json
534
+ end
535
+
536
+ end
537
+
538
+ end
539
+
540
+ end
541
+
542
+ end
543
+
544
+ end