]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - modules/private/monitoring/status/app.py
Add Eban monitoring
[perso/Immae/Config/Nix.git] / modules / private / monitoring / status / app.py
CommitLineData
6e9f30f4
IB
1from flask import Flask, request, render_template_string, jsonify, make_response
2from flask_login import LoginManager, UserMixin, login_required
3import socket
4import json
5import time
6import os
7
8login_manager = LoginManager()
9app = Flask(__name__)
10login_manager.init_app(app)
11
12STATUS = [
13 "ok",
14 "warning",
15 "error",
16 "unknown"
17 ]
18
19HOST_STATUS = [
20 "up",
21 "down",
22 "unreachable",
23 ]
24
25#### Push
26AUTHORIZED_KEYS = os.environ.get("TOKENS", "").split()
27COMMAND_FILE = "/var/run/naemon/naemon.cmd"
28
29ERROR_NO_REQUEST_HANDLER="NO REQUEST HANDLER"
30ERROR_NO_TOKEN_SUPPLIED="NO TOKEN"
31ERROR_BAD_TOKEN_SUPPLIED="BAD TOKEN"
32
33ERROR_BAD_COMMAND_FILE="BAD COMMAND FILE"
34ERROR_COMMAND_FILE_OPEN_WRITE="COMMAND FILE UNWRITEABLE"
35ERROR_COMMAND_FILE_OPEN="CANNOT OPEN COMMAND FILE"
36ERROR_BAD_WRITE="WRITE ERROR"
37
38ERROR_BAD_DATA="BAD DATA"
39ERROR_BAD_JSON="BAD JSON"
40
41ERROR_NO_CORRECT_STATUS="NO STATUS WAS CORRECT"
42#### /Push
43
44def 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
57class 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
81Filter: groups >= webstatus-hosts
82Columns: name alias state custom_variables
83OutputFormat: 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
90class 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
108Filter: name ~ ^webstatus-
109Columns: name alias custom_variables
110OutputFormat: 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
120class 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
154Columns: display_name host_name groups state custom_variables description plugin_output
155OutputFormat: json
156""")
157 return cls.parse_services(answer)
158
159 def __repr__(self):
160 return "Service {}: {}".format(self.name, self.webname)
161
2edbb2d8
IB
162def 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
173TEMPLATE='''<?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
296def 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
306def 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"])
314def 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
322def 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
344def 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
369def render_error(error):
370 return jsonify({
371 "status": "error",
372 "message": error,
373 })
374
375def 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
390class 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