Compare commits

..

31 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
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
93869e042b
remove unneeded call to api/objects/hosts 2021-01-02 14:40:05 +01:00
0e5209a725
remove unneeded heading from template.html 2021-01-02 14:34:07 +01:00
d7910e5fd3
remove host list from top of page
This is duplicated in the services list
2021-01-02 14:33:40 +01:00
10 changed files with 236 additions and 226 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 index.html
config.conf config.conf
config.toml

View file

@ -2,17 +2,21 @@
## Config file ## 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] [icinga2_api]
baseurl = https://example.org:5665 baseurl = "https://127.0.0.1:5665"
username = root username = "root"
password = foobar password = "foobar"
[filters] [filters]
services = "checks_with_sms" in service.groups services = '"checks_with_sms" in service.groups'
hosts = "checks_with_sms" in host.groups
[prettify]
NGINX = "Webserver"
CONTENT = ""
PROCESS = ""
[output] [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>

View file

@ -1,26 +0,0 @@
<div class="row">
<div class="col">
<div class="card text-white border-success mb-3" style="max-width: 20rem;">
<h5 class="card-header">Operational</h5>
<div class="card-body">
{hosts_operational}
</div>
</div>
</div>
<div class="col">
<div class="card text-white border-warning mb-3" style="max-width: 20rem;">
<h5 class="card-header">Warning</h5>
<div class="card-body">
{hosts_warning}
</div>
</div>
</div>
<div class="col">
<div class="card text-white border-danger mb-3" style="max-width: 20rem;">
<h5 class="card-header">Critical</h5>
<div class="card-body">
{hosts_critical}
</div>
</div>
</div>
</div>

3
requirements.txt Normal file
View file

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

View file

@ -1,188 +1,163 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json import logging
import shutil
import sys
from datetime import datetime
from os import environ
import requests import requests
import configparser import tomlkit
import urllib3 import urllib3
from mako.template import Template
urllib3.disable_warnings() urllib3.disable_warnings()
CONFIGFILE = environ.get('STATUSPAGE_CONFIG', 'config.toml')
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()
# hosts
request_url = "{}/v1/objects/hosts".format(config['icinga2_api']['baseurl'])
headers = {
'Accept': 'application/json',
'X-HTTP-Method-Override': 'GET'
}
requestbody = {
"attrs": [ "name", "state" ],
"filter": config['filters']['hosts'],
}
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['hosts'] = r.json()
else:
r.raise_for_status()
return data
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']))
def render_hosts(data): class StatusPage:
hosts_operational = '' def get_api_result(self):
hosts_warning = '' if self.services:
hosts_critical = '' log.debug('services already exist, returning early')
hosts_operational_template = """ return self.services
<li class="list-group-item d-flex justify-content-between align-items-center">
{}
<span class="badge badge-success">OK</span>
</li>
"""
hosts_warning_template = """
<li class="list-group-item d-flex justify-content-between align-items-center">
{}
<span class="badge badge-warning">WARNING</span>
</li>
"""
hosts_critical_template = """
<li class="list-group-item d-flex justify-content-between align-items-center">
{}
<span class="badge badge-danger">CRITICAL</span>
</li>
"""
for host in data['hosts']['results']: headers = {'Accept': 'application/json', 'X-HTTP-Method-Override': 'GET'}
if host['attrs']['state'] == 0:
hosts_operational = hosts_operational + hosts_operational_template.format(host['name']) requestbody = {
elif host['attrs']['state'] == 1: "attrs": [
hosts_warning = hosts_warning + hosts_critical_template.format(host['name']) "name",
"state",
"last_check_result",
"host_name",
"display_name",
],
"joins": ["host", "host.state", "host.last_check_result", "host.vars"],
"filter": self.config['filters']['services'],
}
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,
)
self.logger.info(f'got http status code {r.status_code}')
self.logger.debug(r.text)
if r.status_code == 200:
self.services = r.json()['results']
else: else:
hosts_critical = hosts_critical + hosts_critical_template.format(host['name']) r.raise_for_status()
with open("hosts_template.html", "r") as f: self.logger.info(f'got {len(self.services)} services from api')
htmlTemplate = f.read()
htmlOutput = htmlTemplate.format(
hosts_operational = hosts_operational,
hosts_warning = hosts_warning,
hosts_critical = hosts_critical,
)
return htmlOutput
def render_services_per_host(host, data): return self.services
services_operational = ''
services_warning = ''
services_critical = ''
card_header = ''
services_template = """ def prettify(self, text):
<li class="list-group-item d-flex justify-content-between align-items-center"> for search, replace in self.config.get('prettify', {}).items():
<a href="#{0}">{0}</a> text = text.replace(search, replace)
<span class="badge badge-{1}">{2}</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>"""
for service in sorted(data['services']['results'], key=lambda x: x['attrs']['display_name']): return text
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')
if service['joins']['host']['state'] == 0:
card_header = services_hostname_template.format(host, 'success', 'UP')
elif service['joins']['host']['state'] == 0:
card_header = services_hostname_template.format(host, 'warning', 'WARNING')
else:
card_header = services_hostname_template.format(host, 'danger', 'DOWN')
with open("services_template.html", "r") as f: def get_services_per_host(self):
htmlTemplate = f.read() state_to_design_mapping = [
('success', 'OK'),
('warning', 'WARNING'),
('danger', 'CRITICAL'),
('info', 'UNKNOWN'),
]
result = {}
htmlOutput = htmlTemplate.format( for service in self.get_api_result():
card_header = card_header, self.logger.info(
services_operational = services_operational, f'now processing {service["attrs"]["host_name"]} "{service["attrs"]["display_name"]}"'
services_warning = services_warning, )
services_critical = services_critical self.logger.debug(service)
)
return htmlOutput
def render_service_details(data): host = service['joins']['host']['vars']['pretty_name']
# 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 render_index_html(filename, host_summary, service_details): if host not in result:
with open("template.html", "r") as f: result[host] = {
htmlTemplate = f.read() '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
htmlOutput = htmlTemplate.format( state = int(service['attrs']['state'])
hosts = host_summary,
services = service_details
)
with open(filename, "w") as f: if state in (1, 2):
f.write(htmlOutput) 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)
host_summary = render_hosts(data)
service_details = render_service_details(data)
render_index_html(config['output']['filename'], host_summary, service_details)
if __name__ == "__main__": 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,35 +2,47 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Status Page</title> <title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="bootstrap.min.css"> <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> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="page-header my-5" id="banner"> <div class="page-header my-5" id="banner">
<div class="row"> <div class="row">
<div class="col-lg-8 col-md-7 col-sm-6"> <div class="col-lg-8">
<h1>Status Page</h1> <h1>Status: ${mood}</h1>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2 id="typography">Hosts</h2>
</div> </div>
</div> </div>
</div> </div>
{hosts} % for prettyname, details in sorted(hosts.items()):
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col">
<div class="page-header"> <div class="card text-white border-primary mb-3">
<h2 id="typography">Services</h2> <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> </div>
</div> </div>
{services} % endfor
</div> </div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 30000);
</script>
</body> </body>
</html> </html>