commit 79cd2f1a39d0bec3f9bea03e2dfa17edff3879ad Author: Franziska Kunsmann Date: Sun Sep 10 09:14:33 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f32296 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build/ +customer_statuspage.egg-info/ +dist/ +statuspage/__pycache__/ +venv/ +config.json +*.swp +*.swo diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..630d4b8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask +psycopg2-binary diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dbd83eb --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +from setuptools import find_packages, setup + +setup( + name='customer-statuspage', + packages=find_packages(), +) diff --git a/statuspage/__init__.py b/statuspage/__init__.py new file mode 100644 index 0000000..116ff31 --- /dev/null +++ b/statuspage/__init__.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +import json +import logging +from datetime import datetime, timedelta +from os import environ +from re import sub + +from flask import Flask, abort, jsonify, render_template, request +from psycopg2.pool import ThreadedConnectionPool + +app = Flask(__name__) +app.config.from_file(environ['APP_CONFIG'], json.load) + +if not app.debug: + log = logging.StreamHandler() + log.setLevel(logging.INFO) + app.logger.addHandler(log) + + +pg_pool = ThreadedConnectionPool( + minconn=1, + maxconn=20, + user=app.config['DB_USER'], + password=app.config['DB_PASS'], + database=app.config['DB_NAME'], + host='localhost', +) + + +def check_freshness(cur): + cur.execute('select status_update_time from icinga_programstatus;') + if datetime.utcnow() - cur.fetchone()[0] > timedelta(minutes=5): + abort(503) + + +def get_nodename(identifier): + if identifier in HOSTS: + return HOSTS[identifier] + abort(404) + + +@app.route('/status.json') +def services_as_json(): + conn = pg_pool.getconn() + results = [] + + try: + cur = conn.cursor() + + check_freshness(cur) + + cur.execute( + ''' + select + objs.name1, + objs.name2, + icinga_servicestatus.current_state, + icinga_servicestatus.scheduled_downtime_depth, + icinga_servicestatus.state_type, + icinga_servicestatus.output, + icinga_servicestatus.problem_has_been_acknowledged, + icinga_servicestatus.status_update_time, + icinga_servicestatus.last_state_change, + ( + select vars.varvalue + from icinga_customvariables vars, icinga_services + where objs.object_id = icinga_services.service_object_id + and icinga_services.host_object_id = vars.object_id + and vars.varname = 'pretty_name' + ) + from icinga_objects objs + left join icinga_servicestatus on objs.object_id = icinga_servicestatus.service_object_id + left join icinga_servicegroup_members sgrm on objs.object_id = sgrm.service_object_id + where + objs.objecttype_id = 2 and + objs.is_active = 1 and + sgrm.servicegroup_id = %s + ;''', + (app.config['SERVICEGROUP_ID'],), + ) + + for ( + host_name, + service_name, + state, + downtime_depth, + state_type, + output, + acked, + update_time, + last_state_change, + pretty_name, + ) in cur.fetchall(): + if last_state_change is None: + last_state_change = update_time + + for regex, replacement in app.config.get('NAME_REPLACEMENTS', {}).items(): + service_name = sub(regex, replacement, service_name) + + results.append( + { + 'type': 'Service', + 'attrs': { + 'acknowledgement': acked, + 'display_name': service_name, + 'downtime_depth': downtime_depth, + 'host_name': pretty_name, + 'last_check': update_time.timestamp(), + 'last_state_change': last_state_change.timestamp(), + 'state': state, + 'state_type': state_type, + 'last_check_result': { + 'output': output, + }, + '__custom': { + 'last_check': update_time.strftime('%Y-%m-%d %H:%M:%S UTC'), + 'last_state_change': last_state_change.strftime( + '%Y-%m-%d %H:%M:%S UTC' + ), + }, + }, + } + ) + + cur.close() + finally: + pg_pool.putconn(conn) + + return jsonify({'results': results}) + + +@app.route('/') +def statuspage(): + return render_template('statuspage.html') + + +if __name__ == '__main__': + app.run(host='::') diff --git a/statuspage/static/OpenDyslexic-LICENSE.txt b/statuspage/static/OpenDyslexic-LICENSE.txt new file mode 100644 index 0000000..bb86782 --- /dev/null +++ b/statuspage/static/OpenDyslexic-LICENSE.txt @@ -0,0 +1,94 @@ +Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es), +with Reserved Font Name OpenDyslexic. +Copyright (c) 12/2012 - 2019 +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/statuspage/static/OpenDyslexic-Regular.woff2 b/statuspage/static/OpenDyslexic-Regular.woff2 new file mode 100644 index 0000000..47e26d8 Binary files /dev/null and b/statuspage/static/OpenDyslexic-Regular.woff2 differ diff --git a/statuspage/static/UbuntuMono-LICENSE.txt b/statuspage/static/UbuntuMono-LICENSE.txt new file mode 100644 index 0000000..f6f6ad4 Binary files /dev/null and b/statuspage/static/UbuntuMono-LICENSE.txt differ diff --git a/statuspage/static/UbuntuMono-Regular.ttf b/statuspage/static/UbuntuMono-Regular.ttf new file mode 100644 index 0000000..c8add8e Binary files /dev/null and b/statuspage/static/UbuntuMono-Regular.ttf differ diff --git a/statuspage/static/scripts.js b/statuspage/static/scripts.js new file mode 100644 index 0000000..f40bb3e --- /dev/null +++ b/statuspage/static/scripts.js @@ -0,0 +1,95 @@ +function render_status_page() { + console.info('updating status page'); + + req = new XMLHttpRequest(); + req.open('GET', '/status.json'); + req.setRequestHeader('Accept', 'application/json'); + + req.addEventListener('load', function(event) { + result = JSON.parse(req.responseText)['results']; + + result.sort(function(a, b) { + aname = a['attrs']['host_name'] + a['attrs']['display_name']; + bname = b['attrs']['host_name'] + b['attrs']['display_name']; + + if (aname < bname) return -1; + if (aname > bname) return 1; + return 0; + }); + + out = ''; + last_hostname = ''; + + result.forEach(function(item) { + if (last_hostname != item['attrs']['host_name']) { + out += '

' + item['attrs']['host_name'] + '

'; + last_hostname = item['attrs']['host_name'] + } + + if (item['attrs']['state'] == 0) { + out += '
'; + } else { + out += '
'; + } + + out += '

'; + out += downtime_or_ack_to_string(item['attrs']['downtime_depth'], item['attrs']['acknowledgement']) + ' '; + out += state_to_string(item['attrs']['state'], item['attrs']['state_type']) + ' '; + out += escape_html(item['attrs']['display_name']); + out += '

'; + + out += '
' + escape_html(item['attrs']['last_check_result']['output']) + '
'; + + out += '

'; + out += 'Last checked: ' + item['attrs']['__custom']['last_check'] + '
'; + out += 'Last state change: ' + item['attrs']['__custom']['last_state_change']; + out += '

'; + + out += '
'; + }); + + if (out.length == 0) { + out += '
'; + } + + document.getElementById('output').innerHTML = out; + + console.info('updated status page'); + }); + + req.send(); +} + +function state_to_string(state, type) { + if (type != 1) { + maybe = ' (pending)'; + } else { + maybe = ''; + } + + if (state == 0) { + return 'OK' + maybe + ''; + } else if (state == 1) { + return 'WARNING' + maybe + ''; + } else if (state == 2) { + return 'CRITICAL' + maybe + ''; + } else { + return 'UNKNOWN' + maybe + ''; + } +} + +function downtime_or_ack_to_string(dt, ack) { + if ((dt + ack) > 0) { + return 'Work in Progress'; + } + return ''; +} + +function escape_html(text) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/statuspage/static/style.css b/statuspage/static/style.css new file mode 100644 index 0000000..f503a55 --- /dev/null +++ b/statuspage/static/style.css @@ -0,0 +1,132 @@ +:root { + --text-color: #333333; + --bg-root-color: #FFFFFF; + --line-color: #CCCCCC; +} + +@media (prefers-color-scheme: dark) { + :root { + --text-color: #FFFFFF; + --bg-root-color: #222222; + --line-color: #444444; + } +} + +@font-face { + font-family: "Ubuntu Mono"; + src: url(UbuntuMono-Regular.ttf); +} + +@font-face { + font-family: "OpenDyslexic"; + src: url(OpenDyslexic-Regular.woff2); +} + +* { + font-family: "OpenDyslexic",sans-serif; + font-size: 1em; + color: var(--text-color); + margin: 0; + padding: 0; +} + +html, body { + background-color: var(--bg-root-color); +} + +body { + max-width: 1000px; + margin: 20px auto; +} + +h1 { + display: none; +} + +h2 { + margin-top: 30px; + vertical-align: middle; + line-height: 1.3em; + font-size: 2em; + text-align: center; +} + +.service { + margin: 2px; + padding: 10px; + border: 1px solid var(--line-color); + border-radius: 5px; +} + +.service_ok { + cursor: pointer; +} + +.service_ok .output, .service_ok .statusline { + display: none; +} + +.service_ok:hover .output, .service_ok:hover .statusline { + display: block; +} + +.service h3 { + font-weight: normal; + line-height: 1em; +} + +.output { + font-family: "Ubuntu Mono", monospace; + font-size: 1.2em; + margin: 10px; + white-space: pre-wrap; +} + +.statusline { + margin-top: 10px; + text-align: right; +} + +.status_ok, +.status_warn, +.status_crit, +.status_unknown { + border-radius: 5px; + padding: 2px 10px; + margin-right: 15px; + color: #444444; + vertical-align: text-top; + font-weight: normal; +} + +.status_ok { + background-color: #00bc8c; +} + +.status_warn { + background-color: #ffca2c; +} + +.status_crit { + background-color: #dc3545; + color: #FFFFFF; +} + +.status_unknown { + background-color: #0dcaf0; +} + +.loader { + border: 7px solid var(--bg-root-color); + border-top: 7px solid var(--text-color); + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 2s linear infinite; + margin: 10px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/statuspage/templates/statuspage.html b/statuspage/templates/statuspage.html new file mode 100644 index 0000000..7066f59 --- /dev/null +++ b/statuspage/templates/statuspage.html @@ -0,0 +1,17 @@ + + + + franzi.business service status + + + + + +

franzi.business service status page

+
+ + +