"""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.