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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
34360c9644
commit
f7a9b8ad50
@@ -168,6 +168,7 @@ def test_endpunkt_der_box(daten):
|
||||
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
|
||||
@@ -258,6 +259,7 @@ def test_homelab_endpunkte(daten):
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Füllstands-Vorhersage „voll in etwa N Tagen“ (kern/prognose.py, Ausbauplan Welle 1, 25.09.2026): die Rechnung auf
|
||||
künstlichen Verläufen (stetig, ein Sprung, plötzlich schneller, zu wenig Verlauf) und wie Speicherkarte und Wächter sie
|
||||
nutzen."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from kern import einstellungen as einstellungen_mod
|
||||
from kern import messreihen, prognose
|
||||
|
||||
JETZT = 1_790_340_000.0 # 25.09.2026 ~14:40
|
||||
TAG = 86400
|
||||
|
||||
|
||||
def _reihe(wert_bei, tage: float = 7, alter_ab: float = 0) -> list[list]:
|
||||
"""30-min-Raster wie messreihen.lesen("7d"): [[t, wert], …], ältester zuerst; wert_bei(alter in Tagen)."""
|
||||
schritte = int(tage * TAG // 1800)
|
||||
return [[JETZT - alter_ab - i * 1800, wert_bei((alter_ab + i * 1800) / TAG)] for i in reversed(range(schritte))]
|
||||
|
||||
|
||||
def test_stetiges_wachstum():
|
||||
v = prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT) # 1 % am Tag, jetzt 57 %
|
||||
assert v == {"tage": 43.0, "rate": 1.0, "anteil": 57.0, "grund": None}
|
||||
assert prognose.stufe(v) == "ok"
|
||||
assert prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT, voll_bei=95)["tage"] == 38.0
|
||||
|
||||
|
||||
def test_ein_einmaliger_sprung_haelt_keinen_alarm():
|
||||
"""Ein großer Download vor zweieinhalb Tagen, seitdem nichts: Die Woche steigt, der letzte Tag nicht."""
|
||||
v = prognose.vorhersage(_reihe(lambda alter: 50 if alter < 2.5 else 40), JETZT)
|
||||
assert (v["grund"], v["tage"], v["rate"]) == ("gleich", None, 0.0)
|
||||
|
||||
|
||||
def test_ploetzlich_schneller_wartet_nicht_eine_woche():
|
||||
"""Fünf Tage gleich, dann 5 % am Tag: Die Woche allein sähe rund 1 % am Tag (voll in 30 Tagen)."""
|
||||
v = prognose.vorhersage(_reihe(lambda alter: 60 + 5 * max(0.0, 2 - alter)), JETZT)
|
||||
assert v["rate"] == pytest.approx(3.75, abs=0.05) and v["anteil"] == 70.0 and v["tage"] == pytest.approx(8, abs=0.2)
|
||||
assert prognose.stufe(v) == "gelb"
|
||||
|
||||
|
||||
def test_freigeraeumt_in_der_woche_zaehlt_die_woche():
|
||||
"""Sicherungsziel: jeden Tag +1 %, vor dreieinhalb Tagen hat das Aufräumen 6 % freigegeben — netto kaum Zuwachs.
|
||||
Die letzten beiden Tage allein (je +1 %) sollen nicht „schneller“ heißen."""
|
||||
v = prognose.vorhersage(_reihe(lambda alter: 80 - alter - (0 if alter < 3.5 else -6)), JETZT)
|
||||
assert v["rate"] < 1.0
|
||||
|
||||
|
||||
def test_ohne_genug_verlauf_keine_vorhersage():
|
||||
assert prognose.vorhersage([], JETZT)["grund"] == "verlauf"
|
||||
kurz = prognose.vorhersage(_reihe(lambda alter: 50 - alter, tage=1.5), JETZT)
|
||||
assert (kurz["grund"], kurz["anteil"]) == ("verlauf", 50.0)
|
||||
alt = prognose.vorhersage(_reihe(lambda alter: 50 - alter, alter_ab=3 * 3600), JETZT) # seit 3 h nichts
|
||||
assert (alt["grund"], alt["anteil"]) == ("verlauf", None)
|
||||
luecken = [[t, w if i % 5 == 0 else None] for i, (t, w) in enumerate(_reihe(lambda alter: 50 - alter))]
|
||||
assert prognose.vorhersage(luecken, JETZT)["grund"] == "verlauf" # je Tag keine sechs Stunden mit Messungen
|
||||
|
||||
|
||||
def test_texte_und_stufen():
|
||||
assert [prognose.in_tagen(t) for t in (0.4, 1.2, 9.4, 35, 200, 400)] == [
|
||||
"in weniger als einem Tag", "in etwa einem Tag", "in etwa 9 Tagen", "in etwa 5 Wochen", "in etwa 7 Monaten",
|
||||
"frühestens in einem Jahr"]
|
||||
v = {"tage": 2.5, "rate": 1.25, "anteil": 91.9, "grund": None}
|
||||
assert prognose.tempo(v) == "Die Belegung wächst um etwa 1,2 % am Tag (jetzt 92 %)."
|
||||
assert prognose.schlimmer("gelb", v) == ("rot", True) and prognose.schlimmer("rot", v) == ("rot", False)
|
||||
assert prognose.schlimmer(None, {"tage": None}) == ("ok", False)
|
||||
assert prognose.dazu({"tage": 30}) == " Bei diesem Tempo voll in etwa 4 Wochen." and prognose.dazu(None) == ""
|
||||
assert prognose.dazu({"tage": 80}) == ""
|
||||
|
||||
|
||||
# --- Mit echten Dateien: Verlauf aus den Berichten -------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def daten(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
|
||||
einstellungen_mod.einstellungen.cache_clear()
|
||||
yield tmp_path
|
||||
einstellungen_mod.einstellungen.cache_clear()
|
||||
|
||||
|
||||
def _bericht(pool: float, vm: float) -> dict:
|
||||
return {"host": {"pools": [{"speicher": "local-lvm", "belegt": int(pool * 1000), "gesamt": 1000, "meta_anteil": 0.01}],
|
||||
"netzlaufwerke": [{"ziel": "/mnt/qnap-backup", "platz": {"belegt": 400, "gesamt": 1000}},
|
||||
{"ziel": "/mnt/weg", "zustand": "haengt", "platz": None}],
|
||||
"sicherungsziele": {"pbs-qnap": {"belegt": 400, "gesamt": 1000}}},
|
||||
"gaeste": [{"vmid": 106, "art": "qemu", "platte": {"belegt": int(vm * 1000), "gesamt": 1000}},
|
||||
{"vmid": 102, "art": "lxc", "platte": {"belegt": 1, "gesamt": 4}}]}
|
||||
|
||||
|
||||
def test_berichte_schreiben_den_verlauf_und_die_vorhersage_liest_ihn(daten):
|
||||
from services.homelab import speicher
|
||||
for stunde in range(3 * 24, -1, -1): # drei Tage lang stündlich ein Bericht
|
||||
tage = stunde / 24
|
||||
speicher.verlauf_merken(_bericht(pool=0.70 - 0.02 * tage, vm=0.5), JETZT - stunde * 3600)
|
||||
letzte = messreihen.letzter_punkt(speicher.VERLAUF, JETZT)
|
||||
assert {k: v for k, v in letzte.items() if k != "t"} == {
|
||||
"pool:local-lvm": 70.0, "pool-meta:local-lvm": 1.0, "nas:/mnt/qnap-backup": 40.0, "ziel:pbs-qnap": 40.0,
|
||||
"vm-106": 50.0}
|
||||
v, meta = speicher.pool_vorhersage("local-lvm", JETZT)
|
||||
assert meta is False and v["rate"] == pytest.approx(2.0, abs=0.05) and v["tage"] == pytest.approx(15, abs=0.5)
|
||||
assert speicher.platten_vorhersage("vm-106", JETZT)["grund"] == "gleich"
|
||||
assert speicher.platten_vorhersage("ct-102", JETZT)["grund"] == "verlauf" # Minutenwerte fehlen hier
|
||||
assert speicher.platten_reihe("pve") == ("pve", "platte", 95.0) and speicher.platten_reihe("nas") is None
|
||||
speicher.verlauf_merken({"host": "kaputt"}, JETZT) # kostet nur diesen Punkt
|
||||
|
||||
|
||||
def test_speicherkarte_und_waechter_mit_vorhersage(monkeypatch):
|
||||
from services.homelab import speicher
|
||||
from test_homelab_speicher import _bericht as echter_bericht
|
||||
monkeypatch.setattr(speicher, "antwortet", lambda s, p: True)
|
||||
monkeypatch.setattr(speicher, "_bericht", lambda: echter_bericht())
|
||||
bald = {"tage": 5.2, "rate": 2.0, "anteil": 60.0, "grund": None}
|
||||
spaeter = {"tage": 30.0, "rate": 0.3, "anteil": 91.0, "grund": None}
|
||||
kurven = {"pool:local-lvm": bald, "nas:/mnt/qnap-backup": spaeter, "ziel:pbs-qnap": spaeter}
|
||||
monkeypatch.setattr(speicher, "_vorhersage", lambda name, jetzt: kurven.get(name, {"tage": None, "grund": "verlauf"}))
|
||||
monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: {"tage": 2.0, "rate": 5.0, "anteil": 85.0})
|
||||
|
||||
lage = speicher.lage()
|
||||
assert lage["pools"][0]["vorhersage"] == bald and lage["nas"][0]["vorhersage"] == spaeter
|
||||
assert lage["systemplatte"] is None and lage["sicherungen"]["ziele"][0]["vorhersage"] == spaeter
|
||||
|
||||
befunde = {b.id: b for b in speicher.pruefe_speicher(1_790_330_000.0)}
|
||||
pool = befunde["pool:local-lvm"]
|
||||
assert (pool.stufe, pool.titel) == ("gelb", "Speicherpool local-lvm in etwa 5 Tagen voll")
|
||||
assert pool.text.startswith("Die Belegung wächst um etwa 2,0 % am Tag (jetzt 60 %). Noch ")
|
||||
assert "nas:/mnt/qnap-backup" not in befunde # 39 % und 30 Tage: nichts zu melden
|
||||
|
||||
# Der Füllstand löst aus, die Vorhersage steht dabei; die Systemplatte läuft in zwei Tagen voll (rot).
|
||||
b = echter_bericht(nas_anteil=0.91)
|
||||
b["host"]["systemplatte"] = {"belegt": 40, "gesamt": 100}
|
||||
monkeypatch.setattr(speicher, "_bericht", lambda: b)
|
||||
befunde = {x.id: x for x in speicher.pruefe_speicher(1_790_330_000.0)}
|
||||
nas = befunde["nas:/mnt/qnap-backup"]
|
||||
assert (nas.stufe, nas.titel) == ("rot", "NAS (192.168.178.62) zu 91 % voll")
|
||||
assert "frei. Bei diesem Tempo voll in etwa 4 Wochen. Die Sicherungen" in nas.text
|
||||
host = befunde["host-platte"]
|
||||
assert (host.stufe, host.titel) == ("rot", "Systemplatte des Proxmox-Hosts in etwa 2 Tagen voll")
|
||||
|
||||
|
||||
def test_waechter_gastplatten_und_box_platte(monkeypatch):
|
||||
from services import waechter
|
||||
from services.homelab import kanal, speicher
|
||||
bericht = {"gaeste": [{"vmid": 101, "art": "lxc", "name": "npmplus", "platte": {"belegt": 1, "gesamt": 4}},
|
||||
{"vmid": 106, "art": "qemu", "name": "arcane", "platte": {"belegt": 90, "gesamt": 100}}]}
|
||||
monkeypatch.setattr(kanal, "bericht", lambda: {"bericht": bericht})
|
||||
kurven = {"ct-101": {"tage": 1.2, "rate": 60.0, "anteil": 25.0}, "vm-106": {"tage": 40.0, "rate": 0.1, "anteil": 90.0}}
|
||||
monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: kurven[gid])
|
||||
befunde = {b.id: b for b in waechter.pruefe_gast_platten()}
|
||||
ct = befunde["gast-platte:101"]
|
||||
assert (ct.stufe, ct.quelle) == ("rot", "ct-101") and ct.titel.endswith(" in etwa einem Tag voll")
|
||||
assert ct.text.startswith("Die Belegung wächst um etwa 60,0 % am Tag (jetzt 25 %). 0,0 von 0,0 GB belegt")
|
||||
vm = befunde["gast-platte:106"]
|
||||
assert vm.stufe == "rot" and vm.titel.endswith(" zu 90 % voll") and "(VM 106). Bei diesem Tempo voll in etwa 6 Wochen. Platz" in vm.text
|
||||
|
||||
# KI-Box: Füllstand unauffällig, aber in 10 Tagen voll → eigener gelber Hinweis.
|
||||
monkeypatch.setattr(waechter, "ROLLE", "box")
|
||||
monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=62.0))
|
||||
monkeypatch.setattr(waechter.prognose, "aus_messreihe",
|
||||
lambda quelle, name, voll_bei=100.0, jetzt=None: {"tage": 10.4, "rate": 3.6, "anteil": 62.0})
|
||||
(tempo,) = waechter.pruefe_platte()
|
||||
assert (tempo.id, tempo.stufe, tempo.titel) == ("platte:tempo", "gelb", "Die Platte ist in etwa 10 Tagen voll")
|
||||
monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=85.0))
|
||||
assert [b.id for b in waechter.pruefe_platte()] == ["platte"] # gelb ist gelb: kein zweiter Hinweis
|
||||
Reference in New Issue
Block a user