Compare commits

..

No commits in common. "main" and "kunsi-heading-links" have entirely different histories.

10 changed files with 229 additions and 239 deletions

View file

@ -1,12 +0,0 @@
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,4 +136,3 @@ dmypy.json
index.html
config.conf
config.toml

View file

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

2
bootstrap.min.css vendored

File diff suppressed because one or more lines are too long

View file

@ -1,41 +0,0 @@
<!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>

26
hosts_template.html Normal file
View file

@ -0,0 +1,26 @@
<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>

View file

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

View file

@ -1,163 +1,188 @@
#!/usr/bin/env python3
import logging
import shutil
import sys
from datetime import datetime
from os import environ
import json
import requests
import tomlkit
import configparser
import urllib3
from mako.template import Template
urllib3.disable_warnings()
CONFIGFILE = environ.get('STATUSPAGE_CONFIG', 'config.toml')
def do_api_calls(config):
data = {}
class StatusPage:
def get_api_result(self):
if self.services:
log.debug('services already exist, returning early')
return self.services
headers = {'Accept': 'application/json', 'X-HTTP-Method-Override': 'GET'}
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'],
#services
request_url = "{}/v1/objects/services".format(config['icinga2_api']['baseurl'])
headers = {
'Accept': 'application/json',
'X-HTTP-Method-Override': 'GET'
}
r = requests.get(
'{}/v1/objects/services'.format(self.config['icinga2_api']['baseurl']),
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,
json=requestbody,
auth=(
self.config['icinga2_api']['username'],
self.config['icinga2_api']['password'],
),
verify=False,
)
data=json.dumps(requestbody),
auth=(config['icinga2_api']['username'], 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']
if (r.status_code == 200):
data['services'] = r.json()
else:
r.raise_for_status()
self.logger.info(f'got {len(self.services)} services from api')
return self.services
def prettify(self, text):
for search, replace in self.config.get('prettify', {}).items():
text = text.replace(search, replace)
return text
def get_services_per_host(self):
state_to_design_mapping = [
('success', 'OK'),
('warning', 'WARNING'),
('danger', 'CRITICAL'),
('info', 'UNKNOWN'),
]
result = {}
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)
host = service['joins']['host']['vars']['pretty_name']
if host not in result:
result[host] = {
'hostname': service['attrs']['host_name'],
'services': {},
# 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):
hosts_operational = ''
hosts_warning = ''
hosts_critical = ''
hosts_operational_template = """
<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']:
if host['attrs']['state'] == 0:
hosts_operational = hosts_operational + hosts_operational_template.format(host['name'])
elif host['attrs']['state'] == 1:
hosts_warning = hosts_warning + hosts_critical_template.format(host['name'])
else:
hosts_critical = hosts_critical + hosts_critical_template.format(host['name'])
with open("hosts_template.html", "r") as f:
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):
services_operational = ''
services_warning = ''
services_critical = ''
card_header = ''
services_template = """
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="#{0}">{0}</a>
<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']):
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:
result[host]['host_badge'] = 'success'
result[host]['host_state'] = 'UP'
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:
result[host]['host_badge'] = 'danger'
result[host]['host_state'] = 'DOWN'
self.ragecounter += 10
card_header = services_hostname_template.format(host, 'danger', 'DOWN')
state = int(service['attrs']['state'])
with open("services_template.html", "r") as f:
htmlTemplate = f.read()
if state in (1, 2):
self.ragecounter += state
htmlOutput = htmlTemplate.format(
card_header = card_header,
services_operational = services_operational,
services_warning = services_warning,
services_critical = services_critical
)
return htmlOutput
result[host]['services'][
self.prettify(service['attrs']['display_name'])
] = {
'badge': state_to_design_mapping[state][0],
'state': state_to_design_mapping[state][1],
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 render_index_html(filename, host_summary, service_details):
with open("template.html", "r") as f:
htmlTemplate = f.read()
htmlOutput = htmlTemplate.format(
hosts = host_summary,
services = service_details
)
with open(filename, "w") as f:
f.write(htmlOutput)
def main():
config = configparser.ConfigParser()
config['icinga2_api'] = {
'baseurl': 'https://localhost:5665',
'username': 'root',
'password': 'foobar'
}
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'))
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__":
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
main()

12
services_template.html Normal file
View file

@ -0,0 +1,12 @@
<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,47 +2,35 @@
<html>
<head>
<meta charset="utf-8">
<title>${title}</title>
<title>Status Page</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">
<h1>Status: ${mood}</h1>
<div class="col-lg-8 col-md-7 col-sm-6">
<h1>Status Page</h1>
</div>
</div>
</div>
% 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 class="col-lg-12">
<div class="page-header">
<h2 id="typography">Hosts</h2>
</div>
</div>
</div>
{hosts}
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2 id="typography">Services</h2>
</div>
% endfor
</div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 30000);
</script>
</div>
{services}
</div>
</body>
</html>