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 index.html
config.conf config.conf
config.toml

View file

@ -2,21 +2,17 @@
## Config file ## 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] [icinga2_api]
baseurl = "https://127.0.0.1:5665" baseurl = https://example.org: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

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 #!/usr/bin/env python3
import logging import json
import shutil
import sys
from datetime import datetime
from os import environ
import requests import requests
import tomlkit import configparser
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 = {}
class StatusPage: #services
def get_api_result(self): request_url = "{}/v1/objects/services".format(config['icinga2_api']['baseurl'])
if self.services: headers = {
log.debug('services already exist, returning early') 'Accept': 'application/json',
'X-HTTP-Method-Override': 'GET'
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'],
} }
requestbody = {
r = requests.get( "attrs": [ "name", "state", "last_check_result", "host_name", "display_name" ],
'{}/v1/objects/services'.format(self.config['icinga2_api']['baseurl']), "joins": [ "host.name", "host.state", "host.last_check_result" ],
"filter": config['filters']['services'],
}
r = requests.get(request_url,
headers=headers, headers=headers,
json=requestbody, data=json.dumps(requestbody),
auth=( auth=(config['icinga2_api']['username'], config['icinga2_api']['password']),
self.config['icinga2_api']['username'], verify=False)
self.config['icinga2_api']['password'],
),
verify=False,
)
self.logger.info(f'got http status code {r.status_code}') if (r.status_code == 200):
self.logger.debug(r.text) data['services'] = r.json()
if r.status_code == 200:
self.services = r.json()['results']
else: else:
r.raise_for_status() r.raise_for_status()
self.logger.info(f'got {len(self.services)} services from api') # hosts
request_url = "{}/v1/objects/hosts".format(config['icinga2_api']['baseurl'])
return self.services headers = {
'Accept': 'application/json',
def prettify(self, text): 'X-HTTP-Method-Override': 'GET'
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': {},
} }
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: if service['joins']['host']['state'] == 0:
result[host]['host_badge'] = 'success' card_header = services_hostname_template.format(host, 'success', 'UP')
result[host]['host_state'] = 'UP' elif service['joins']['host']['state'] == 0:
card_header = services_hostname_template.format(host, 'warning', 'WARNING')
else: else:
result[host]['host_badge'] = 'danger' card_header = services_hostname_template.format(host, 'danger', 'DOWN')
result[host]['host_state'] = 'DOWN'
self.ragecounter += 10
state = int(service['attrs']['state']) with open("services_template.html", "r") as f:
htmlTemplate = f.read()
if state in (1, 2): htmlOutput = htmlTemplate.format(
self.ragecounter += state card_header = card_header,
services_operational = services_operational,
services_warning = services_warning,
services_critical = services_critical
)
return htmlOutput
result[host]['services'][ def render_service_details(data):
self.prettify(service['attrs']['display_name']) # generate list of hosts by scanning services for unique host_name
] = { host_names = []
'badge': state_to_design_mapping[state][0], for service in data['services']['results']:
'state': state_to_design_mapping[state][1], 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}') with open('config.conf', 'r') as configfile:
config.read('config.conf')
return result data = do_api_calls(config)
host_summary = render_hosts(data)
def render_html(self, service_details): service_details = render_service_details(data)
if self.ragecounter == 0: render_index_html(config['output']['filename'], host_summary, service_details)
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'))
if __name__ == "__main__": if __name__ == "__main__":
page = StatusPage() main()
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

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> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>${title}</title> <title>Status Page</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"> <div class="col-lg-8 col-md-7 col-sm-6">
<h1>Status: ${mood}</h1> <h1>Status Page</h1>
</div> </div>
</div> </div>
</div> </div>
% for prettyname, details in sorted(hosts.items()):
<div class="row"> <div class="row">
<div class="col"> <div class="col-lg-12">
<div class="card text-white border-primary mb-3"> <div class="page-header">
<div id="${details['hostname']}" class="card-header d-flex justify-content-between align-items-center"> <h2 id="typography">Hosts</h2>
<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>
{hosts}
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2 id="typography">Services</h2>
</div> </div>
% endfor
</div> </div>
<script type="text/javascript"> </div>
window.setTimeout(function() { {services}
window.location.reload(); </div>
}, 30000);
</script>
</body> </body>
</html> </html>