Compare commits

...

29 commits

Author SHA1 Message Date
Franzi 6e349ad35c
auto-reload status page after 30 seconds 2023-05-21 11:24:58 +02:00
Franzi 9f4f2818c5
can haz some formatting? 2023-05-20 20:09:16 +02:00
Franzi 0a0ee1bbfc
add some logging and error handling 2023-05-20 20:08:08 +02:00
Franzi fee4b8dac0
remove debug print 2021-04-11 11:17:48 +02:00
Franzi 9fec7340c7
add requests to requirements.txt 2021-04-11 11:13:42 +02:00
Franzi 077d9dfed1
Merge branch 'kunsi-improvements' 2021-04-11 11:13:00 +02:00
sophie 755b649197 Merge pull request 'kunsi-mako-templating' (#7) from kunsi-mako-templating into kunsi-improvements
Reviewed-on: https://git.kunsmann.eu/sophie/simple-icinga-dashboard/pulls/7
2021-04-11 08:59:36 +00:00
sophie 3ab0a05b59 Merge pull request 'Some improvements' (#6) from kunsi-improvements into main
Reviewed-on: https://git.kunsmann.eu/sophie/simple-icinga-dashboard/pulls/6
2021-04-11 08:59:27 +00:00
Franzi 676d7cabbc
rework templating using mako templates 2021-04-11 10:57:02 +02:00
Franzi 2ff0ca382f
make config file location configurable via environment variables 2021-04-11 10:26:09 +02:00
Franzi 48232c50cf
sort imports 2021-04-11 10:24:46 +02:00
Franzi b43b07a18c
add requirements.txt 2021-04-11 08:13:57 +02:00
Franzi a0a359d5dd
sort hosts by pretty name, not by internal hostname 2021-04-11 08:07:24 +02:00
Franzi 6b95291821
rename do_api_calls to get_api_results, return early if services were already requested 2021-04-11 08:00:36 +02:00
Franzi cb0f3bc13c
add editorconfig file 2021-04-11 07:56:55 +02:00
Sophie Schiller 6cde8347a7 fixed typo 2021-04-10 17:45:20 +02:00
Sophie Schiller a10c38e85d emojimood depending on ragecounter 2021-04-10 17:39:06 +02:00
Sophie Schiller 6d2279a51a updatereadme 2021-04-10 16:07:17 +02:00
Sophie Schiller 44293e9323 move config to toml file 2021-04-10 16:05:09 +02:00
Sophie Schiller f899e6e13b move stuff to class, add prettifier, use pretty names 2021-04-10 15:34:41 +02:00
Sophie Schiller e03a10abb6 set kunsi's favourite emoji 2021-01-03 09:46:05 +01:00
Sophie Schiller 67f17e1e12 add icon 2021-01-03 06:01:23 +01:00
Sophie Schiller 9e3394ab9d remove google font 2021-01-03 05:54:02 +01:00
Sophie Schiller 596bf8f682 set ids for intra-page-links 2021-01-02 14:55:04 +01:00
sophie 51840f17cd Merge pull request 'link hostnames, not service names' (#3) from kunsi-heading-links-fixup into main
Reviewed-on: https://git.kunsmann.eu/sophie/simple-icinga-dashboard/pulls/3
2021-01-02 13:48:30 +00:00
Franzi 50fd7db773
link hostnames, not service names 2021-01-02 14:44:26 +01:00
sophie 34aebe2809 Merge pull request 'kunsi-remove-hosts' (#1) from kunsi-remove-hosts into main
Reviewed-on: https://git.kunsmann.eu/sophie/simple-icinga-dashboard/pulls/1
2021-01-02 13:41:42 +00:00
sophie 666ab8f6a6 Merge pull request 'add linkable anchors to host names' (#2) from kunsi-heading-links into main
Reviewed-on: https://git.kunsmann.eu/sophie/simple-icinga-dashboard/pulls/2
2021-01-02 13:41:29 +00:00
Franzi f2b2aaa862
add linkable anchors to host names 2021-01-02 14:36:46 +01:00
9 changed files with 238 additions and 124 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.tolm]
indent_size = 2

1
.gitignore vendored
View file

@ -136,3 +136,4 @@ dmypy.json
index.html
config.conf
config.toml

View file

@ -2,17 +2,21 @@
## Config file
This script requires an ini-style config file named `config.conf` for icinga base url and credentials.
This script requires an toml config file named `config.toml` for icinga base url and credentials.
```
[icinga2_api]
baseurl = https://example.org:5665
username = root
password = foobar
baseurl = "https://127.0.0.1:5665"
username = "root"
password = "foobar"
[filters]
services = "checks_with_sms" in service.groups
hosts = "checks_with_sms" in host.groups
services = '"checks_with_sms" in service.groups'
[prettify]
NGINX = "Webserver"
CONTENT = ""
PROCESS = ""
[output]
filename = index.html
filename = "index.html"
```

2
bootstrap.min.css vendored

File diff suppressed because one or more lines are too long

41
error.html Normal file
View file

@ -0,0 +1,41 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>">
</head>
<body>
<div class="container">
<div class="page-header my-5" id="banner">
<div class="row">
<div class="col-lg-8">
<h1>Status: 🔥</h1>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card text-white border-primary mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h4>Something went wrong</h4>
</div>
<div class="card-body">
<p>
There was an error rendering the status page.
Admins have been notified.
</p>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 10000);
</script>
</body>
</html>

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
Mako
tomlkit
requests

View file

@ -1,125 +1,163 @@
#!/usr/bin/env python3
import json
import logging
import shutil
import sys
from datetime import datetime
from os import environ
import requests
import configparser
import tomlkit
import urllib3
from mako.template import Template
urllib3.disable_warnings()
def do_api_calls(config):
data = {}
#services
request_url = "{}/v1/objects/services".format(config['icinga2_api']['baseurl'])
headers = {
'Accept': 'application/json',
'X-HTTP-Method-Override': 'GET'
}
requestbody = {
"attrs": [ "name", "state", "last_check_result", "host_name", "display_name" ],
"joins": [ "host.name", "host.state", "host.last_check_result" ],
"filter": config['filters']['services'],
}
r = requests.get(request_url,
headers=headers,
data=json.dumps(requestbody),
auth=(config['icinga2_api']['username'], config['icinga2_api']['password']),
verify=False)
if (r.status_code == 200):
data['services'] = r.json()
else:
r.raise_for_status()
return data
CONFIGFILE = environ.get('STATUSPAGE_CONFIG', 'config.toml')
def render_text_output(data):
print("{:50s} {:10s}".format("host", "status"))
for host in data['hosts']['results']:
print("{:50s} {}".format(host['name'], host['attrs']['state']))
for service in data['services']['results']:
print("{:50s} {}".format(service['name'], service['attrs']['state']))
class StatusPage:
def get_api_result(self):
if self.services:
log.debug('services already exist, returning early')
return self.services
def render_services_per_host(host, data):
services_operational = ''
services_warning = ''
services_critical = ''
card_header = ''
headers = {'Accept': 'application/json', 'X-HTTP-Method-Override': 'GET'}
services_template = """
<li class="list-group-item d-flex justify-content-between align-items-center">
{}
<span class="badge badge-{}">{}</span>
</li>
"""
services_hostname_template = """<div class="card-header d-flex justify-content-between align-items-center"><h4>{}</h4> <span class="badge badge-success">OK</span></div>"""
requestbody = {
"attrs": [
"name",
"state",
"last_check_result",
"host_name",
"display_name",
],
"joins": ["host", "host.state", "host.last_check_result", "host.vars"],
"filter": self.config['filters']['services'],
}
for service in sorted(data['services']['results'], key=lambda x: x['attrs']['display_name']):
if service['attrs']['host_name'] == host:
if service['attrs']['state'] == 0:
services_operational = services_operational + services_template.format(service['attrs']['display_name'], 'success', 'OK')
elif service['attrs']['state'] == 1:
services_warning = services_warning + services_template.format(service['attrs']['display_name'], 'warning', 'WARNING')
else:
services_critical = services_critical + services_template.format(service['attrs']['display_name'], 'danger', 'CRITICAL')
r = requests.get(
'{}/v1/objects/services'.format(self.config['icinga2_api']['baseurl']),
headers=headers,
json=requestbody,
auth=(
self.config['icinga2_api']['username'],
self.config['icinga2_api']['password'],
),
verify=False,
)
if service['joins']['host']['state'] == 0:
card_header = services_hostname_template.format(host, 'success', 'UP')
else:
card_header = services_hostname_template.format(host, 'danger', 'DOWN')
self.logger.info(f'got http status code {r.status_code}')
self.logger.debug(r.text)
with open("services_template.html", "r") as f:
htmlTemplate = f.read()
if r.status_code == 200:
self.services = r.json()['results']
else:
r.raise_for_status()
htmlOutput = htmlTemplate.format(
card_header = card_header,
services_operational = services_operational,
services_warning = services_warning,
services_critical = services_critical
)
return htmlOutput
self.logger.info(f'got {len(self.services)} services from api')
return self.services
def render_service_details(data):
# generate list of hosts by scanning services for unique host_name
host_names = []
for service in data['services']['results']:
if service['attrs']['host_name'] not in host_names:
host_names.append(service['attrs']['host_name'])
# render html for each host_name
html_output = ""
for host in sorted(host_names):
html_output = html_output + render_services_per_host(host, data)
return html_output
def prettify(self, text):
for search, replace in self.config.get('prettify', {}).items():
text = text.replace(search, replace)
return text
def render_index_html(filename, service_details):
with open("template.html", "r") as f:
htmlTemplate = f.read()
def get_services_per_host(self):
state_to_design_mapping = [
('success', 'OK'),
('warning', 'WARNING'),
('danger', 'CRITICAL'),
('info', 'UNKNOWN'),
]
result = {}
htmlOutput = htmlTemplate.format(
services = service_details
)
for service in self.get_api_result():
self.logger.info(
f'now processing {service["attrs"]["host_name"]} "{service["attrs"]["display_name"]}"'
)
self.logger.debug(service)
with open(filename, "w") as f:
f.write(htmlOutput)
host = service['joins']['host']['vars']['pretty_name']
if host not in result:
result[host] = {
'hostname': service['attrs']['host_name'],
'services': {},
}
if service['joins']['host']['state'] == 0:
result[host]['host_badge'] = 'success'
result[host]['host_state'] = 'UP'
else:
result[host]['host_badge'] = 'danger'
result[host]['host_state'] = 'DOWN'
self.ragecounter += 10
state = int(service['attrs']['state'])
if state in (1, 2):
self.ragecounter += state
result[host]['services'][
self.prettify(service['attrs']['display_name'])
] = {
'badge': state_to_design_mapping[state][0],
'state': state_to_design_mapping[state][1],
}
self.logger.info(f'ragecounter is now {self.ragecounter}')
return result
def render_html(self, service_details):
if self.ragecounter == 0:
mood = '🆗'
elif self.ragecounter < 10:
mood = '🚨'
else:
mood = '🔥'
self.logger.info('rendering output html')
start = datetime.now()
template = Template(
filename=self.config['output'].get('template', 'template.html')
)
output = template.render(
title=self.config['output'].get('page_title', 'Status Page'),
mood=mood,
hosts=service_details,
)
end = datetime.now()
self.logger.info(f'rendered in {(end-start).total_seconds():.09f}s')
with open(self.config['output']['filename'], 'w') as f:
f.write(output)
def __init__(self):
self.config = tomlkit.loads(open(CONFIGFILE).read())
self.services = {}
self.ragecounter = 0
self.logger = logging.getLogger('StatusPage')
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
'%(levelname)s {%(filename)s:%(lineno)d} %(message)s'
)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(self.config.get('loglevel', 'INFO'))
def main():
config = configparser.ConfigParser()
config['icinga2_api'] = {
'baseurl': 'https://localhost:5665',
'username': 'root',
'password': 'foobar'
}
with open('config.conf', 'r') as configfile:
config.read('config.conf')
data = do_api_calls(config)
service_details = render_service_details(data)
render_index_html(config['output']['filename'], service_details)
if __name__ == "__main__":
main()
page = StatusPage()
try:
service_details = page.get_services_per_host()
page.render_html(service_details)
except Exception as e:
shutil.copyfile('error.html', page.config['output']['filename'])
raise e

View file

@ -1,12 +0,0 @@
<div class="row">
<div class="col">
<div class="card text-white border-primary mb-3">
{card_header}
<div class="card-body">
<ul class="list-group">{services_critical}</ul>
<ul class="list-group">{services_warning}</ul>
<ul class="list-group">{services_operational}</ul>
</div>
</div>
</div>
</div>

View file

@ -2,20 +2,47 @@
<html>
<head>
<meta charset="utf-8">
<title>Status Page</title>
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bootstrap.min.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${mood}</text></svg>">
</head>
<body>
<div class="container">
<div class="page-header my-5" id="banner">
<div class="row">
<div class="col-lg-8 col-md-7 col-sm-6">
<h1>Status Page</h1>
<div class="col-lg-8">
<h1>Status: ${mood}</h1>
</div>
</div>
</div>
{services}
% for prettyname, details in sorted(hosts.items()):
<div class="row">
<div class="col">
<div class="card text-white border-primary mb-3">
<div id="${details['hostname']}" class="card-header d-flex justify-content-between align-items-center">
<h4><a href="#${details['hostname']}">${prettyname}</a></h4>
<span class="badge badge-${details['host_badge']}">${details['host_state']}</span>
</div>
<div class="card-body">
<ul class="list-group">
% for service_name, service_details in sorted(details['services'].items()):
<li class="list-group-item d-flex justify-content-between align-items-center">
${service_name}
<span class="badge badge-${service_details['badge']}">${service_details['state']}</span>
</li>
% endfor
</ul>
</div>
</div>
</div>
</div>
% endfor
</div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 30000);
</script>
</body>
</html>