Compare commits

..

No commits in common. "main" and "kunsi-remove-hosts" have entirely different histories.

9 changed files with 124 additions and 238 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>

View file

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

View file

@ -1,163 +1,125 @@
#!/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 = {}
#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
class StatusPage:
def get_api_result(self):
if self.services:
log.debug('services already exist, returning early')
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']))
return self.services
headers = {'Accept': 'application/json', 'X-HTTP-Method-Override': 'GET'}
def render_services_per_host(host, data):
services_operational = ''
services_warning = ''
services_critical = ''
card_header = ''
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_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>{}</h4> <span class="badge badge-success">OK</span></div>"""
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,
)
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')
self.logger.info(f'got http status code {r.status_code}')
self.logger.debug(r.text)
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')
if r.status_code == 200:
self.services = r.json()['results']
else:
r.raise_for_status()
with open("services_template.html", "r") as f:
htmlTemplate = f.read()
self.logger.info(f'got {len(self.services)} services from api')
htmlOutput = htmlTemplate.format(
card_header = card_header,
services_operational = services_operational,
services_warning = services_warning,
services_critical = services_critical
)
return htmlOutput
return self.services
def prettify(self, text):
for search, replace in self.config.get('prettify', {}).items():
text = text.replace(search, replace)
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
return text
def get_services_per_host(self):
state_to_design_mapping = [
('success', 'OK'),
('warning', 'WARNING'),
('danger', 'CRITICAL'),
('info', 'UNKNOWN'),
]
result = {}
def render_index_html(filename, service_details):
with open("template.html", "r") as f:
htmlTemplate = f.read()
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)
htmlOutput = htmlTemplate.format(
services = service_details
)
host = service['joins']['host']['vars']['pretty_name']
with open(filename, "w") as f:
f.write(htmlOutput)
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__":
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,20 @@
<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>
</div>
</div>
</div>
% endfor
{services}
</div>
<script type="text/javascript">
window.setTimeout(function() {
window.location.reload();
}, 30000);
</script>
</body>
</html>