Initial MVP

This commit is contained in:
Erik Thuning 2024-09-09 11:41:02 +02:00
parent f6a56339e6
commit 18b2244133
6 changed files with 220 additions and 0 deletions

7
cron.sh Executable file

@ -0,0 +1,7 @@
#!/bin/sh
set -eu
cd $(dirname $0)
./env/bin/python 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()

25
public/get.php Normal file

@ -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));
?>

26
public/index.html Normal file

@ -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>

47
public/script.js Normal file

@ -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";
}
}

26
public/style.css Normal file

@ -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;
}