P42-S.M.: Server-Monitoring-Agent als Docker-Container

Misst CPU, RAM, Swap, Platten, Netzwerk, Temperaturen und Prozesse des
Hosts und liefert sie als JSON-API (/api/metrics, /api/history) für das
P42-Dashboard. Nur Python-Standardbibliothek, Token-geschützt,
Container ohne Root und mit schreibgeschütztem Dateisystem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-14 15:15:01 +02:00
commit c6eef176a2
11 files changed
+727

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
# P42-S.M. – Server-Monitoring-Agent
Kleiner Docker-Dienst, der die Auslastung des Ubuntu-Servers misst und als JSON-API bereitstellt.
**P42-F.W.** (Dashboard) liest die Werte aus und stellt sie dar.
Gemessen wird alle 5 Sekunden: CPU (gesamt, je Thread, iowait, Takt, Load), RAM, Swap, Platten
(Belegung je Laufwerk, Lese-/Schreibrate), Netzwerk (Rate je Schnittstelle und Summe),
Temperaturen, Prozesse, Uptime. Die letzte Stunde wird als Verlauf vorgehalten.
- Nur Python-Standardbibliothek, keine Pakete. Das Image ist `python:3.13-slim` plus etwa 15 KB Code.
- Liest `/proc` und `/` des Hosts **nur lesend** ein und läuft ohne Root, ohne Capabilities und mit
schreibgeschütztem Dateisystem.
- Kein Docker-Socket: der Container sieht keine anderen Container und kann nichts steuern.
## Betrieb
### Einrichten und Starten
```bash
git clone <repo> ~/festplatte/dockercontainer/p42-sm # oder Ordner kopieren
cd ~/festplatte/dockercontainer/p42-sm
./deploy/update-docker.sh
```
Beim ersten Aufruf legt das Skript `.env` mit einem zufälligen Token an und gibt ihn aus. **Diesen Token
an P42-F.W. weitergeben.** Nach etwa 10 Sekunden zeigt `docker compose ps` den Zustand `healthy`.
Test auf dem Server:
```bash
curl -H "Authorization: Bearer $(grep ^SM_TOKEN= .env | cut -d= -f2)" http://127.0.0.1:9142/api/metrics
```
### Hoch- und runterfahren, beobachten
```bash
docker compose stop # anhalten
docker compose start # wieder starten
docker compose restart # nach Änderungen an .env: besser "docker compose up -d"
docker compose down # Container entfernen (Netz "p42" wird mit entfernt, falls unbenutzt)
docker compose ps # Status inkl. healthy/unhealthy
docker compose logs -f # Log: nur Start, Fehler und abgelehnte Anfragen
docker stats p42-sm # Verbrauch des Agenten selbst (wenige MB RAM)
```
Updates: `./deploy/update-docker.sh` holt per `git pull` den neuen Stand und baut den Container neu.
### Einstellungen (`.env`)
| Variable | Standard | Bedeutung |
|---|---|---|
| `SM_TOKEN` | – (Pflicht) | Zugangs-Token der API |
| `SM_PORT` | `9142` | Port auf dem Server |
| `SM_BIND` | `127.0.0.1` | An welche Adresse der Port gebunden wird. Für Abfragen aus dem LAN die Server-IP, nie `0.0.0.0` |
| `SM_INTERVAL` | `5` | Messintervall in Sekunden |
| `SM_HISTORY` | `720` | Anzahl Verlaufspunkte (720 × 5 s = 1 h) |
| `SM_UID` | `1000` | Benutzer-ID des Containers, siehe unten |
**Fehlt eine Platte in `disks`?** Der Container muss die Ordner bis zum Einhängepunkt durchsuchen dürfen.
Liegt die Platte z. B. unter `/home/<user>/festplatte` (Rechte 750), muss `SM_UID` die UID dieses Benutzers
sein (`id -u <user>`). Netzlaufwerke (NFS/SMB) werden bewusst nicht gemessen.
## Anbindung an P42-F.W.
### Erreichbarkeit
**A) Dashboard läuft als Container auf demselben Server (empfohlen):** P42-S.M. legt das Docker-Netz `p42` an.
In der `docker-compose.yml` von P42-F.W. ergänzen:
```yaml
services:
p42:
# ... wie bisher ...
networks: [default, p42]
networks:
p42:
external: true
```
Danach erreicht das Dashboard-Backend den Dienst unter **`http://p42-sm:9142`**. Ein offener Port nach außen
ist dafür nicht nötig. P42-S.M. muss zuerst gestartet sein, da es das Netz anlegt.
**B) Dashboard läuft auf einem anderen Rechner:** In `.env` `SM_BIND` auf die LAN-IP des Servers setzen.
Die Adresse lautet dann `http://<server-ip>:9142`. Per Firewall sollte nur der Dashboard-Rechner zugreifen dürfen.
Abgefragt wird **serverseitig** aus dem Dashboard-Backend, nicht aus dem Browser. Token und Port bleiben so
verborgen, deshalb gibt es kein CORS.
### API
Alle Antworten sind JSON. Byte-Werte sind ganze Zahlen in Byte, Raten in Byte pro Sekunde, Prozente auf
eine Nachkommastelle gerundet, und Zeitstempel stehen in ISO 8601 mit Zeitzone. Sinnvolles Abfrageintervall:
alle 5–30 Sekunden; häufiger bringt nichts, weil nur alle `intervalSeconds` neu gemessen wird.
| Anfrage | Token | Antwort |
|---|---|---|
| `GET /api/health` | nein | `200 {"status":"ok"}`, bzw. `503`, solange noch keine Werte da sind oder die Messung hängt |
| `GET /api/metrics` | ja | aktueller Stand, siehe unten |
| `GET /api/history` | ja | Verlauf; `?minutes=N` begrenzt auf die letzten N Minuten |
Token als Header: `Authorization: Bearer <SM_TOKEN>`. Fehlt er oder ist er falsch, kommt `401`.
Bis die erste Messung vorliegt (etwa 1 s nach dem Start), kommt `503`.
#### `GET /api/metrics`
```json
{
"timestamp": "2026-10-14T14:59:01+02:00",
"intervalSeconds": 5,
"host": { "hostname": "server", "os": "Ubuntu 24.04.1 LTS", "kernel": "6.8.0-45-generic", "uptimeSeconds": 15943 },
"cpu": {
"model": "AMD Ryzen 7 5700X 8-Core Processor",
"threads": 16,
"percent": 6.2,
"iowaitPercent": 0.0,
"perCorePercent": [2.6, 2.0, 5.5, 21.0],
"mhz": 2894,
"load": { "1m": 0.81, "5m": 0.91, "15m": 0.8 }
},
"memory": { "totalBytes": 16682397696, "usedBytes": 6809317376, "availableBytes": 9873080320, "cachedBytes": 5789290496, "percent": 40.8 },
"swap": { "totalBytes": 536866816, "usedBytes": 0, "percent": 0.0 },
"disks": [
{ "mount": "/", "device": "/dev/sda2", "fs": "ext4", "totalBytes": 983040749568, "usedBytes": 883751505920, "freeBytes": 49278058496, "percent": 94.7 }
],
"diskIo": { "readBytesPerSec": 44951, "writeBytesPerSec": 73557 },
"network": {
"rxBytesPerSec": 10279,
"txBytesPerSec": 51353,
"interfaces": [
{ "interface": "eth0", "virtual": false, "rxBytesPerSec": 10279, "txBytesPerSec": 51353, "rxBytesTotal": 704405487, "txBytesTotal": 89327228 }
]
},
"temperatures": [
{ "sensor": "k10temp", "label": "Tctl", "celsius": 51.0, "criticalCelsius": null },
{ "sensor": "nvme", "label": "Composite", "celsius": 39.85, "criticalCelsius": 79.85 }
],
"processes": { "total": 429, "running": 1 }
}
```
Hinweise zu den Feldern:
- `memory.usedBytes` ist der tatsächlich belegte Speicher, also ohne Cache, der jederzeit freigegeben werden
kann. Das entspricht der Spalte „used“ von `free`.
- `disks[].percent` wird wie bei `df` berechnet. `freeBytes` ist der für Benutzer verfügbare Platz.
- `network.rxBytesPerSec`/`txBytesPerSec` summiert nur echte Schnittstellen. Docker-Bridges und `veth*`
stehen mit `"virtual": true` in der Liste, werden aber nicht mitgezählt, sonst zählt Container-Verkehr doppelt.
- `temperatures`: welche Sensoren es gibt, hängt von der Hardware ab. Die Liste kann leer sein (z. B. bei VMs)
und `criticalCelsius` kann `null` sein.
- `cpu.mhz` ist der Durchschnitt aller Threads und kann bei VMs `null` sein.
#### `GET /api/history?minutes=30`
```json
{
"intervalSeconds": 5,
"points": [
{ "timestamp": "2026-10-14T14:58:59+02:00", "cpuPercent": 5.0, "memoryPercent": 40.9, "load1m": 0.81,
"rxBytesPerSec": 2769, "txBytesPerSec": 6111, "readBytesPerSec": 0, "writeBytesPerSec": 2757518 }
]
}
```
Älteste Punkte zuerst. Der Verlauf liegt nur im Speicher und beginnt nach einem Neustart des Containers von vorn.
Für längere Zeiträume speichert das Dashboard die Werte selbst.
## Lokal testen (ohne Docker)
```bash
HOST_PROC=/proc HOST_ROOT=/ SM_TOKEN=test PORT=9142 HOST=127.0.0.1 python3 -m app
```
## Aufbau
```
app/collector.py liest /proc und /sys, berechnet Raten aus zwei Messungen
app/server.py HTTP-API, Token-Prüfung, Mess-Thread, Verlauf
app/__main__.py Start, sauberes Beenden bei "docker stop"
Dockerfile, docker-compose.yml
deploy/update-docker.sh Bauen/Aktualisieren, legt beim ersten Mal .env mit Token an
.env Token und Einstellungen (nicht ins Git)
```