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
):
61 self
.webname
= webname
or alias
66 def parse_hosts(cls
, payload
):
67 parsed
= [cls
.parse(p
) for p
in json
.loads(payload
)]
68 return {p.name: p for p in parsed}
71 def parse(cls
, payload
):
72 return cls(payload
[0], payload
[1], HOST_STATUS
[payload
[2]], payload
[3].get("WEBSTATUS_NAME"))
75 return "Host {}: {} ({})".format(self
.name
, self
.alias
, self
.webname
)
79 answer
= get_lq("""GET hosts
80 Filter: groups >= webstatus-hosts
81 Columns: name alias state custom_variables
84 return cls
.parse_hosts(answer
)
86 def fill_services(self
, services
):
87 self
.services
= [service
for service
in services
if service
.host
== self
.name
]
90 def __init__(self
, name
, alias
):
96 def parse_groups(cls
, payload
):
97 parsed
= [cls
.parse(p
) for p
in json
.loads(payload
)]
98 return {p.name: p for p in parsed}
101 def parse(cls
, payload
):
102 return cls(payload
[0], payload
[1])
106 answer
= get_lq("""GET servicegroups
107 Filter: name ~ ^webstatus-
108 Columns: name alias custom_variables
111 return cls
.parse_groups(answer
)
113 def fill_services(self
, services
):
114 self
.services
= [service
for service
in services
if any([group
== self
.name
for group
in service
.groups
])]
117 return "ServiceGroup {}: {}".format(self
.name
, self
.alias
)
120 def __init__(self
, name
, host
, groups
, status
, webname
, url
, description
, infos
):
125 self
.webname
= webname
127 self
.description
= description
131 def parse_services(cls
, payload
):
132 parsed
= json
.loads(payload
)
133 return [cls
.parse(p
) for p
in parsed
if cls
.valid(p
[2])]
137 return any([b
.startswith("webstatus-") for b
in groups
])
140 def parse(cls
, payload
):
141 return cls(payload
[0],
145 payload
[4].get("WEBSTATUS_NAME"),
146 payload
[4].get("WEBSTATUS_URL"),
152 answer
= get_lq("""GET services
153 Columns: display_name host_name groups state custom_variables description plugin_output
156 return cls
.parse_services(answer
)
159 return "Service {}: {}".format(self
.name
, self
.webname
)
163 servicegroups
= ServiceGroup
.query()
164 services
= Service
.query()
167 hosts
[host
].fill_services(services
)
168 for group
in servicegroups
:
169 servicegroups
[group
].fill_services(services
)
170 return (hosts
, servicegroups
, services
)
172 TEMPLATE
='''<?xml version="1.0" encoding="UTF-8"?>
173 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
174 <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
176 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
177 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
178 <title>Status</title>
179 <meta name="referrer" content="no-referrer" />
180 <style type="text/css">
185 ul li:nth-child(2n) {
186 background-color: rgb(240, 240, 240);
188 li.resource, li.service {
192 display: inline-block;
196 font-variant: small-caps;
199 .status_ok,.status_up {
200 background-color: rgba(0, 255, 0, 0.5);;
203 background-color: rgba(255, 255, 0, 0.5);;
205 .status_error,.status_down {
206 background-color: rgba(255, 0, 0, 0.5);;
208 .status_unknown,.status_unreachable {
209 background-color: rgba(0, 0, 255, 0.5);;
213 color: rgb(100, 100, 100);
220 -webkit-column-break-inside: avoid;
223 h3.servicegroup_title, h3.host_title {
226 span.service_host, span.infos {
228 display: inline-block;
229 color: rgb(100, 100, 100);
235 {%- for host in hosts.values() %}
236 <h3 class="host_title">
237 <span class="status status_{{ host.status }}">{{ host.status }}</span>
238 <span class="host">{{ host.webname }}</span>
240 {%- for service in servicegroups["webstatus-resources"].services if service.host == host.name -%}
242 <ul class="resources">
245 <li class="resource">
246 <span class="status status_{{ service.status }}">{{ service.status }}</span>
247 <span class="description">{{ service.description }}</span>
248 <span class="infos">{{ service.infos }}</span>
259 {%- for group in servicegroups.values() if group.services and group.name != "webstatus-resources" %}
260 <div class="servicegroup">
261 <h3 class="servicegroup_title">{{ group.alias }}</h3>
262 {%- for service in group.services -%}
264 <ul class="services">
267 <li class="service" title="{{ service.infos }}">
268 <span class="status status_{{ service.status }}">{{ service.status }}</span>
269 <span class="description">
270 {% if service.url and service.url.startswith("https://") %}
271 <a href="{{ service.url }}">{{ service.webname or service.description }}</a>
273 {{ service.webname or service.description }}
276 <span class="service_host">{{ hosts[service.host].webname }}</span>
290 @login_manager.request_loader
291 def load_user_from_request(request
):
292 api_key
= request
.headers
.get('Token')
293 if api_key
in AUTHORIZED_KEYS
:
295 content
= request
.get_json(force
=True, silent
=True)
296 if content
is not None and content
.get("token") in AUTHORIZED_KEYS
:
299 @app.route("/live", methods
=["POST"])
302 query
= request
.get_data()
303 result
= get_lq(query
.decode() + "\n")
304 resp
= make_response(result
)
305 resp
.content_type
= "text/plain"
308 @app.route("/", methods
=["GET"])
310 (hosts
, servicegroups
, services
) = get_infos()
311 resp
= make_response(render_template_string(TEMPLATE
, hosts
=hosts
, servicegroups
=servicegroups
))
312 resp
.content_type
= "text/html"
315 @app.route("/", methods
=["POST"])
318 content
= request
.get_json(force
=True, silent
=True)
320 return ERROR_BAD_JSON
321 if content
.get("cmd") != "submitcheck":
322 return render_error(ERROR_NO_REQUEST_HANDLER
)
323 if "checkresult" not in content
or not isinstance(content
["checkresult"], list):
324 return render_error(ERROR_BAD_DATA
)
328 for check
in map(lambda x
: CheckResult
.from_json(x
), content
["checkresult"]):
333 write_check_output(check
)
334 except Exception as e
:
335 return render_error(str(e
))
337 return render_response(checks
, errors
)
339 def write_check_output(check
):
340 if check
.type== "service":
341 command
= "[{time}] PROCESS_SERVICE_CHECK_RESULT;{hostname};{servicename};{state};{output}";
343 command
= "[{time}] PROCESS_HOST_CHECK_RESULT;{hostname};{state};{output}";
344 formatted
= command
.format(
345 time
=int(time
.time()),
346 hostname
=check
.hostname
,
349 servicename
=check
.servicename
,
352 if not os
.path
.exists(COMMAND_FILE
):
353 raise Exception(ERROR_BAD_COMMAND_FILE
)
354 if not os
.access(COMMAND_FILE
, os
.W_OK
):
355 raise Exception(ERROR_COMMAND_FILE_OPEN_WRITE
)
356 if not os
.access(COMMAND_FILE
, os
.W_OK
):
357 raise Exception(ERROR_COMMAND_FILE_OPEN_WRITE
)
359 with open(COMMAND_FILE
, "w") as c
:
360 c
.write(formatted
+ "\n")
361 except Exception as e
:
362 raise Exception(ERROR_BAD_WRITE
)
364 def render_error(error
):
370 def render_response(checks
, errors
):
382 "message": ERROR_NO_CORRECT_STATUS
,
386 def __init__(self
, hostname
, state
, output
, servicename
, checktype
):
387 self
.hostname
= hostname
390 self
.servicename
= servicename
391 self
.type = checktype
394 def from_json(klass
, j
):
395 if not isinstance(j
, dict):
397 for key
in ["hostname", "state", "output"]:
398 if key
not in j
or not isinstance(j
[key
], str):
400 for key
in ["servicename", "type"]:
401 if key
in j
and not isinstance(j
[key
], str):
407 j
.get("servicename", ""),
408 j
.get("type", "host"))