Initial MVP
This commit is contained in:
parent
f6a56339e6
commit
18b2244133
7
cron.sh
Executable file
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
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
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
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
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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user