Compare commits

..

25 commits

Author SHA1 Message Date
6e349ad35c
auto-reload status page after 30 seconds 2023-05-21 11:24:58 +02:00
9f4f2818c5
can haz some formatting? 2023-05-20 20:09:16 +02:00
0a0ee1bbfc
add some logging and error handling 2023-05-20 20:08:08 +02:00
fee4b8dac0
remove debug print 2021-04-11 11:17:48 +02:00
9fec7340c7
add requests to requirements.txt 2021-04-11 11:13:42 +02:00
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
676d7cabbc
rework templating using mako templates 2021-04-11 10:57:02 +02:00
2ff0ca382f
make config file location configurable via environment variables 2021-04-11 10:26:09 +02:00
48232c50cf
sort imports 2021-04-11 10:24:46 +02:00
b43b07a18c
add requirements.txt 2021-04-11 08:13:57 +02:00
a0a359d5dd
sort hosts by pretty name, not by internal hostname 2021-04-11 08:07:24 +02:00
6b95291821
rename do_api_calls to get_api_results, return early if services were already requested 2021-04-11 08:00:36 +02:00
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
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><a href="#{0}">{0}</a></h4> <span class="badge badge-{1}">{2}</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>