1 from flask
import Flask
, request
, render_template_string
, jsonify
, make_response
2 from flask_login
import LoginManager
, UserMixin
, login_required
8 login_manager
= LoginManager()
10 login_manager
.init_app(app
)
26 AUTHORIZED_KEYS
= os
.environ
.get("TOKENS", "").split()
27 COMMAND_FILE
= "/var/run/naemon/naemon.cmd"
29 ERROR_NO_REQUEST_HANDLER
="NO REQUEST HANDLER"
30 ERROR_NO_TOKEN_SUPPLIED
="NO TOKEN"
31 ERROR_BAD_TOKEN_SUPPLIED
="BAD TOKEN"
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"
38 ERROR_BAD_DATA
="BAD DATA"
39 ERROR_BAD_JSON
="BAD JSON"
41 ERROR_NO_CORRECT_STATUS
="NO STATUS WAS CORRECT"
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
)
52 while len(chunks
) == 0 or len(chunks
[-1]) > 0:
53 chunks
.append(s
.recv(4096))
55 return b
"".join(chunks
).decode()
58 def __init__(self
, name
, alias
, status
, webname
, vhost
):
61 self
.webname
= webname
or alias
67 def parse_hosts(cls
, payload
, vhost
):
68 parsed
= filter(lambda x
: x
.vhost
== vhost
, [cls
.parse(p
) for p
in json
.loads(payload
)])
69 return {p.name: p for p in parsed}
72 def parse(cls
, payload
):
73 return cls(payload
[0], payload
[1], HOST_STATUS
[payload
[2]], payload
[3].get("WEBSTATUS_NAME"), payload
[3].get("WEBSTATUS_VHOST"))
76 return "Host {}: {} ({})".format(self
.name
, self
.alias
, self
.webname
)
79 def query(cls
, vhost
):
80 answer
= get_lq("""GET hosts
81 Filter: groups >= webstatus-hosts
82 Columns: name alias state custom_variables
85 return cls
.parse_hosts(answer
, vhost
)
87 def fill_services(self
, services
):
88 self
.services
= [service
for service
in services
if service
.host
== self
.name
]
91 def __init__(self
, name
, alias
):
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}
102 def parse(cls
, payload
):
103 return cls(payload
[0], payload
[1])
107 answer
= get_lq("""GET servicegroups
108 Filter: name ~ ^webstatus-
109 Columns: name alias custom_variables
112 return cls
.parse_groups(answer
)
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
]
118 return "ServiceGroup {}: {}".format(self
.name
, self
.alias
)
121 def __init__(self
, name
, host
, groups
, status
, webname
, url
, description
, infos
):
126 self
.webname
= webname
128 self
.description
= description
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])]
138 return any([b
.startswith("webstatus-") for b
in groups
])
141 def parse(cls
, payload
):
142 return cls(payload
[0],
146 payload
[4].get("WEBSTATUS_NAME"),
147 payload
[4].get("WEBSTATUS_URL"),
153 answer
= get_lq("""GET services
154 Columns: display_name host_name groups state custom_variables description plugin_output
157 return cls
.parse_services(answer
)
160 return "Service {}: {}".format(self
.name
, self
.webname
)
162 def get_infos(vhost
):
163 hosts
= Host
.query(vhost
)
164 servicegroups
= ServiceGroup
.query()
165 services
= Service
.query()
168 hosts
[host
].fill_services(services
)
169 for group
in servicegroups
:
170 servicegroups
[group
].fill_services(services
, hosts
)
171 return (hosts
, servicegroups
, services
)
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">
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">
186 ul li:nth-child(2n) {
187 background-color: rgb(240, 240, 240);
189 li.resource, li.service {
193 display: inline-block;
197 font-variant: small-caps;
200 .status_ok,.status_up {
201 background-color: rgba(0, 255, 0, 0.5);;
204 background-color: rgba(255, 255, 0, 0.5);;
206 .status_error,.status_down {
207 background-color: rgba(255, 0, 0, 0.5);;
209 .status_unknown,.status_unreachable {
210 background-color: rgba(0, 0, 255, 0.5);;
214 color: rgb(100, 100, 100);
221 -webkit-column-break-inside: avoid;
224 h3.servicegroup_title, h3.host_title {
227 span.service_host, span.infos {
229 display: inline-block;
230 color: rgb(100, 100, 100);
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>
241 {%- for service in servicegroups["webstatus-resources"].services if service.host == host.name -%}
243 <ul class="resources">
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>
258 {%- for group in servicegroups.values() if group.services and group.name != "webstatus-resources" %}
263 <div class="servicegroup">
264 <h3 class="servicegroup_title">{{ group.alias }}</h3>
265 {%- for service in group.services if service.host in hosts -%}
267 <ul class="services">
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>
276 {{ service.webname or service.description }}
279 <span class="service_host">{{ hosts[service.host].webname }}</span>
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
:
300 content
= request
.get_json(force
=True, silent
=True)
301 if content
is not None and content
.get("token") in AUTHORIZED_KEYS
:
304 @app.route("/live", methods
=["POST"])
307 query
= request
.get_data()
308 result
= get_lq(query
.decode() + "\n")
309 resp
= make_response(result
)
310 resp
.content_type
= "text/plain"
313 @app.route("/", methods
=["GET"])
315 (hosts
, servicegroups
, services
) = get_infos(request
.host
)
316 resp
= make_response(render_template_string(TEMPLATE
, hosts
=hosts
, servicegroups
=servicegroups
))
317 resp
.content_type
= "text/html"
320 @app.route("/", methods
=["POST"])
323 content
= request
.get_json(force
=True, silent
=True)
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
)
333 for check
in map(lambda x
: CheckResult
.from_json(x
), content
["checkresult"]):
338 write_check_output(check
)
339 except Exception as e
:
340 return render_error(str(e
))
342 return render_response(checks
, errors
)
344 def write_check_output(check
):
345 if check
.type== "service":
346 command
= "[{time}] PROCESS_SERVICE_CHECK_RESULT;{hostname};{servicename};{state};{output}";
348 command
= "[{time}] PROCESS_HOST_CHECK_RESULT;{hostname};{state};{output}";
349 formatted
= command
.format(
350 time
=int(time
.time()),
351 hostname
=check
.hostname
,
354 servicename
=check
.servicename
,
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
)
364 with open(COMMAND_FILE
, "w") as c
:
365 c
.write(formatted
+ "\n")
366 except Exception as e
:
367 raise Exception(ERROR_BAD_WRITE
)
369 def render_error(error
):
375 def render_response(checks
, errors
):
387 "message": ERROR_NO_CORRECT_STATUS
,
391 def __init__(self
, hostname
, state
, output
, servicename
, checktype
):
392 self
.hostname
= hostname
395 self
.servicename
= servicename
396 self
.type = checktype
399 def from_json(klass
, j
):
400 if not isinstance(j
, dict):
402 for key
in ["hostname", "state", "output"]:
403 if key
not in j
or not isinstance(j
[key
], str):
405 for key
in ["servicename", "type"]:
406 if key
in j
and not isinstance(j
[key
], str):
412 j
.get("servicename", ""),
413 j
.get("type", "host"))