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 = {}
#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']))
class StatusPage: def render_hosts(data):
def get_api_result(self): hosts_operational = ''
if self.services: hosts_warning = ''
log.debug('services already exist, returning early') hosts_critical = ''
return self.services 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>
"""
headers = {'Accept': 'application/json', 'X-HTTP-Method-Override': 'GET'} for host in data['hosts']['results']:
if host['attrs']['state'] == 0:
requestbody = { hosts_operational = hosts_operational + hosts_operational_template.format(host['name'])
"attrs": [ elif host['attrs']['state'] == 1:
"name", hosts_warning = hosts_warning + hosts_critical_template.format(host['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:
r.raise_for_status() hosts_critical = hosts_critical + hosts_critical_template.format(host['name'])
self.logger.info(f'got {len(self.services)} services from api') 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
return self.services def render_services_per_host(host, data):
services_operational = ''
services_warning = ''
services_critical = ''
card_header = ''
def prettify(self, text): services_template = """
for search, replace in self.config.get('prettify', {}).items(): <li class="list-group-item d-flex justify-content-between align-items-center">
text = text.replace(search, replace) <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>"""
return text 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')
def get_services_per_host(self): if service['joins']['host']['state'] == 0:
state_to_design_mapping = [ card_header = services_hostname_template.format(host, 'success', 'UP')
('success', 'OK'), elif service['joins']['host']['state'] == 0:
('warning', 'WARNING'), card_header = services_hostname_template.format(host, 'warning', 'WARNING')
('danger', 'CRITICAL'), else:
('info', 'UNKNOWN'), card_header = services_hostname_template.format(host, 'danger', 'DOWN')
]
result = {}
for service in self.get_api_result(): with open("services_template.html", "r") as f:
self.logger.info( htmlTemplate = f.read()
f'now processing {service["attrs"]["host_name"]} "{service["attrs"]["display_name"]}"'
)
self.logger.debug(service)
host = service['joins']['host']['vars']['pretty_name'] htmlOutput = htmlTemplate.format(
card_header = card_header,
services_operational = services_operational,
services_warning = services_warning,
services_critical = services_critical
)
return htmlOutput
if host not in result: def render_service_details(data):
result[host] = { # generate list of hosts by scanning services for unique host_name
'hostname': service['attrs']['host_name'], host_names = []
'services': {}, for service in data['services']['results']:
} if service['attrs']['host_name'] not in host_names:
if service['joins']['host']['state'] == 0: host_names.append(service['attrs']['host_name'])
result[host]['host_badge'] = 'success' # render html for each host_name
result[host]['host_state'] = 'UP' html_output = ""
else: for host in sorted(host_names):
result[host]['host_badge'] = 'danger' html_output = html_output + render_services_per_host(host, data)
result[host]['host_state'] = 'DOWN' return html_output
self.ragecounter += 10
state = int(service['attrs']['state']) def render_index_html(filename, host_summary, service_details):
with open("template.html", "r") as f:
htmlTemplate = f.read()
if state in (1, 2): htmlOutput = htmlTemplate.format(
self.ragecounter += state hosts = host_summary,
services = service_details
)
result[host]['services'][ with open(filename, "w") as f:
self.prettify(service['attrs']['display_name']) f.write(htmlOutput)
] = {
'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__":
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> </div>
% endfor {hosts}
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2 id="typography">Services</h2>
</div>
</div>
</div>
{services}
</div> </div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 30000);
</script>
</body> </body>
</html> </html>