diff --git a/cron.sh b/cron.sh new file mode 100755 index 0000000..9bcf587 --- /dev/null +++ b/cron.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -eu + +cd $(dirname $0) + +./env/bin/python devicecheck.py diff --git a/devicecheck.py b/devicecheck.py index 70a16dc..c8ed449 100644 --- a/devicecheck.py +++ b/devicecheck.py @@ -1,6 +1,95 @@ #!/usr/bin/env python3 +import subprocess + +from datetime import datetime + import mysql.connector as sql from mysql.connector import IntegrityError import requests + +class Checker: + def __init__(self, config): + self.apisession = requests.Session() + self.apisession.auth = (config['deviceapi']['user'], + config['deviceapi']['pass']) + self.apisession.headers = {'Accept': 'application/json', + 'Content-Type': 'application/json'} + + self.apiurl = config['deviceapi']['url'] + if self.apiurl.endswith('/'): + self.apiurl = self.apiurl[:-1] + + self.dbparameters = { + 'host': config['database']['host'], + 'port': config['database']['port'], + 'database': config['database']['database'], + 'user': config['database']['user'], + 'password': config['database']['pass'], + } + + def _do_request(self, path): + response = self.apisession.get(self.apiurl + path) + response.raise_for_status() + return response.json() + + def check_all(self): + hostcache = set() + for item in self._do_request('/config/devices'): + if 'host' in item: + hostcache.add(item['host']) + + for host in hostcache: + result = self.ping(host) + self.store(host, result) + + if result.returncode == 0: + print(f'{host} OK') + else: + print(f'{host}: {result.returncode} - {result.stderr}') + + self._prune_db() + + def ping(self, host): + # Ping settings: + # - send 3 packets + # - wait 0.5 seconds between packets + # - timeout each packet after 1 second + # - timeout entire operation after 5 seconds + command = ['ping', '-q', + '-c', '3', + '-i', '0.5', + '-W', '1', + '-w', '5', + host] + return subprocess.run(command, capture_output=True, text=True) + + def _prune_db(self): + stmt = 'delete from `results`' + stmt += 'where `timestamp` < unix_timestamp(date_sub(now(), interval 30 day))' + with sql.connect(**self.dbparameters) as db: + with db.cursor() as cursor: + cursor.execute(stmt) + db.commit() + + def store(self, host, result): + now = datetime.now().timestamp() + stmt = 'insert into `results`' + stmt += '(`host`, `timestamp`, `returncode`, `detail`)' + stmt += 'values(%s, %s, %s, %s)' + with sql.connect(**self.dbparameters) as db: + with db.cursor() as cursor: + cursor.execute(stmt, (host, + now, + result.returncode, + result.stderr)) + db.commit() + + +if __name__ == '__main__': + from configparser import ConfigParser + conf = ConfigParser() + conf.read('config.ini') + checker = Checker(conf) + checker.check_all() diff --git a/public/get.php b/public/get.php new file mode 100644 index 0000000..db520b2 --- /dev/null +++ b/public/get.php @@ -0,0 +1,25 @@ +<?php +header('content-type: application/json'); + +$conf = parse_ini_file('../config.ini', true); +$dbconf = $conf['database']; +$db = new mysqli($dbconf['host'], + $dbconf['user'], + $dbconf['pass'], + $dbconf['database'], + $dbconf['port']); + +$result = $db->query('select o.* from results as o + inner join (select host, max(timestamp) as latest + from results group by host) + as i on o.host= i.host + where o.timestamp = i.latest'); + +$out = []; +foreach($result as $row) { + $out[$row['host']] = ['time' => $row['timestamp'], + 'status' => $row['returncode'], + 'detail' => $row['detail']]; +} +print(json_encode($out)); +?> diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9ea5098 --- /dev/null +++ b/public/index.html @@ -0,0 +1,26 @@ +<!doctype html> +<html lang="se"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <meta http-equiv="refresh" content="60" /> + <title>Status salsteknik</title> + <script type="text/javascript" src="./script.js" defer></script> + <link rel="stylesheet" href="./style.css" /> + </head> + <body> + <h2>Devices with errors</h2> + <errors></errors> + <h2>Devices working as expected</h2> + <oks></oks> + <template id="itembox"> + <div class="itembox"> + <name></name> + <checktime></checktime> + <status></status> + <detail></detail> + </div> + </template> + </body> +</html> diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..a7802f2 --- /dev/null +++ b/public/script.js @@ -0,0 +1,47 @@ +const template = document.getElementById('itembox'); +const oks = document.querySelector('oks'); +const errors = document.querySelector('errors'); +const now = Date.now(); +fetch('./get.php') + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + names = Object.keys(data); + names.sort(); + for(var i = 0; i < names.length; ++i) { + const key = names[i]; + const item = data[key]; + const instance = template.content.cloneNode(true); + const name = instance.querySelector('name'); + const checktime = instance.querySelector('checktime'); + const status = instance.querySelector('status'); + const detail = instance.querySelector('detail'); + + name.textContent = key.replace(/\.dsv\.local\.su\.se/, ''); + checktime.textContent = formatTime(now/1000 - item.time); + + if(item.status == '0') { + status.parentNode.removeChild(status); + detail.parentNode.removeChild(detail); + instance.firstElementChild.classList.add('good'); + oks.appendChild(instance); + } else { + status.textContent = item.status; + detail.textContent = item.detail; + instance.firstElementChild.classList.add('bad'); + errors.appendChild(instance); + } + } + }); + +function formatTime(secondsAgo) { + if (secondsAgo < 60) { + return secondsAgo + " seconds ago"; + } else { + return (secondsAgo/60).toFixed(0) + " minutes ago"; + } +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..26efbcb --- /dev/null +++ b/public/style.css @@ -0,0 +1,26 @@ +oks, errors { + display: grid; + gap: 20px; + place-items: stretch; + place-content: center; + grid-template-columns: repeat(auto-fill, 300px); + #grid-auto-rows: 75px; + grid-auto-flow: row; +} + +.itembox { + display: flex; + flex-direction: column; + align-items: center; + color: white; + font-weight: bold; + padding: 10px; +} + +.good { + background-color: green; +} + +.bad { + background-color: red; +}