- kern/messreihen.py: Minutenwerte als JSON-Zeilen je Quelle und Tag unter <Datenordner>/mc2-messwerte/, Anhaengen unter flock, Aufraeumen nach 8 Tagen, Lesen fuer 1h/24h/7d (60 s, 5-min- und 30-min-Mittel), Luecken bleiben null, kaputte Zeilen werden uebersprungen, Zaehler-Raten ohne Spruenge - KI-Box: Taktgeber im Steward (services/messwerte.py) schreibt jede Minute cpu, ram, gpu, Temperaturen, platte, Netz in Bytes/s und Tokens pro Minute; GET /api/messwerte (nur Rolle box) - Homelab: Der Ausfuehrer schickt jede Minute in einem eigenen Faden Host-Werte aus /proc und die Gaeste aus einem pvesh-Aufruf an POST /api/homelab/ausfuehrer/messwerte; services/homelab/messwerte.py rechnet die Zaehler in Bytes/s um (Neustarts und Spruenge ergeben null), GET /api/homelab/messwerte liefert Host und Gaeste wie im Inventar - Waechter (homelab): gelb, wenn der Host 10 Minuten ueber 95 % RAM oder 90 °C liegt - Aufraeumen der Modell-Platte bietet mc2-messwerte nie zum Loeschen an Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
395 lines
24 KiB
Python
395 lines
24 KiB
Python
"""Messwerte mit Verlauf (Monitoring, 24.09.2026): der gemeinsame Speicher (kern/messreihen.py), der Minutenpunkt der
|
|
KI-Box, die Raten und Endpunkte des Homelabs, die reine Auswertung des Ausführers (mit echten Zeilen vom Proxmox-PC,
|
|
gelesen am 24.09.) und die Wächter-Prüfung des Hosts."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from kern import einstellungen as einstellungen_mod
|
|
from kern import messreihen
|
|
from kern.zeit import LOCAL_TZ
|
|
|
|
BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8"))
|
|
AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py"
|
|
MITTAG = datetime(2026, 9, 24, 12, 0, 10, tzinfo=LOCAL_TZ).timestamp() # Do 24.09.2026, 12:00:10 Berlin
|
|
|
|
|
|
@pytest.fixture
|
|
def daten(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
|
|
monkeypatch.delenv("MC_AUSFUEHRER_TOKEN", raising=False)
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
from services.homelab import kanal
|
|
from services.homelab import messwerte as homelab_messwerte
|
|
monkeypatch.setattr(kanal, "_kontakt", {"zuletzt": None})
|
|
monkeypatch.setattr(homelab_messwerte, "_zustand", {"zaehler": {}, "geraete": {}})
|
|
yield tmp_path
|
|
einstellungen_mod.einstellungen.cache_clear()
|
|
|
|
|
|
def _werte(reihe: list) -> list:
|
|
return [v for _, v in reihe if v is not None]
|
|
|
|
|
|
# --- Der gemeinsame Speicher -----------------------------------------------------------------------
|
|
|
|
def test_stunde_im_minutenraster_mit_luecke(daten):
|
|
for minute, cpu in ((-5, 10), (-4, 20.0), (-2, 40), (-1, 50)): # 11:57 fehlt
|
|
messreihen.schreiben("box", {"cpu": cpu, "ram": 30}, t=MITTAG - 10 + minute * 60 + 30)
|
|
stunde = messreihen.lesen("box", "1h", ("cpu", "ram"), jetzt=MITTAG)
|
|
cpu = stunde["reihen"]["cpu"]
|
|
assert stunde["schritt_s"] == 60 and len(cpu) == 60 and len(stunde["reihen"]["ram"]) == 60
|
|
assert all(t % 60 == 0 for t, _ in cpu) and cpu[-1][0] == MITTAG - 10 # der laufende Schritt ist der letzte
|
|
assert [v for _, v in cpu[-6:]] == [10, 20.0, None, 40, 50, None] # Lücke bleibt Lücke, nichts erfunden
|
|
assert _werte(stunde["reihen"]["ram"]) == [30, 30, 30, 30]
|
|
|
|
|
|
def test_verdichtung_auf_fuenf_und_dreissig_minuten(daten):
|
|
beginn = MITTAG - 10 - 3600 # 11:00:00
|
|
for minute in range(60):
|
|
messreihen.schreiben("box", {"cpu": minute}, t=beginn + minute * 60 + 30)
|
|
tag = messreihen.lesen("box", "24h", ("cpu",), jetzt=MITTAG)
|
|
assert tag["schritt_s"] == 300 and len(tag["reihen"]["cpu"]) == 288
|
|
assert _werte(tag["reihen"]["cpu"]) == [2, 7, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57] # Mittel je 5 Minuten
|
|
woche = messreihen.lesen("box", "7d", ("cpu",), jetzt=MITTAG)
|
|
assert woche["schritt_s"] == 1800 and len(woche["reihen"]["cpu"]) == 336
|
|
assert _werte(woche["reihen"]["cpu"]) == [14.5, 44.5]
|
|
# Ein zweites Lesen kommt aus den gemerkten Summen und ist gleich; ein neuer Punkt ändert die Datei und zählt mit.
|
|
assert messreihen.lesen("box", "7d", ("cpu",), jetzt=MITTAG) == woche
|
|
messreihen.schreiben("box", {"cpu": 100}, t=MITTAG)
|
|
assert _werte(messreihen.lesen("box", "7d", ("cpu",), jetzt=MITTAG)["reihen"]["cpu"]) == [14.5, 44.5, 100]
|
|
with pytest.raises(ValueError):
|
|
messreihen.lesen("box", "2d", ("cpu",), jetzt=MITTAG)
|
|
|
|
|
|
def test_kaputte_zeilen_werden_uebersprungen(daten):
|
|
messreihen.schreiben("pve", {"cpu": 1.0}, t=MITTAG - 200)
|
|
datei = messreihen.ordner() / "pve-2026-09-24.jsonl"
|
|
with open(datei, "ab") as f:
|
|
f.write(b'kein json\n[1, 2]\n{"cpu": 5}\n{"t": "gestern", "cpu": 5}\n\x00\x00\n')
|
|
f.write(b'{"t": %d, "cpu": NaN, "ram": true}\n' % int(MITTAG - 140))
|
|
f.write(b'{"t": %d, "cpu": 9' % int(MITTAG - 100)) # Absturz mitten in der Zeile
|
|
messreihen.schreiben("pve", {"cpu": 3.0, "ram": True, "platte": float("inf")}, t=MITTAG - 40)
|
|
punkte = messreihen.punkte("pve", MITTAG - 300, MITTAG)
|
|
assert [p["t"] for p in punkte] == [int(MITTAG - 200), int(MITTAG - 140), int(MITTAG - 40)]
|
|
assert punkte[-1] == {"t": int(MITTAG - 40), "cpu": 3.0, "ram": None, "platte": None}
|
|
assert _werte(messreihen.lesen("pve", "1h", ("cpu", "ram"), jetzt=MITTAG)["reihen"]["cpu"]) == [1.0, 3.0]
|
|
assert messreihen.letzter_punkt("pve", jetzt=MITTAG)["cpu"] == 3.0
|
|
|
|
|
|
def test_aufraeumen_nach_acht_tagen(daten):
|
|
ordner = messreihen.ordner()
|
|
ordner.mkdir(parents=True)
|
|
for name in ("box-2026-09-14.jsonl", "box-2026-09-15.jsonl", "box-2026-09-16.jsonl", "ct-100-2026-09-15.jsonl",
|
|
"ct-100-2026-09-23.jsonl", "notiz.txt", "zustand.json"):
|
|
(ordner / name).write_text("", encoding="utf-8")
|
|
messreihen.schreiben("box", {"cpu": 1}, t=MITTAG) # das erste Schreiben des Tages räumt auf
|
|
assert sorted(p.name for p in ordner.iterdir()) == [
|
|
"box-2026-09-16.jsonl", "box-2026-09-24.jsonl", "ct-100-2026-09-23.jsonl", "notiz.txt", "zustand.json"]
|
|
assert messreihen.aufraeumen(MITTAG) == 0
|
|
|
|
|
|
def test_letzter_punkt_auch_von_gestern_und_nie_zu_alt(daten):
|
|
mitternacht = datetime(2026, 9, 24, 0, 0, 20, tzinfo=LOCAL_TZ).timestamp()
|
|
messreihen.schreiben("box", {"cpu": 7}, t=mitternacht - 50) # 23.09., 23:59:30
|
|
assert messreihen.letzter_punkt("box", jetzt=mitternacht)["cpu"] == 7
|
|
assert messreihen.letzter_punkt("box", jetzt=mitternacht, max_alter_s=30) is None
|
|
assert messreihen.letzter_punkt("ct-999", jetzt=mitternacht) is None
|
|
assert messreihen.quellen(mitternacht - 3600, mitternacht) == ["box"]
|
|
|
|
|
|
def test_quellennamen_werden_geprueft(daten):
|
|
for boese in ("../box", "Box", "", "box/x"):
|
|
with pytest.raises(ValueError):
|
|
messreihen.schreiben(boese, {"cpu": 1}, t=MITTAG)
|
|
|
|
|
|
def test_rate_aus_zaehlerstaenden():
|
|
assert messreihen.rate(1000, 7000, 60) == 100.0
|
|
assert messreihen.rate(7000, 1000, 60) is None # Zähler kleiner: Neustart, keine negative Rate
|
|
assert messreihen.rate(1000, 7000, 301) is None # Lücke: keine Rate über fünf Minuten
|
|
assert messreihen.rate(None, 7000, 60) is None and messreihen.rate(1000, 7000, 0) is None
|
|
assert messreihen.rate(0, 1e12, 60) is None # 16 GB/s: ein Sprung, kein Verkehr
|
|
|
|
|
|
# --- KI-Box --------------------------------------------------------------------------------------
|
|
|
|
def test_minutenpunkt_der_box(monkeypatch):
|
|
from services import messwerte, system
|
|
folge = {"cpu": [(1000.0, 10000.0), (1300.0, 10600.0), (1400.0, 10700.0)],
|
|
"netz": [(1_000_000, 500_000), (1_600_000, 800_000), (100, 50)],
|
|
"tokens": [1000, 4000, 10], "gpu": [10, 30, 50, 0], "uhr": [0.0, 60.0, 120.0]}
|
|
monkeypatch.setattr(messwerte, "_cpu_zeiten", lambda: folge["cpu"].pop(0))
|
|
monkeypatch.setattr(messwerte, "_netz", lambda: folge["netz"].pop(0))
|
|
monkeypatch.setattr(messwerte, "_tokens", lambda: folge["tokens"].pop(0))
|
|
monkeypatch.setattr(messwerte, "_platte", lambda: 15.2)
|
|
monkeypatch.setattr(system, "_gpu_sysfs", lambda: {"busy_percent": folge["gpu"].pop(0)})
|
|
monkeypatch.setattr(system, "_temps", lambda: {"cpu": 40.0, "gpu": 35.5})
|
|
monkeypatch.setattr(messwerte.psutil, "virtual_memory", lambda: SimpleNamespace(percent=41.26))
|
|
messer = messwerte.Messer(uhr=lambda: folge["uhr"].pop(0))
|
|
messer.probe()
|
|
messer.probe()
|
|
# CPU: 300 von 600 Ticks beschäftigt; GPU: Mittel aus drei Proben; Netz und Tokens aus der Differenz über 60 s.
|
|
assert messer.punkt() == {"cpu": 50.0, "ram": 41.3, "gpu": 30.0, "temp_cpu": 40.0, "temp_gpu": 35.5,
|
|
"platte": 15.2, "netz_rx": 10000, "netz_tx": 5000, "tokens": 3000.0}
|
|
# Neustart des Gateways (neue Token-Zählung) und neue Netz-Zähler: diese Minute keine Rate statt einer negativen.
|
|
zweiter = messer.punkt()
|
|
assert (zweiter["netz_rx"], zweiter["netz_tx"], zweiter["tokens"], zweiter["gpu"]) == (None, None, None, 0.0)
|
|
assert zweiter["cpu"] == 100.0
|
|
|
|
|
|
def test_gemessen_wird_zur_halben_minute():
|
|
from services import messwerte
|
|
assert messwerte.naechste_messung(MITTAG) == MITTAG + 20 # 12:00:10 → 12:00:30
|
|
assert messwerte.naechste_messung(MITTAG + 20.5) == MITTAG + 80 # eben gemessen → 12:01:30
|
|
assert messwerte.naechste_messung(MITTAG + 25) == MITTAG + 80 # 12:00:35 → 12:01:30
|
|
|
|
|
|
def test_endpunkt_der_box(daten):
|
|
from routers import messwerte as messwerte_router
|
|
from services import messwerte
|
|
app = FastAPI()
|
|
app.include_router(messwerte_router.router)
|
|
client = TestClient(app)
|
|
jetzt = time.time()
|
|
punkt = {"cpu": 12.5, "ram": 40.0, "gpu": 3.0, "temp_cpu": 45.0, "temp_gpu": 40.0, "platte": 15.0,
|
|
"netz_rx": 1200, "netz_tx": 300, "tokens": 900.0}
|
|
messreihen.schreiben("box", punkt, t=jetzt - 30)
|
|
antwort = client.get("/api/messwerte", params={"zeitraum": "24h"}).json()
|
|
assert (antwort["zeitraum"], antwort["schritt_s"]) == ("24h", 300)
|
|
assert list(antwort["reihen"]) == list(messwerte.REIHEN)
|
|
assert all(len(reihe) == 288 for reihe in antwort["reihen"].values())
|
|
assert _werte(antwort["reihen"]["tokens"]) == [900] and _werte(antwort["reihen"]["cpu"]) == [12.5]
|
|
assert antwort["aktuell"] == punkt
|
|
stunde = client.get("/api/messwerte").json() # ohne Angabe: die letzte Stunde
|
|
assert stunde["schritt_s"] == 60 and len(stunde["reihen"]["cpu"]) == 60
|
|
assert client.get("/api/messwerte", params={"zeitraum": "1w"}).status_code == 400
|
|
# Schreibt der Steward nicht mehr, ist nichts „aktuell“.
|
|
assert set(messwerte.abfrage("1h", jetzt=jetzt + 600)["aktuell"].values()) == {None}
|
|
|
|
|
|
def test_aufraeumen_bietet_den_messwerte_ordner_nie_an(tmp_path, monkeypatch):
|
|
from services import aufraeumen, llamaswap
|
|
(tmp_path / "mc2-messwerte").mkdir()
|
|
(tmp_path / "Alt-GGUF").mkdir()
|
|
monkeypatch.setattr(aufraeumen, "MODELS_DIR", tmp_path)
|
|
monkeypatch.setattr(llamaswap, "read_config", lambda: {"models": {}})
|
|
assert [k["name"] for k in aufraeumen.kandidaten()] == ["Alt-GGUF"]
|
|
|
|
|
|
# --- Homelab: Raten, Annahme, Endpunkte ---------------------------------------------------------------
|
|
|
|
def test_netz_raten_mit_zaehlersprung_und_neustart():
|
|
from services.homelab import messwerte as hm
|
|
a = {"t": 0, "rx": 1000, "tx": 2000, "uptime": 500, "laeuft": True}
|
|
b = {"t": 60, "rx": 61000, "tx": 2600, "uptime": 560, "laeuft": True}
|
|
assert hm.raten(a, b) == (1000, 10)
|
|
assert hm.raten(None, b) == (None, None) # kein voriger Stand (dieser Teil neu gestartet)
|
|
c = {"t": 120, "rx": 500, "tx": 100, "uptime": 30, "laeuft": True}
|
|
assert hm.raten(b, c) == (None, None) # Gast neu gestartet: Laufzeit kleiner
|
|
d = {"t": 180, "rx": 400, "tx": 99999, "uptime": 90, "laeuft": True}
|
|
assert hm.raten(c, d) == (None, 1665) # nur rx kleiner geworden: nur dort keine Rate
|
|
assert hm.raten(d, {**d, "t": 600, "rx": 1000}) == (None, None) # Lücke über fünf Minuten
|
|
aus = {"t": 240, "rx": 0, "tx": 0, "uptime": 0, "laeuft": False}
|
|
assert hm.raten(d, aus) == (None, None) and hm.raten(aus, {**d, "t": 300}) == (None, None)
|
|
|
|
|
|
def _gast(vmid, art, name, etiketten, status="running", **werte):
|
|
gast = {"vmid": vmid, "art": art, "name": name, "etiketten": etiketten, "status": status, "cpu": 0.0, "maxcpu": 1,
|
|
"mem": 0, "maxmem": 536870912, "disk": 0, "maxdisk": 4143677440, "netin": 0, "netout": 0, "uptime": 0}
|
|
return {**gast, **werte}
|
|
|
|
|
|
def _minute(zeit: float, host_rx: int, gitea_netin: int) -> dict:
|
|
return {"zeit": zeit,
|
|
"host": {"cpu": 0.125, "speicher": {"belegt": 12_000_000_000, "gesamt": 32_000_000_000},
|
|
"rootfs": {"belegt": 48e9, "gesamt": 100e9}, "load1": 0.72, "uptime": 3379388, "temp_cpu": 50.5,
|
|
"netz": {"rx": host_rx, "tx": 1000, "schnittstellen": ["eno1"]}},
|
|
"gaeste": [
|
|
_gast(104, "lxc", "gitea", ["community-script", "git"], cpu=0.0183, mem=340934656,
|
|
maxmem=1073741824, disk=4914618368, maxdisk=8350298112, netin=gitea_netin, netout=5000,
|
|
uptime=1000),
|
|
_gast(106, "qemu", "arcane", [], cpu=0.25, maxcpu=4, mem=5325914112, maxmem=8589934592,
|
|
maxdisk=161061273600, netin=10, netout=10, uptime=99),
|
|
_gast(107, "lxc", "homelab-orchestrator", ["mc2"], cpu=0.5, mem=1, maxmem=2),
|
|
_gast(102, "lxc", "netbird", ["community-script"], status="stopped"),
|
|
]}
|
|
|
|
|
|
def _homelab_client() -> TestClient:
|
|
from routers import homelab
|
|
app = FastAPI()
|
|
app.include_router(homelab.router)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_homelab_endpunkte(daten):
|
|
from services.homelab import kanal
|
|
client = _homelab_client()
|
|
jetzt = time.time()
|
|
assert client.post("/api/homelab/ausfuehrer/messwerte", json=_minute(jetzt, 0, 0)).status_code == 403
|
|
kopf = {"X-MC2-Ausfuehrer": kanal.token()}
|
|
erste = client.post("/api/homelab/ausfuehrer/messwerte", json=_minute(jetzt - 60, 5_000_000, 1_000_000),
|
|
headers=kopf)
|
|
assert erste.json() == {"ok": True, "geraete": 4} # Host und drei Gäste; der eigene Container fehlt
|
|
client.post("/api/homelab/ausfuehrer/messwerte", json=_minute(jetzt, 5_600_000, 1_600_000), headers=kopf)
|
|
assert not list(messreihen.ordner().glob("ct-107-*"))
|
|
|
|
# Noch ohne Bericht: die Geräte der letzten Meldung, Namen soweit bekannt.
|
|
ohne = client.get("/api/homelab/messwerte").json()
|
|
assert [(g["id"], g["name"], g["art"]) for g in ohne["geraete"]] == [
|
|
("pve", "Proxmox-Host", "proxmox-host"), ("ct-102", "netbird", "container"), ("ct-104", "gitea", "container"),
|
|
("vm-106", "Arcane (Docker)", "vm")]
|
|
|
|
kanal.bericht_speichern(BERICHT) # mit Bericht: Geräte und Namen wie im Inventar
|
|
antwort = client.get("/api/homelab/messwerte", params={"zeitraum": "1h"}).json()
|
|
assert (antwort["zeitraum"], antwort["schritt_s"]) == ("1h", 60)
|
|
geraete = {g["id"]: g for g in antwort["geraete"]}
|
|
assert list(geraete) == ["pve", "ct-100", "ct-101", "ct-102", "ct-103", "ct-104", "ct-105", "vm-106"]
|
|
assert (geraete["ct-104"]["name"], geraete["ct-100"]["name"], geraete["vm-106"]["name"]) == (
|
|
"Gitea", "AdGuard Home", "Arcane (Docker)")
|
|
|
|
host = geraete["pve"]
|
|
assert list(host["reihen"]) == ["cpu", "ram", "platte", "netz_rx", "netz_tx", "temp_cpu"]
|
|
assert all(len(reihe) == 60 for reihe in host["reihen"].values())
|
|
assert _werte(host["reihen"]["netz_rx"]) == [10000] # erste Minute ohne Vorgänger: keine Rate
|
|
assert host["aktuell"] == {"cpu": 12.5, "ram": 37.5, "ram_used": 12_000_000_000, "ram_total": 32_000_000_000,
|
|
"platte": 48.0, "netz_rx": 10000, "netz_tx": 0, "temp_cpu": 50.5, "load1": 0.72,
|
|
"uptime_s": 3379388}
|
|
gitea = geraete["ct-104"]
|
|
assert list(gitea["reihen"]) == ["cpu", "ram", "platte", "netz_rx", "netz_tx"]
|
|
assert gitea["aktuell"] == {"cpu": 1.8, "ram": 31.8, "ram_used": 340934656, "ram_total": 1073741824,
|
|
"platte": 58.9, "netz_rx": 10000, "netz_tx": 0, "temp_cpu": None, "load1": None,
|
|
"uptime_s": 1000}
|
|
arcane = geraete["vm-106"]["aktuell"]
|
|
assert (arcane["cpu"], arcane["ram"], arcane["platte"]) == (25.0, 62.0, None) # die Platte einer VM sieht Proxmox nicht
|
|
netbird = geraete["ct-102"]["aktuell"] # gestoppt: keine Messwerte, keine Nullen
|
|
assert (netbird["cpu"], netbird["ram"], netbird["netz_rx"], netbird["ram_total"]) == (None, None, None, 536870912)
|
|
assert set(geraete["ct-100"]["aktuell"].values()) == {None} # nichts gemessen
|
|
assert client.get("/api/homelab/messwerte", params={"zeitraum": "7d"}).json()["schritt_s"] == 1800
|
|
assert client.get("/api/homelab/messwerte", params={"zeitraum": "30d"}).status_code == 400
|
|
|
|
|
|
# --- Der Ausführer (läuft auf dem Proxmox-Host) -----------------------------------------------------------
|
|
|
|
NETDEV = """Inter-| Receive | Transmit
|
|
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
|
|
lo: 12220307 51502 0 0 0 0 0 0 12220307 51502 0 0 0 0 0 0
|
|
eno1: 848364497291 629881217 0 114788 0 0 0 3360103 211535843861 102116072 0 4 0 0 0 0
|
|
enp2s0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
vmbr0: 736642320585 66694945 0 3399405 0 0 0 2462406 212959884634 52971342 0 5 0 0 0 0
|
|
veth100i0: 963620053 5314692 0 0 0 0 0 0 5535391141 14176389 0 0 0 0 0 0
|
|
veth104i0: 205263214515 35385337 0 0 0 0 0 0 6152585548 42736082 0 0 0 0 0 0
|
|
tap106i0: 10110528132 6394361 0 0 0 0 0 0 35706903774 10586880 0 0 0 0 0 0
|
|
fwpr106p0: 10110528132 6394361 0 0 0 0 0 0 35706903840 10586881 0 0 0 0 0 0
|
|
fwln106i0: 35706903840 10586881 0 0 0 0 0 0 10110528132 6394361 0 0 0 0 0 0
|
|
"""
|
|
MEMINFO = "MemTotal: 32225256 kB\nMemFree: 13537532 kB\nMemAvailable: 20092076 kB\nBuffers: 1 kB\n"
|
|
RESSOURCEN = [
|
|
{"id": "lxc/100", "type": "lxc", "vmid": 100, "name": "adguard", "node": "pve", "status": "running",
|
|
"tags": "adblock;community-script", "template": 0, "cpu": 0.000218199761345051, "maxcpu": 1, "mem": 247558144,
|
|
"maxmem": 536870912, "disk": 1361436672, "maxdisk": 2040373248, "netin": 5535402482, "netout": 963621401,
|
|
"uptime": 3379433},
|
|
{"id": "qemu/106", "type": "qemu", "vmid": 106, "name": "arcane", "node": "pve", "status": "running", "tags": None,
|
|
"template": 0, "cpu": 0.0194595849334057, "maxcpu": 4, "mem": 5325914112, "maxmem": 8589934592, "disk": 0,
|
|
"maxdisk": 161061273600, "netin": 35706938260, "netout": 10111961580, "uptime": 1726847},
|
|
{"id": "lxc/102", "type": "lxc", "vmid": 102, "name": "netbird", "node": "pve", "status": "stopped",
|
|
"tags": "community-script", "template": 0, "cpu": 0, "maxcpu": 1, "mem": 0, "maxmem": 536870912, "disk": 0,
|
|
"maxdisk": 4143677440, "netin": 0, "netout": 0, "uptime": 0},
|
|
{"id": "lxc/900", "type": "lxc", "vmid": 900, "name": "vorlage", "node": "pve", "template": 1},
|
|
{"id": "lxc/300", "type": "lxc", "vmid": 300, "name": "fremd", "node": "pve2", "status": "running"},
|
|
]
|
|
FUEHLER = [("r8169_0_100:00", "", 40.0), ("nvme", "Composite", 40.85), ("k10temp", "Tctl", 50.5),
|
|
("amdgpu", "edge", 38.0)]
|
|
|
|
|
|
def _ausfuehrer_modul():
|
|
spec = importlib.util.spec_from_file_location("ausfuehrer_messwerte", AUSFUEHRER)
|
|
modul = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(modul)
|
|
return modul
|
|
|
|
|
|
def _roh(zeit: float, netdev: str = NETDEV) -> dict:
|
|
return {"zeit": zeit, "stat": "cpu 1000 0 500 8000 500 0 0 0 0 0\ncpu0 500 0 250 4000 250 0 0 0 0 0\n",
|
|
"meminfo": MEMINFO, "loadavg": "0.72 0.50 0.49 3/665 2945458\n", "uptime": "3379388.63 39689887.90\n",
|
|
"netdev": netdev, "rootfs": (100861726720, 52717445120), "physisch": {"eno1", "enp2s0", "wlp3s0"},
|
|
"fuehler": FUEHLER, "ressourcen": RESSOURCEN}
|
|
|
|
|
|
def test_ausfuehrer_baut_den_messpunkt_aus_echten_zeilen():
|
|
a = _ausfuehrer_modul()
|
|
vorher = a.cpu_zeiten("cpu 900 0 400 7500 480 0 0 0 0 0\n")
|
|
punkt = a.messpunkt(_roh(1790000000.0), vorher, node="pve")
|
|
# Speicher und rootfs wie pvesh /nodes/pve/status am 24.09.2026 (used 12424376320 bzw. 48144281600).
|
|
assert punkt["host"] == {"cpu": 0.2778, "speicher": {"belegt": 12424376320, "gesamt": 32998662144},
|
|
"rootfs": {"belegt": 48144281600, "gesamt": 100861726720}, "load1": 0.72,
|
|
"uptime": 3379388, "temp_cpu": 50.5,
|
|
"netz": {"rx": 848364497291, "tx": 211535843861, "schnittstellen": ["eno1", "enp2s0"]}}
|
|
gaeste = {g["vmid"]: g for g in punkt["gaeste"]}
|
|
assert list(gaeste) == [100, 102, 106] # keine Vorlage, kein fremder Knoten
|
|
# Netz aus veth/tap, zeitgleich mit dem Host gelesen (Richtung aus Sicht des Gasts), nicht aus fwpr/fwln.
|
|
assert (gaeste[100]["netin"], gaeste[100]["netout"]) == (5535391141, 963620053)
|
|
assert (gaeste[106]["netin"], gaeste[106]["netout"]) == (35706903774, 10110528132)
|
|
assert (gaeste[102]["netin"], gaeste[102]["status"]) == (0, "stopped") # ohne Schnittstelle: /cluster/resources
|
|
assert gaeste[100]["etiketten"] == ["adblock", "community-script"] and gaeste[106]["art"] == "qemu"
|
|
assert (gaeste[106]["cpu"], gaeste[106]["maxcpu"], gaeste[106]["maxdisk"]) == (0.0194595849334057, 4, 161061273600)
|
|
# Ohne vorigen Stand keine CPU, ohne pvesh keine Gäste — der Host kommt trotzdem.
|
|
leer = a.messpunkt({**_roh(1.0), "ressourcen": None, "rootfs": None, "fuehler": []}, None, node="pve")
|
|
assert leer["gaeste"] == [] and leer["host"]["cpu"] is None and leer["host"]["temp_cpu"] is None
|
|
assert leer["host"]["rootfs"] is None and leer["host"]["load1"] == 0.72
|
|
|
|
|
|
def test_ausfuehrer_cpu_temperatur_und_netz_ohne_physische_schnittstelle():
|
|
a = _ausfuehrer_modul()
|
|
assert a.cpu_temperatur([("k10temp", "Tctl", 60.0), ("k10temp", "Tdie", 55.0)]) == 55.0
|
|
assert a.cpu_temperatur([("coretemp", "Core 0", 50.0), ("coretemp", "Package id 0", 58.3)]) == 58.3
|
|
assert a.cpu_temperatur([("nvme", "Composite", 40.0), ("amdgpu", "edge", 38.0)]) is None
|
|
zaehler = a.netz_zaehler(NETDEV)
|
|
assert a.host_netz(zaehler, set())["schnittstellen"] == ["vmbr0"]
|
|
assert a.host_netz({"lo": (1, 1)}, set()) is None
|
|
assert a.cpu_anteil((100, 1000), (100, 1000)) is None and a.speicher_aus("") is None
|
|
|
|
|
|
def test_ausfuehrer_und_homelab_teil_verstehen_sich(daten):
|
|
"""Der Messpunkt des Ausführers geht so, wie er ist, durch annehmen(): Namen der Felder und Richtung der Zähler."""
|
|
from services.homelab import messwerte as hm
|
|
a = _ausfuehrer_modul()
|
|
t0 = MITTAG + 20
|
|
erster = a.messpunkt(_roh(t0 - 60), None, node="pve")
|
|
netdev = NETDEV.replace("848364497291", "848370497291").replace("5535391141", "5535991141")
|
|
zweiter = a.messpunkt(_roh(t0, netdev), None, node="pve")
|
|
assert hm.annehmen(erster, jetzt=t0) == 4 and hm.annehmen(zweiter, jetzt=t0) == 4
|
|
host = messreihen.letzter_punkt("pve", jetzt=t0)
|
|
# 6 MB mehr auf eno1 in 60 s; RAM 12,42 von 33,0 GB wie im Proxmox-Knoten.
|
|
assert (host["netz_rx"], host["netz_tx"], host["ram"], host["platte"], host["temp_cpu"]) == (100000, 0, 37.7, 47.7,
|
|
50.5)
|
|
adguard = messreihen.letzter_punkt("ct-100", jetzt=t0)
|
|
assert (adguard["netz_rx"], adguard["netz_tx"], adguard["cpu"], adguard["platte"]) == (10000, 0, 0.0, 66.7)
|
|
assert messreihen.letzter_punkt("vm-106", jetzt=t0)["platte"] is None
|
|
|
|
|
|
# --- Wächter (Rolle homelab) ---------------------------------------------------------------------------
|
|
|
|
def test_waechter_meldet_vollen_oder_heissen_host(daten):
|
|
from services.homelab import messwerte as hm
|
|
for i in range(10):
|
|
messreihen.schreiben("pve", {"ram": 96.0 + i / 10, "ram_used": 31e9, "ram_total": 32e9, "temp_cpu": 91.0},
|
|
t=MITTAG - 590 + i * 60)
|
|
befunde = {b.id: b for b in hm.pruefe_host(MITTAG)}
|
|
assert set(befunde) == {"host-ram", "host-temp"} and {b.stufe for b in befunde.values()} == {"gelb"}
|
|
assert befunde["host-ram"].titel == "Proxmox-Host: Arbeitsspeicher seit 10 Minuten über 95 %"
|
|
assert "Zuletzt 97 % belegt (31,0 von 32,0 GB)" in befunde["host-ram"].text
|
|
assert befunde["host-temp"].titel == "Proxmox-Host: CPU seit 10 Minuten über 90 °C"
|
|
assert hm.pruefe_host(MITTAG + 400) == [] # nur drei Minuten im Fenster: zu wenig
|
|
messreihen.schreiben("pve", {"ram": 90.0, "temp_cpu": 80.0}, t=MITTAG - 30)
|
|
assert hm.pruefe_host(MITTAG) == [] # eine Minute darunter: nicht „10 Minuten lang“
|
|
# Registriert ist die Prüfung nur in der Rolle homelab: test_partner.py prüft die Liste in einem eigenen Prozess.
|