flapjack 0.7.35 → 0.8.0

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