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.
- 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
|