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
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import mysql.connector as sql
|
import mysql.connector as sql
|
||||||
from mysql.connector import IntegrityError
|
from mysql.connector import IntegrityError
|
||||||
import requests
|
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