portertech-sensu 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +961 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.md +65 -0
- data/exe/sensu-api +10 -0
- data/exe/sensu-client +10 -0
- data/exe/sensu-install +195 -0
- data/exe/sensu-server +10 -0
- data/lib/sensu/api/http_handler.rb +434 -0
- data/lib/sensu/api/process.rb +79 -0
- data/lib/sensu/api/routes/aggregates.rb +196 -0
- data/lib/sensu/api/routes/checks.rb +44 -0
- data/lib/sensu/api/routes/clients.rb +171 -0
- data/lib/sensu/api/routes/events.rb +86 -0
- data/lib/sensu/api/routes/health.rb +45 -0
- data/lib/sensu/api/routes/info.rb +37 -0
- data/lib/sensu/api/routes/request.rb +44 -0
- data/lib/sensu/api/routes/resolve.rb +32 -0
- data/lib/sensu/api/routes/results.rb +153 -0
- data/lib/sensu/api/routes/settings.rb +23 -0
- data/lib/sensu/api/routes/silenced.rb +182 -0
- data/lib/sensu/api/routes/stashes.rb +107 -0
- data/lib/sensu/api/routes.rb +88 -0
- data/lib/sensu/api/utilities/filter_response_content.rb +44 -0
- data/lib/sensu/api/utilities/publish_check_request.rb +107 -0
- data/lib/sensu/api/utilities/publish_check_result.rb +39 -0
- data/lib/sensu/api/utilities/resolve_event.rb +29 -0
- data/lib/sensu/api/utilities/servers_info.rb +43 -0
- data/lib/sensu/api/utilities/transport_info.rb +43 -0
- data/lib/sensu/api/validators/check.rb +55 -0
- data/lib/sensu/api/validators/client.rb +35 -0
- data/lib/sensu/api/validators/invalid.rb +8 -0
- data/lib/sensu/cli.rb +69 -0
- data/lib/sensu/client/http_socket.rb +217 -0
- data/lib/sensu/client/process.rb +655 -0
- data/lib/sensu/client/socket.rb +207 -0
- data/lib/sensu/client/utils.rb +53 -0
- data/lib/sensu/client/validators/check.rb +53 -0
- data/lib/sensu/constants.rb +17 -0
- data/lib/sensu/daemon.rb +396 -0
- data/lib/sensu/sandbox.rb +19 -0
- data/lib/sensu/server/filter.rb +227 -0
- data/lib/sensu/server/handle.rb +201 -0
- data/lib/sensu/server/mutate.rb +92 -0
- data/lib/sensu/server/process.rb +1646 -0
- data/lib/sensu/server/socket.rb +54 -0
- data/lib/sensu/server/tessen.rb +170 -0
- data/lib/sensu/utilities.rb +398 -0
- data/lib/sensu.rb +3 -0
- data/sensu.gemspec +36 -0
- 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
|