]>
Commit | Line | Data |
---|---|---|
6e9f30f4 IB |
1 | from flask import Flask, request, render_template_string, jsonify, make_response |
2 | from flask_login import LoginManager, UserMixin, login_required | |
3 | import socket | |
4 | import json | |
5 | import time | |
6 | import os | |
7 | ||
8 | login_manager = LoginManager() | |
9 | app = Flask(__name__) | |
10 | login_manager.init_app(app) | |
11 | ||
12 | STATUS = [ | |
13 | "ok", | |
14 | "warning", | |
15 | "error", | |
16 | "unknown" | |
17 | ] | |
18 | ||
19 | HOST_STATUS = [ | |
20 | "up", | |
21 | "down", | |
22 | "unreachable", | |
23 | ] | |
24 | ||
25 | #### Push | |
26 | AUTHORIZED_KEYS = os.environ.get("TOKENS", "").split() | |
27 | COMMAND_FILE = "/var/run/naemon/naemon.cmd" | |
28 | ||
29 | ERROR_NO_REQUEST_HANDLER="NO REQUEST HANDLER" | |
30 | ERROR_NO_TOKEN_SUPPLIED="NO TOKEN" | |
31 | ERROR_BAD_TOKEN_SUPPLIED="BAD TOKEN" | |
32 | ||
33 | ERROR_BAD_COMMAND_FILE="BAD COMMAND FILE" | |
34 | ERROR_COMMAND_FILE_OPEN_WRITE="COMMAND FILE UNWRITEABLE" | |
35 | ERROR_COMMAND_FILE_OPEN="CANNOT OPEN COMMAND FILE" | |
36 | ERROR_BAD_WRITE="WRITE ERROR" | |
37 | ||
38 | ERROR_BAD_DATA="BAD DATA" | |
39 | ERROR_BAD_JSON="BAD JSON" | |
40 | ||
41 | ERROR_NO_CORRECT_STATUS="NO STATUS WAS CORRECT" | |
42 | #### /Push | |
43 | ||
44 | def get_lq(request): | |
45 | # https://mathias-kettner.de/checkmk_livestatus.html | |
46 | socket_path="/var/run/naemon/live" | |
47 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
48 | s.connect(socket_path) | |
49 | s.send(request.encode()) | |
50 | s.shutdown(socket.SHUT_WR) | |
51 | chunks = [] | |
52 | while len(chunks) == 0 or len(chunks[-1]) > 0: | |
53 | chunks.append(s.recv(4096)) | |
54 | s.close() | |
55 | return b"".join(chunks).decode() | |
56 | ||
57 | class Host: | |
2edbb2d8 | 58 | def __init__(self, name, alias, status, webname, vhost): |
6e9f30f4 IB |
59 | self.name = name |
60 | self.alias = alias | |
61 | self.webname = webname or alias | |
2edbb2d8 | 62 | self.vhost = vhost |
6e9f30f4 IB |
63 | self.status = status |
64 | self.services = [] | |
65 | ||
66 | @classmethod | |
2edbb2d8 IB |
67 | def parse_hosts(cls, payload, vhost): |
68 | parsed = filter(lambda x: x.vhost == vhost, [cls.parse(p) for p in json.loads(payload)]) | |
6e9f30f4 IB |
69 | return {p.name: p for p in parsed} |
70 | ||
71 | @classmethod | |
72 | def parse(cls, payload): | |
2edbb2d8 | 73 | return cls(payload[0], payload[1], HOST_STATUS[payload[2]], payload[3].get("WEBSTATUS_NAME"), payload[3].get("WEBSTATUS_VHOST")) |
6e9f30f4 IB |
74 | |
75 | def __repr__(self): | |
76 | return "Host {}: {} ({})".format(self.name, self.alias, self.webname) | |
77 | ||
78 | @classmethod | |
2edbb2d8 | 79 | def query(cls, vhost): |
6e9f30f4 IB |
80 | answer = get_lq("""GET hosts |
81 | Filter: groups >= webstatus-hosts | |
82 | Columns: name alias state custom_variables | |
83 | OutputFormat: json | |
84 | """) | |
2edbb2d8 | 85 | return cls.parse_hosts(answer, vhost) |
6e9f30f4 IB |
86 | |
87 | def fill_services(self, services): | |
88 | self.services = [service for service in services if service.host == self.name] | |
89 | ||
90 | class ServiceGroup: | |
91 | def __init__(self, name, alias): | |
92 | self.name = name | |
93 | self.alias = alias | |
94 | self.services = [] | |
95 | ||
96 | @classmethod | |
97 | def parse_groups(cls, payload): | |
98 | parsed = [cls.parse(p) for p in json.loads(payload)] | |
99 | return {p.name: p for p in parsed} | |
100 | ||
101 | @classmethod | |
102 | def parse(cls, payload): | |
103 | return cls(payload[0], payload[1]) | |
104 | ||
105 | @classmethod | |
106 | def query(cls): | |
107 | answer = get_lq("""GET servicegroups | |
108 | Filter: name ~ ^webstatus- | |
109 | Columns: name alias custom_variables | |
110 | OutputFormat: json | |
111 | """) | |
112 | return cls.parse_groups(answer) | |
113 | ||
2edbb2d8 IB |
114 | def fill_services(self, services, hosts): |
115 | self.services = [service for service in services if any([group == self.name for group in service.groups]) and service.host in hosts] | |
6e9f30f4 IB |
116 | |
117 | def __repr__(self): | |
118 | return "ServiceGroup {}: {}".format(self.name, self.alias) | |
119 | ||
120 | class Service: | |
121 | def __init__(self, name, host, groups, status, webname, url, description, infos): | |
122 | self.name = name | |
123 | self.host = host | |
124 | self.groups = groups | |
125 | self.status = status | |
126 | self.webname = webname | |
127 | self.url = url | |
128 | self.description = description | |
129 | self.infos = infos | |
130 | ||
131 | @classmethod | |
132 | def parse_services(cls, payload): | |
133 | parsed = json.loads(payload) | |
134 | return [cls.parse(p) for p in parsed if cls.valid(p[2])] | |
135 | ||
136 | @staticmethod | |
137 | def valid(groups): | |
138 | return any([b.startswith("webstatus-") for b in groups]) | |
139 | ||
140 | @classmethod | |
141 | def parse(cls, payload): | |
142 | return cls(payload[0], | |
143 | payload[1], | |
144 | payload[2], | |
145 | STATUS[payload[3]], | |
146 | payload[4].get("WEBSTATUS_NAME"), | |
147 | payload[4].get("WEBSTATUS_URL"), | |
148 | payload[5], | |
149 | payload[6]) | |
150 | ||
151 | @classmethod | |
152 | def query(cls): | |
153 | answer = get_lq("""GET services | |
154 | Columns: display_name host_name groups state custom_variables description plugin_output | |
155 | OutputFormat: json | |
156 | """) | |
157 | return cls.parse_services(answer) | |
158 | ||
159 | def __repr__(self): | |
160 | return "Service {}: {}".format(self.name, self.webname) | |
161 | ||
2edbb2d8 IB |
162 | def get_infos(vhost): |
163 | hosts = Host.query(vhost) | |
6e9f30f4 IB |
164 | servicegroups = ServiceGroup.query() |
165 | services = Service.query() | |
166 | ||
167 | for host in hosts: | |
168 | hosts[host].fill_services(services) | |
169 | for group in servicegroups: | |
2edbb2d8 | 170 | servicegroups[group].fill_services(services, hosts) |
6e9f30f4 IB |
171 | return (hosts, servicegroups, services) |
172 | ||
173 | TEMPLATE='''<?xml version="1.0" encoding="UTF-8"?> | |
174 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | |
175 | <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> | |
176 | <head> | |
177 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
178 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
179 | <title>Status</title> | |
180 | <meta name="referrer" content="no-referrer" /> | |
181 | <style type="text/css"> | |
182 | ul { | |
183 | list-style: none; | |
184 | margin: 0px; | |
185 | } | |
186 | ul li:nth-child(2n) { | |
187 | background-color: rgb(240, 240, 240); | |
188 | } | |
189 | li.resource, li.service { | |
190 | margin: 1px 0px; | |
191 | } | |
192 | span.status { | |
193 | display: inline-block; | |
194 | width: 150px; | |
195 | text-align: center; | |
196 | margin-right: 5px; | |
197 | font-variant: small-caps; | |
198 | font-size: 1.2em; | |
199 | } | |
200 | .status_ok,.status_up { | |
201 | background-color: rgba(0, 255, 0, 0.5);; | |
202 | } | |
203 | .status_warning { | |
204 | background-color: rgba(255, 255, 0, 0.5);; | |
205 | } | |
206 | .status_error,.status_down { | |
207 | background-color: rgba(255, 0, 0, 0.5);; | |
208 | } | |
209 | .status_unknown,.status_unreachable { | |
210 | background-color: rgba(0, 0, 255, 0.5);; | |
211 | } | |
212 | .infos { | |
213 | margin-left: 40px; | |
214 | color: rgb(100, 100, 100); | |
215 | } | |
216 | div#services { | |
217 | column-count: auto; | |
218 | column-width: 36em; | |
219 | } | |
220 | div.servicegroup { | |
221 | -webkit-column-break-inside: avoid; | |
222 | break-inside: avoid; | |
223 | } | |
224 | h3.servicegroup_title, h3.host_title { | |
225 | margin: 1px 0px; | |
226 | } | |
227 | span.service_host, span.infos { | |
228 | float: right; | |
229 | display: inline-block; | |
230 | color: rgb(100, 100, 100); | |
231 | } | |
232 | </style> | |
233 | </head> | |
234 | <body> | |
235 | <h2>Hosts</h2> | |
236 | {%- for host in hosts.values() %} | |
237 | <h3 class="host_title"> | |
238 | <span class="status status_{{ host.status }}">{{ host.status }}</span> | |
239 | <span class="host">{{ host.webname }}</span> | |
240 | </h3> | |
241 | {%- for service in servicegroups["webstatus-resources"].services if service.host == host.name -%} | |
242 | {%- if loop.first %} | |
243 | <ul class="resources"> | |
244 | {% endif %} | |
245 | ||
246 | <li class="resource"> | |
247 | <span class="status status_{{ service.status }}">{{ service.status }}</span> | |
248 | <span class="description">{{ service.description }}</span> | |
249 | <span class="infos">{{ service.infos }}</span> | |
250 | </li> | |
251 | ||
252 | {%- if loop.last %} | |
253 | </ul> | |
254 | {% endif %} | |
255 | {% endfor %} | |
256 | {%- endfor %} | |
257 | ||
2edbb2d8 IB |
258 | {%- for group in servicegroups.values() if group.services and group.name != "webstatus-resources" %} |
259 | {%- if loop.first %} | |
6e9f30f4 IB |
260 | <h2>Services</h2> |
261 | <div id="services"> | |
2edbb2d8 | 262 | {%- endif %} |
6e9f30f4 IB |
263 | <div class="servicegroup"> |
264 | <h3 class="servicegroup_title">{{ group.alias }}</h3> | |
2edbb2d8 | 265 | {%- for service in group.services if service.host in hosts -%} |
6e9f30f4 IB |
266 | {%- if loop.first %} |
267 | <ul class="services"> | |
268 | {% endif %} | |
269 | ||
270 | <li class="service" title="{{ service.infos }}"> | |
271 | <span class="status status_{{ service.status }}">{{ service.status }}</span> | |
272 | <span class="description"> | |
273 | {% if service.url and service.url.startswith("https://") %} | |
274 | <a href="{{ service.url }}">{{ service.webname or service.description }}</a> | |
275 | {% else %} | |
276 | {{ service.webname or service.description }} | |
277 | {% endif %} | |
278 | </span> | |
279 | <span class="service_host">{{ hosts[service.host].webname }}</span> | |
280 | </li> | |
281 | ||
282 | {%- if loop.last %} | |
283 | </ul> | |
284 | {% endif %} | |
285 | {%- endfor -%} | |
286 | </div> | |
2edbb2d8 | 287 | {%- if loop.last %} |
6e9f30f4 | 288 | </div> |
2edbb2d8 IB |
289 | {% endif %} |
290 | {%- endfor %} | |
6e9f30f4 IB |
291 | </body> |
292 | </html> | |
293 | ''' | |
294 | ||
295 | @login_manager.request_loader | |
296 | def load_user_from_request(request): | |
297 | api_key = request.headers.get('Token') | |
298 | if api_key in AUTHORIZED_KEYS: | |
299 | return UserMixin() | |
300 | content = request.get_json(force=True, silent=True) | |
301 | if content is not None and content.get("token") in AUTHORIZED_KEYS: | |
302 | return UserMixin() | |
303 | ||
304 | @app.route("/live", methods=["POST"]) | |
305 | @login_required | |
306 | def live(): | |
307 | query = request.get_data() | |
308 | result = get_lq(query.decode() + "\n") | |
309 | resp = make_response(result) | |
310 | resp.content_type = "text/plain" | |
311 | return resp | |
312 | ||
313 | @app.route("/", methods=["GET"]) | |
314 | def get(): | |
2edbb2d8 | 315 | (hosts, servicegroups, services) = get_infos(request.host) |
6e9f30f4 IB |
316 | resp = make_response(render_template_string(TEMPLATE, hosts=hosts, servicegroups=servicegroups)) |
317 | resp.content_type = "text/html" | |
318 | return resp | |
319 | ||
320 | @app.route("/", methods=["POST"]) | |
321 | @login_required | |
322 | def push(): | |
323 | content = request.get_json(force=True, silent=True) | |
324 | if content is None: | |
325 | return ERROR_BAD_JSON | |
326 | if content.get("cmd") != "submitcheck": | |
327 | return render_error(ERROR_NO_REQUEST_HANDLER) | |
328 | if "checkresult" not in content or not isinstance(content["checkresult"], list): | |
329 | return render_error(ERROR_BAD_DATA) | |
330 | ||
331 | checks = 0 | |
332 | errors = 0 | |
333 | for check in map(lambda x: CheckResult.from_json(x), content["checkresult"]): | |
334 | if check is None: | |
335 | errors += 1 | |
336 | continue | |
337 | try: | |
338 | write_check_output(check) | |
339 | except Exception as e: | |
340 | return render_error(str(e)) | |
341 | checks += 1 | |
342 | return render_response(checks, errors) | |
343 | ||
344 | def write_check_output(check): | |
345 | if check.type== "service": | |
346 | command = "[{time}] PROCESS_SERVICE_CHECK_RESULT;{hostname};{servicename};{state};{output}"; | |
347 | else: | |
348 | command = "[{time}] PROCESS_HOST_CHECK_RESULT;{hostname};{state};{output}"; | |
349 | formatted = command.format( | |
350 | time=int(time.time()), | |
351 | hostname=check.hostname, | |
352 | state=check.state, | |
353 | output=check.output, | |
354 | servicename=check.servicename, | |
355 | ) | |
356 | ||
357 | if not os.path.exists(COMMAND_FILE): | |
358 | raise Exception(ERROR_BAD_COMMAND_FILE) | |
359 | if not os.access(COMMAND_FILE, os.W_OK): | |
360 | raise Exception(ERROR_COMMAND_FILE_OPEN_WRITE) | |
361 | if not os.access(COMMAND_FILE, os.W_OK): | |
362 | raise Exception(ERROR_COMMAND_FILE_OPEN_WRITE) | |
363 | try: | |
364 | with open(COMMAND_FILE, "w") as c: | |
365 | c.write(formatted + "\n") | |
366 | except Exception as e: | |
367 | raise Exception(ERROR_BAD_WRITE) | |
368 | ||
369 | def render_error(error): | |
370 | return jsonify({ | |
371 | "status": "error", | |
372 | "message": error, | |
373 | }) | |
374 | ||
375 | def render_response(checks, errors): | |
376 | if checks > 0: | |
377 | return jsonify({ | |
378 | "status": "ok", | |
379 | "result": { | |
380 | "checks": checks, | |
381 | "errors": errors, | |
382 | } | |
383 | }) | |
384 | else: | |
385 | return jsonify({ | |
386 | "status": "error", | |
387 | "message": ERROR_NO_CORRECT_STATUS, | |
388 | }) | |
389 | ||
390 | class CheckResult: | |
391 | def __init__(self, hostname, state, output, servicename, checktype): | |
392 | self.hostname = hostname | |
393 | self.state = state | |
394 | self.output = output | |
395 | self.servicename = servicename | |
396 | self.type = checktype | |
397 | ||
398 | @classmethod | |
399 | def from_json(klass, j): | |
400 | if not isinstance(j, dict): | |
401 | return None | |
402 | for key in ["hostname", "state", "output"]: | |
403 | if key not in j or not isinstance(j[key], str): | |
404 | return None | |
405 | for key in ["servicename", "type"]: | |
406 | if key in j and not isinstance(j[key], str): | |
407 | return None | |
408 | return klass( | |
409 | j["hostname"], | |
410 | j["state"], | |
411 | j["output"], | |
412 | j.get("servicename", ""), | |
413 | j.get("type", "host")) | |
414 |