Files
mission-control-v2/backend/tests/test_messwerte.py
T
HitonabiandClaude Opus 5.5 f7a9b8ad50 Welle 1 · Vorhersage „voll in etwa N Tagen“: Pool, NAS, Systemplatte, Container, VMs und die Platte der KI-Box
Aus dem Verlauf der Woche (Tagesmittel, Schutz gegen einmalige Sprünge, schneller werdendes Volllaufen zählt sofort):
gelb unter 14, rot unter 3 Tagen. Der Wächter meldet es, auch wenn der Füllstand selbst noch unauffällig ist;
Speicherkarte, Geräte-Tabelle, Instrumente der Übersicht und die KI-Box-Seite zeigen es an. Jeder Bericht des
Ausführers schreibt dafür die Belegungen in die Messreihe „speicher“.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:21:38 +02:00

397 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
assert antwort["prognose"]["platte"]["grund"] == "verlauf" # „voll in etwa N Tagen“ erst ab zwei Tagen Verlauf
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 host["prognose"]["platte"]["grund"] == "verlauf" and geraete["vm-106"]["prognose"]["platte"]["anteil"] is None
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.