portertech-sensu 1.10.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +961 -0
  3. data/MIT-LICENSE.txt +20 -0
  4. data/README.md +65 -0
  5. data/exe/sensu-api +10 -0
  6. data/exe/sensu-client +10 -0
  7. data/exe/sensu-install +195 -0
  8. data/exe/sensu-server +10 -0
  9. data/lib/sensu/api/http_handler.rb +434 -0
  10. data/lib/sensu/api/process.rb +79 -0
  11. data/lib/sensu/api/routes/aggregates.rb +196 -0
  12. data/lib/sensu/api/routes/checks.rb +44 -0
  13. data/lib/sensu/api/routes/clients.rb +171 -0
  14. data/lib/sensu/api/routes/events.rb +86 -0
  15. data/lib/sensu/api/routes/health.rb +45 -0
  16. data/lib/sensu/api/routes/info.rb +37 -0
  17. data/lib/sensu/api/routes/request.rb +44 -0
  18. data/lib/sensu/api/routes/resolve.rb +32 -0
  19. data/lib/sensu/api/routes/results.rb +153 -0
  20. data/lib/sensu/api/routes/settings.rb +23 -0
  21. data/lib/sensu/api/routes/silenced.rb +182 -0
  22. data/lib/sensu/api/routes/stashes.rb +107 -0
  23. data/lib/sensu/api/routes.rb +88 -0
  24. data/lib/sensu/api/utilities/filter_response_content.rb +44 -0
  25. data/lib/sensu/api/utilities/publish_check_request.rb +107 -0
  26. data/lib/sensu/api/utilities/publish_check_result.rb +39 -0
  27. data/lib/sensu/api/utilities/resolve_event.rb +29 -0
  28. data/lib/sensu/api/utilities/servers_info.rb +43 -0
  29. data/lib/sensu/api/utilities/transport_info.rb +43 -0
  30. data/lib/sensu/api/validators/check.rb +55 -0
  31. data/lib/sensu/api/validators/client.rb +35 -0
  32. data/lib/sensu/api/validators/invalid.rb +8 -0
  33. data/lib/sensu/cli.rb +69 -0
  34. data/lib/sensu/client/http_socket.rb +217 -0
  35. data/lib/sensu/client/process.rb +655 -0
  36. data/lib/sensu/client/socket.rb +207 -0
  37. data/lib/sensu/client/utils.rb +53 -0
  38. data/lib/sensu/client/validators/check.rb +53 -0
  39. data/lib/sensu/constants.rb +17 -0
  40. data/lib/sensu/daemon.rb +396 -0
  41. data/lib/sensu/sandbox.rb +19 -0
  42. data/lib/sensu/server/filter.rb +227 -0
  43. data/lib/sensu/server/handle.rb +201 -0
  44. data/lib/sensu/server/mutate.rb +92 -0
  45. data/lib/sensu/server/process.rb +1646 -0
  46. data/lib/sensu/server/socket.rb +54 -0
  47. data/lib/sensu/server/tessen.rb +170 -0
  48. data/lib/sensu/utilities.rb +398 -0
  49. data/lib/sensu.rb +3 -0
  50. data/sensu.gemspec +36 -0
  51. metadata +322 -0
@@ -0,0 +1,434 @@
1
+ require "sensu/utilities"
2
+ require "sensu/api/routes"
3
+ require "sensu/api/utilities/filter_response_content"
4
+
5
+ gem "em-http-server", "0.1.8"
6
+
7
+ require "em-http-server"
8
+ require "base64"
9
+
10
+ module Sensu
11
+ module API
12
+ class HTTPHandler < EM::HttpServer::Server
13
+ include Routes
14
+ include Utilities::FilterResponseContent
15
+
16
+ attr_accessor :logger, :settings, :redis, :transport
17
+
18
+ # Create a hash containing the HTTP request details. This method
19
+ # determines the remote address for the HTTP client (using
20
+ # EventMachine Connection `get_peername()`).
21
+ #
22
+ # @result [Hash]
23
+ def request_details
24
+ return @request_details if @request_details
25
+ @request_id = @http.fetch(:x_request_id, random_uuid)
26
+ @request_start_time = Time.now.to_f
27
+ _, remote_address = Socket.unpack_sockaddr_in(get_peername)
28
+ @request_details = {
29
+ :request_id => @request_id,
30
+ :remote_address => remote_address,
31
+ :user_agent => @http[:user_agent],
32
+ :method => @http_request_method,
33
+ :uri => @http_request_uri,
34
+ :query_string => @http_query_string,
35
+ :body => @http_content
36
+ }
37
+ if @http[:x_forwarded_for]
38
+ @request_details[:x_forwarded_for] = @http[:x_forwarded_for]
39
+ end
40
+ @request_details
41
+ end
42
+
43
+ # Log the HTTP request. The debug log level is used for requests
44
+ # as response logging includes the same information.
45
+ def log_request
46
+ @logger.debug("api request", request_details)
47
+ end
48
+
49
+ # Log the HTTP response. This method calculates the
50
+ # request/response time. The debug log level is used for the
51
+ # response body log event, as it is generally very verbose and
52
+ # unnecessary in most cases.
53
+ def log_response
54
+ @logger.info("api response", {
55
+ :request => request_details,
56
+ :status => @response.status,
57
+ :content_length => @response.content.to_s.bytesize,
58
+ :time => (Time.now.to_f - @request_start_time).round(3)
59
+ })
60
+ end
61
+
62
+ # Parse the HTTP request URI using a regular expression,
63
+ # returning the URI unescaped match data values.
64
+ #
65
+ # @param regex [Regexp]
66
+ # @return [Array] URI unescaped match data values.
67
+ def parse_uri(regex)
68
+ uri_match = regex.match(@http_request_uri)[1..-1]
69
+ uri_match.map { |s| URI.decode_www_form_component(s) }
70
+ end
71
+
72
+ # Parse the HTTP request query string for parameters. This
73
+ # method creates `@params`, a hash of parsed query parameters,
74
+ # used by the API routes. This method also creates
75
+ # `@filter_params`, a hash of parsed response content filter
76
+ # parameters.
77
+ def parse_parameters
78
+ @params = {}
79
+ if @http_query_string
80
+ @http_query_string.split("&").each do |pair|
81
+ key, value = pair.split("=")
82
+ @params[key.to_sym] = value
83
+ if key.start_with?("filter.")
84
+ filter_param = key.sub(/^filter\./, "")
85
+ @filter_params ||= {}
86
+ @filter_params[filter_param] = value
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # Determine if a parameter has an integer value and if so return
93
+ # it as one. This method will return `nil` if the parameter
94
+ # value is not an integer.
95
+ #
96
+ # @param value [String]
97
+ # @return [Integer, nil]
98
+ def integer_parameter(value)
99
+ value =~ /\A[0-9]+\z/ ? value.to_i : nil
100
+ end
101
+
102
+ # Read JSON data from the HTTP request content and validate it
103
+ # with the provided rules. If the HTTP request content does not
104
+ # contain valid JSON or it does not pass validation, this method
105
+ # returns a `400` (Bad Request) HTTP response.
106
+ #
107
+ # @param rules [Hash] containing the validation rules.
108
+ # @yield [Object] the callback/block called with the data after successfully
109
+ # parsing and validating the the HTTP request content.
110
+ def read_data(rules={})
111
+ begin
112
+ data = Sensu::JSON.load(@http_content)
113
+ valid = data.is_a?(Hash) && rules.all? do |key, rule|
114
+ value = data[key]
115
+ (Array(rule[:type]).any? {|type| value.is_a?(type)} ||
116
+ (rule[:nil_ok] && value.nil?)) && (value.nil? || rule[:regex].nil?) ||
117
+ (rule[:regex] && value.is_a?(String) && (value =~ rule[:regex]) == 0)
118
+ end
119
+ if valid
120
+ yield(data)
121
+ else
122
+ bad_request!
123
+ end
124
+ rescue Sensu::JSON::ParseError
125
+ bad_request!
126
+ end
127
+ end
128
+
129
+ # Create an EM HTTP Server HTTP response object, `@response`.
130
+ # The response object is use to build up the response status,
131
+ # status string, content type, and content. The response object
132
+ # is responsible for sending the HTTP response to the HTTP
133
+ # client and closing the connection afterwards.
134
+ #
135
+ # @return [Object]
136
+ def create_response
137
+ @response = EM::DelegatedHttpResponse.new(self)
138
+ end
139
+
140
+ # Set the cors (Cross-origin resource sharing) HTTP headers.
141
+ def set_cors_headers
142
+ api = @settings[:api] || {}
143
+ api[:cors] ||= {
144
+ "Origin" => "*",
145
+ "Methods" => "GET, POST, PUT, DELETE, OPTIONS",
146
+ "Credentials" => "true",
147
+ "Headers" => "Origin, X-Requested-With, Content-Type, Accept, Authorization"
148
+ }
149
+ if api[:cors].is_a?(Hash)
150
+ api[:cors].each do |header, value|
151
+ @response.headers["Access-Control-Allow-#{header}"] = value
152
+ end
153
+ end
154
+ end
155
+
156
+ # Set the HTTP response headers, including the request ID and
157
+ # cors headers (via `set_cores_headers()`).
158
+ def set_headers
159
+ @response.headers["X-Request-ID"] = @request_id
160
+ set_cors_headers
161
+ end
162
+
163
+ # Paginate the provided items. This method uses two HTTP query
164
+ # parameters to determine how to paginate the items, `limit` and
165
+ # `offset`. The parameter `limit` specifies how many items are
166
+ # to be returned in the response. The parameter `offset`
167
+ # specifies the items array index, skipping a number of items.
168
+ # This method sets the "X-Pagination" HTTP response header to a
169
+ # JSON object containing the `limit`, `offset` and `total`
170
+ # number of items that are being paginated.
171
+ #
172
+ # @param items [Array]
173
+ # @return [Array] paginated items.
174
+ def pagination(items)
175
+ limit = integer_parameter(@params[:limit])
176
+ offset = integer_parameter(@params[:offset]) || 0
177
+ unless limit.nil?
178
+ @response.headers["X-Pagination"] = Sensu::JSON.dump(
179
+ :limit => limit,
180
+ :offset => offset,
181
+ :total => items.length
182
+ )
183
+ paginated = items.slice(offset, limit)
184
+ Array(paginated)
185
+ else
186
+ items
187
+ end
188
+ end
189
+
190
+ # Respond to an HTTP request. The routes set `@response_status`,
191
+ # `@response_status_string`, and `@response_content`
192
+ # appropriately. The HTTP response status defaults to `200` with
193
+ # the status string `OK`. If filter params were provided,
194
+ # `@response_content` is filtered (mutated). The Sensu API only
195
+ # returns JSON response content, `@response_content` is assumed
196
+ # to be a Ruby object that can be serialized as JSON.
197
+ def respond
198
+ @response.status = @response_status || 200
199
+ @response.status_string = @response_status_string || "OK"
200
+ if @response_content && @http_request_method != HEAD_METHOD
201
+ if @http_request_method == GET_METHOD && @filter_params
202
+ filter_response_content!
203
+ end
204
+ @response.content_type "application/json"
205
+ @response.content = Sensu::JSON.dump(@response_content)
206
+ end
207
+ @response.headers["Connection"] = "close"
208
+ log_response
209
+ @response.send_response
210
+ end
211
+
212
+ # Determine if an HTTP request is authorized. This method
213
+ # compares the configured API user and password (if any) with
214
+ # the HTTP request basic authentication credentials. No
215
+ # authentication is done if the API user and password are not
216
+ # configured. OPTIONS HTTP requests bypass authentication.
217
+ #
218
+ # @return [TrueClass, FalseClass]
219
+ def authorized?
220
+ api = @settings[:api]
221
+ if api && api[:user] && api[:password]
222
+ if @http_request_method == OPTIONS_METHOD
223
+ true
224
+ elsif @http[:authorization]
225
+ scheme, base64 = @http[:authorization].split("\s")
226
+ if scheme == "Basic"
227
+ user, password = Base64.decode64(base64).split(":")
228
+ user == api[:user] && password == api[:password]
229
+ else
230
+ false
231
+ end
232
+ else
233
+ false
234
+ end
235
+ else
236
+ true
237
+ end
238
+ end
239
+
240
+ # Determine if the API is connected to Redis and the Transport.
241
+ # This method sets the `@response_content` if the API is not
242
+ # connected or it has not yet initialized the connection
243
+ # objects. The `/info` and `/health` routes are excluded from
244
+ # the connectivity checks.
245
+ def connected?
246
+ connected = true
247
+ if @redis && @transport
248
+ unless @http_request_uri =~ INFO_URI || @http_request_uri =~ HEALTH_URI
249
+ unless @redis.connected?
250
+ @response_content = {:error => "not connected to redis"}
251
+ connected = false
252
+ end
253
+ unless @transport.connected?
254
+ @response_content = {:error => "not connected to transport"}
255
+ connected = false
256
+ end
257
+ end
258
+ else
259
+ @response_content = {:error => "redis and transport connections not initialized"}
260
+ connected = false
261
+ end
262
+ connected
263
+ end
264
+
265
+ # Respond to the HTTP request with a `201` (Created) response.
266
+ def created!
267
+ @response_status = 201
268
+ @response_status_string = "Created"
269
+ respond
270
+ end
271
+
272
+ # Respond to the HTTP request with a `202` (Accepted) response.
273
+ def accepted!
274
+ @response_status = 202
275
+ @response_status_string = "Accepted"
276
+ respond
277
+ end
278
+
279
+ # Respond to the HTTP request with a `204` (No Content)
280
+ # response.
281
+ def no_content!
282
+ @response_status = 204
283
+ @response_status_string = "No Content"
284
+ @response_content = nil
285
+ respond
286
+ end
287
+
288
+ # Respond to the HTTP request with a `400` (Bad Request)
289
+ # response.
290
+ def bad_request!
291
+ @response_status = 400
292
+ @response_status_string = "Bad Request"
293
+ respond
294
+ end
295
+
296
+ # Respond to the HTTP request with a `401` (Unauthroized)
297
+ # response. This method sets the "WWW-Autenticate" HTTP response
298
+ # header.
299
+ def unauthorized!
300
+ @response.headers["WWW-Authenticate"] = 'Basic realm="Restricted Area"'
301
+ @response_status = 401
302
+ @response_status_string = "Unauthorized"
303
+ respond
304
+ end
305
+
306
+ # Respond to the HTTP request with a `404` (Not Found) response.
307
+ def not_found!
308
+ @response_status = 404
309
+ @response_status_string = "Not Found"
310
+ respond
311
+ end
312
+
313
+ # Respond to the HTTP request with a `405` (Method Not Allowed) response.
314
+ def method_not_allowed!(allowed_http_methods=[])
315
+ @response.headers["Allow"] = allowed_http_methods.join(", ")
316
+ @response_status = 405
317
+ @response_status_string = "Method Not Allowed"
318
+ respond
319
+ end
320
+
321
+ # Respond to the HTTP request with a `412` (Precondition Failed)
322
+ # response.
323
+ def precondition_failed!
324
+ @response_status = 412
325
+ @response_status_string = "Precondition Failed"
326
+ respond
327
+ end
328
+
329
+ # Respond to the HTTP request with a `500` (Internal Server
330
+ # Error) response.
331
+ def error!
332
+ @response_status = 500
333
+ @response_status_string = "Internal Server Error"
334
+ respond
335
+ end
336
+
337
+ # Determine the allowed HTTP methods for a route. The route
338
+ # regular expressions and associated route method calls are
339
+ # provided by `ROUTES`. This method returns an array of HTTP
340
+ # methods that have a route that matches the HTTP request URI.
341
+ #
342
+ # @return [Array]
343
+ def allowed_http_methods?
344
+ ROUTES.map { |http_method, routes|
345
+ match = routes.detect do |route|
346
+ @http_request_uri =~ route[0]
347
+ end
348
+ match ? http_method : nil
349
+ }.flatten.compact
350
+ end
351
+
352
+ # Determine the route method for the HTTP request method and
353
+ # URI. The route regular expressions and associated route method
354
+ # calls are provided by `ROUTES`. This method will return the
355
+ # first route method name (Ruby symbol) that has matching URI
356
+ # regular expression. If an HTTP method is not supported, or
357
+ # there is not a matching regular expression, `nil` will be
358
+ # returned.
359
+ #
360
+ # @return [Symbol]
361
+ def determine_route_method
362
+ if ROUTES.has_key?(@http_request_method)
363
+ route = ROUTES[@http_request_method].detect do |route|
364
+ @http_request_uri =~ route[0]
365
+ end
366
+ route ? route[1] : nil
367
+ else
368
+ nil
369
+ end
370
+ end
371
+
372
+ # Route the HTTP request. OPTIONS HTTP requests will always
373
+ # return a `200` with no response content. This method uses
374
+ # `determine_route_method()` to determine the symbolized route
375
+ # method to send/call. If a route method does not exist for the
376
+ # HTTP request method and URI, this method uses
377
+ # `allowed_http_methods?()` to determine if a 404 (Not Found) or
378
+ # 405 (Method Not Allowed) HTTP response should be used.
379
+ def route_request
380
+ if @http_request_method == OPTIONS_METHOD
381
+ respond
382
+ else
383
+ route_method = determine_route_method
384
+ if route_method
385
+ send(route_method)
386
+ else
387
+ allowed_http_methods = allowed_http_methods?
388
+ if allowed_http_methods.empty?
389
+ not_found!
390
+ else
391
+ method_not_allowed!(allowed_http_methods)
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ # Process a HTTP request. Log the request, parse the HTTP query
398
+ # parameters, create the HTTP response object, set the cors HTTP
399
+ # response headers, determine if the request is authorized,
400
+ # determine if the API is connected to Redis and the Transport,
401
+ # and then route the HTTP request (responding to the request).
402
+ # This method is called by EM HTTP Server when handling a new
403
+ # connection.
404
+ def process_http_request
405
+ log_request
406
+ parse_parameters
407
+ create_response
408
+ set_headers
409
+ if authorized?
410
+ if connected?
411
+ route_request
412
+ else
413
+ error!
414
+ end
415
+ else
416
+ unauthorized!
417
+ end
418
+ end
419
+
420
+ # Catch uncaught/unexpected errors, log them, and attempt to
421
+ # respond with a `500` (Internal Server Error) HTTP response.
422
+ # This method is called by EM HTTP Server.
423
+ #
424
+ # @param error [Object]
425
+ def http_request_errback(error)
426
+ @logger.error("unexpected api error", {
427
+ :error => error.to_s,
428
+ :backtrace => error.backtrace.join("\n")
429
+ })
430
+ error! rescue nil
431
+ end
432
+ end
433
+ end
434
+ end
@@ -0,0 +1,79 @@
1
+ require "sensu/daemon"
2
+ require "sensu/api/http_handler"
3
+
4
+ module Sensu
5
+ module API
6
+ class Process
7
+ include Daemon
8
+
9
+ # Create an instance of the Sensu API process, setup the Redis
10
+ # and Transport connections, start the API HTTP server, set up
11
+ # API process signal traps (for stopping), within the
12
+ # EventMachine event loop.
13
+ #
14
+ # @param options [Hash]
15
+ def self.run(options={})
16
+ api = self.new(options)
17
+ EM::run do
18
+ api.setup_redis
19
+ api.setup_transport
20
+ api.start
21
+ api.setup_signal_traps
22
+ end
23
+ end
24
+
25
+ # Start the API HTTP server. This method sets `@http_server`.
26
+ #
27
+ # @param bind [String] address to listen on.
28
+ # @param port [Integer] to listen on.
29
+ def start_http_server(bind, port)
30
+ @logger.info("api listening", {
31
+ :protocol => "http",
32
+ :bind => bind,
33
+ :port => port
34
+ })
35
+ @http_server = EM::start_server(bind, port, HTTPHandler) do |handler|
36
+ handler.logger = @logger
37
+ handler.settings = @settings
38
+ handler.redis = @redis
39
+ handler.transport = @transport
40
+ end
41
+ end
42
+
43
+ # Start the Sensu API HTTP server. This method sets the service
44
+ # state to `:running`.
45
+ def start
46
+ api = @settings[:api] || {}
47
+ bind = api[:bind] || "0.0.0.0"
48
+ port = api[:port] || 4567
49
+ start_http_server(bind, port)
50
+ super
51
+ end
52
+
53
+ # Stop the Sensu API process. This method stops the HTTP server,
54
+ # closes the Redis and transport connections, sets the service
55
+ # state to `:stopped`, and stops the EventMachine event loop.
56
+ def stop
57
+ @logger.warn("stopping")
58
+ EM::stop_server(@http_server)
59
+ @redis.close if @redis
60
+ @transport.close if @transport
61
+ super
62
+ end
63
+
64
+ # Create an instance of the Sensu API with initialized
65
+ # connections for running test specs.
66
+ #
67
+ # @param options [Hash]
68
+ def self.test(options={})
69
+ api = self.new(options)
70
+ api.setup_redis do
71
+ api.setup_transport do
72
+ api.start
73
+ yield
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,196 @@
1
+ module Sensu
2
+ module API
3
+ module Routes
4
+ module Aggregates
5
+ AGGREGATES_URI = /^\/aggregates$/
6
+ AGGREGATE_URI = /^\/aggregates\/([\w\.-]+)$/
7
+ AGGREGATE_CLIENTS_URI = /^\/aggregates\/([\w\.-]+)\/clients$/
8
+ AGGREGATE_CHECKS_URI = /^\/aggregates\/([\w\.-]+)\/checks$/
9
+ AGGREGATE_RESULTS_SEVERITY_URI = /^\/aggregates\/([\w\.-]+)\/results\/([\w\.-]+)$/
10
+
11
+ # GET /aggregates
12
+ def get_aggregates
13
+ @redis.smembers("aggregates") do |aggregates|
14
+ aggregates = pagination(aggregates)
15
+ aggregates.map! do |aggregate|
16
+ {:name => aggregate}
17
+ end
18
+ @response_content = aggregates
19
+ respond
20
+ end
21
+ end
22
+
23
+ # GET /aggregates/:aggregate
24
+ def get_aggregate
25
+ aggregate = parse_uri(AGGREGATE_URI).first
26
+ @redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
27
+ unless aggregate_members.empty?
28
+ @response_content = {
29
+ :clients => 0,
30
+ :checks => 0,
31
+ :results => {
32
+ :ok => 0,
33
+ :warning => 0,
34
+ :critical => 0,
35
+ :unknown => 0,
36
+ :total => 0,
37
+ :stale => 0
38
+ }
39
+ }
40
+ clients = []
41
+ checks = []
42
+ results = []
43
+ aggregate_members.each_with_index do |member, index|
44
+ client_name, check_name = member.split(":")
45
+ clients << client_name
46
+ checks << check_name
47
+ result_key = "result:#{client_name}:#{check_name}"
48
+ @redis.get(result_key) do |result_json|
49
+ unless result_json.nil?
50
+ results << Sensu::JSON.load(result_json)
51
+ else
52
+ @redis.srem("aggregates:#{aggregate}", member)
53
+ end
54
+ if index == aggregate_members.length - 1
55
+ @response_content[:clients] = clients.uniq.length
56
+ @response_content[:checks] = checks.uniq.length
57
+ max_age = integer_parameter(@params[:max_age])
58
+ if max_age
59
+ result_count = results.length
60
+ timestamp = Time.now.to_i - max_age
61
+ results.reject! do |result|
62
+ result[:executed] && result[:executed] < timestamp
63
+ end
64
+ @response_content[:results][:stale] = result_count - results.length
65
+ end
66
+ @response_content[:results][:total] = results.length
67
+ results.each do |result|
68
+ severity = (SEVERITIES[result[:status]] || "unknown")
69
+ @response_content[:results][severity.to_sym] += 1
70
+ end
71
+ respond
72
+ end
73
+ end
74
+ end
75
+ else
76
+ not_found!
77
+ end
78
+ end
79
+ end
80
+
81
+ # DELETE /aggregates/:aggregate
82
+ def delete_aggregate
83
+ aggregate = parse_uri(AGGREGATE_URI).first
84
+ @redis.smembers("aggregates") do |aggregates|
85
+ if aggregates.include?(aggregate)
86
+ @redis.srem("aggregates", aggregate) do
87
+ @redis.del("aggregates:#{aggregate}") do
88
+ no_content!
89
+ end
90
+ end
91
+ else
92
+ not_found!
93
+ end
94
+ end
95
+ end
96
+
97
+ # GET /aggregates/:aggregate/clients
98
+ def get_aggregate_clients
99
+ aggregate = parse_uri(AGGREGATE_CLIENTS_URI).first
100
+ @response_content = []
101
+ @redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
102
+ unless aggregate_members.empty?
103
+ clients = {}
104
+ aggregate_members.each do |member|
105
+ client_name, check_name = member.split(":")
106
+ clients[client_name] ||= []
107
+ clients[client_name] << check_name
108
+ end
109
+ clients.each do |client_name, checks|
110
+ @response_content << {
111
+ :name => client_name,
112
+ :checks => checks
113
+ }
114
+ end
115
+ respond
116
+ else
117
+ not_found!
118
+ end
119
+ end
120
+ end
121
+
122
+ # GET /aggregates/:aggregate/checks
123
+ def get_aggregate_checks
124
+ aggregate = parse_uri(AGGREGATE_CHECKS_URI).first
125
+ @response_content = []
126
+ @redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
127
+ unless aggregate_members.empty?
128
+ checks = {}
129
+ aggregate_members.each do |member|
130
+ client_name, check_name = member.split(":")
131
+ checks[check_name] ||= []
132
+ checks[check_name] << client_name
133
+ end
134
+ checks.each do |check_name, clients|
135
+ @response_content << {
136
+ :name => check_name,
137
+ :clients => clients
138
+ }
139
+ end
140
+ respond
141
+ else
142
+ not_found!
143
+ end
144
+ end
145
+ end
146
+
147
+ # GET /aggregates/:aggregate/results/:severity
148
+ def get_aggregate_results_severity
149
+ aggregate, severity = parse_uri(AGGREGATE_RESULTS_SEVERITY_URI)
150
+ if SEVERITIES.include?(severity)
151
+ @redis.smembers("aggregates:#{aggregate}") do |aggregate_members|
152
+ unless aggregate_members.empty?
153
+ @response_content = []
154
+ summaries = Hash.new
155
+ max_age = integer_parameter(@params[:max_age])
156
+ current_timestamp = Time.now.to_i
157
+ aggregate_members.each_with_index do |member, index|
158
+ client_name, check_name = member.split(":")
159
+ result_key = "result:#{client_name}:#{check_name}"
160
+ @redis.get(result_key) do |result_json|
161
+ unless result_json.nil?
162
+ result = Sensu::JSON.load(result_json)
163
+ if SEVERITIES[result[:status]] == severity &&
164
+ (max_age.nil? || result[:executed].nil? || result[:executed] >= (current_timestamp - max_age))
165
+ summaries[check_name] ||= {}
166
+ summaries[check_name][result[:output]] ||= {:total => 0, :clients => []}
167
+ summaries[check_name][result[:output]][:total] += 1
168
+ summaries[check_name][result[:output]][:clients] << client_name
169
+ end
170
+ end
171
+ if index == aggregate_members.length - 1
172
+ summaries.each do |check_name, outputs|
173
+ summary = outputs.map do |output, output_summary|
174
+ {:output => output}.merge(output_summary)
175
+ end
176
+ @response_content << {
177
+ :check => check_name,
178
+ :summary => summary
179
+ }
180
+ end
181
+ respond
182
+ end
183
+ end
184
+ end
185
+ else
186
+ not_found!
187
+ end
188
+ end
189
+ else
190
+ bad_request!
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end