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
@@ -24,7 +24,7 @@ import time
|
||||
|
||||
from kern import messreihen
|
||||
|
||||
from services.homelab import apps, inventar, kanal
|
||||
from services.homelab import apps, inventar, kanal, speicher
|
||||
|
||||
HOST = "pve"
|
||||
_GAST = re.compile(r"(ct|vm)-\d+")
|
||||
@@ -199,18 +199,20 @@ def _geraete(von: float, bis: float) -> list[dict]:
|
||||
|
||||
def abfrage(zeitraum: str, jetzt: float | None = None) -> dict:
|
||||
"""Für GET /api/homelab/messwerte: {"zeitraum", "schritt_s", "geraete": [{"id", "name", "art", "reihen",
|
||||
"aktuell"}]}. temp_cpu gibt es als Reihe nur beim Host; in „aktuell“ stehen temp_cpu und load1 bei Gästen auf null."""
|
||||
"aktuell", "prognose"}]}. temp_cpu gibt es als Reihe nur beim Host; in „aktuell“ stehen temp_cpu und load1 bei
|
||||
Gästen auf null. prognose.platte: „voll in etwa N Tagen“ (kern/prognose.py, seit 25.09.2026)."""
|
||||
dauer, schritt = messreihen.zeitraum_pruefen(zeitraum)
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
zeit = time.time() if jetzt is None else jetzt
|
||||
geraete = []
|
||||
for geraet in _geraete(jetzt - dauer, jetzt):
|
||||
for geraet in _geraete(zeit - dauer, zeit):
|
||||
host = geraet["id"] == HOST
|
||||
daten = messreihen.lesen(geraet["id"], zeitraum, HOST_REIHEN if host else GAST_REIHEN, jetzt)
|
||||
letzter = messreihen.letzter_punkt(geraet["id"], jetzt, AKTUELL_MAX_S) or {}
|
||||
daten = messreihen.lesen(geraet["id"], zeitraum, HOST_REIHEN if host else GAST_REIHEN, zeit)
|
||||
letzter = messreihen.letzter_punkt(geraet["id"], zeit, AKTUELL_MAX_S) or {}
|
||||
aktuell = {name: letzter.get(name) for name in AKTUELL}
|
||||
if not host:
|
||||
aktuell["temp_cpu"] = aktuell["load1"] = None
|
||||
geraete.append({**geraet, "reihen": daten["reihen"], "aktuell": aktuell})
|
||||
geraete.append({**geraet, "reihen": daten["reihen"], "aktuell": aktuell,
|
||||
"prognose": {"platte": speicher.platten_vorhersage(geraet["id"], jetzt)}})
|
||||
return {"zeitraum": zeitraum, "schritt_s": schritt, "geraete": geraete}
|
||||
|
||||
|
||||
|
||||
@@ -11,20 +11,27 @@ Alles aus dem Bericht des Ausführers (alle 10 Minuten), dazu eine eigene Prüfu
|
||||
(täglicher Auftrag: höchstens 26 Stunden alt, sonst 8 Tage), und das Ziel ist erreichbar.
|
||||
• Systemplatte des Proxmox-Hosts (Stufen wie oben): Dort liegen die Kernel, die Sicherungen vor Updates (local) und
|
||||
alles vom Host selbst. Alte Kernel lassen sich per Knopf entfernen (Ausführer: kernel_aufraeumen).
|
||||
• Tempo (seit 25.09.2026, kern/prognose.py): Läuft etwas in weniger als 14 Tagen voll, ist es gelb, unter 3 Tagen
|
||||
rot — auch wenn der Füllstand selbst noch unauffällig ist. Den Verlauf dafür schreibt jeder Bericht in die
|
||||
Messreihe VERLAUF (verlauf_merken); Host und Container haben ihn schon in den Minutenwerten.
|
||||
Hinweise gehen über den Wächter (pruefe_speicher), die Oberfläche zeigt alles als Karte „Speicher und Sicherungen“ (lage).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from kern import messreihen, prognose
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services.homelab import apps, kanal
|
||||
from services.homelab.inventar import EIGENES_ETIKETT
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
GELB, ROT = 0.80, 0.90
|
||||
EMPFEHLUNG_AB = 1_000_000_000 # kleinere Gewinne sind keine eigene Empfehlung wert
|
||||
TRIM_AB = 250_000_000 # ein Container zählt beim gemeinsamen TRIM erst ab hier mit
|
||||
@@ -320,60 +327,155 @@ def sicherungen(bericht: dict, jetzt: float) -> dict:
|
||||
"erfasst": erfasst, "fehlend": fehlend, "zuletzt": zuletzt, "probleme": probleme, "stufe": stufe}
|
||||
|
||||
|
||||
# --- Vorhersage „voll in etwa N Tagen“ (seit 25.09.2026) --------------------------------------------------------
|
||||
|
||||
VERLAUF = "speicher" # Quelle in kern/messreihen.py: die Belegungen aus jedem Bericht (alle zehn Minuten)
|
||||
|
||||
|
||||
def _prozent(belegt: object, gesamt: object) -> float | None:
|
||||
if not (messreihen.ist_zahl(belegt) and messreihen.ist_zahl(gesamt)) or gesamt <= 0:
|
||||
return None
|
||||
return round(belegt / gesamt * 100, 2)
|
||||
|
||||
|
||||
def verlauf_merken(bericht: dict, jetzt: float | None = None) -> None:
|
||||
"""Die Belegungen eines Berichts als ein Punkt der Messreihe VERLAUF (in %): je Speicherpool Daten
|
||||
(„pool:<name>“) und Metadaten („pool-meta:<name>“), je Netzlaufwerk („nas:<ziel>“), je Sicherungsziel
|
||||
(„ziel:<name>“) und die Platte jeder VM („vm-<vmid>“ — die Minutenwerte kennen sie nicht). Ein kaputter Bericht
|
||||
kostet nur diesen Punkt."""
|
||||
try:
|
||||
host = bericht.get("host") if isinstance(bericht.get("host"), dict) else {}
|
||||
werte: dict[str, float | None] = {}
|
||||
for p in pools(bericht):
|
||||
werte[f"pool:{p['speicher']}"] = round(p["anteil"] * 100, 2)
|
||||
if messreihen.ist_zahl(p["meta_anteil"]):
|
||||
werte[f"pool-meta:{p['speicher']}"] = round(p["meta_anteil"] * 100, 2)
|
||||
for m in host.get("netzlaufwerke") or []:
|
||||
platz = m.get("platz") or {}
|
||||
werte[f"nas:{m.get('ziel')}"] = _prozent(platz.get("belegt"), platz.get("gesamt"))
|
||||
for name, z in (host.get("sicherungsziele") or {}).items():
|
||||
werte[f"ziel:{name}"] = _prozent(z.get("belegt"), z.get("gesamt"))
|
||||
for g in bericht.get("gaeste") or []:
|
||||
if g.get("art") == "qemu":
|
||||
platte = g.get("platte") or {}
|
||||
werte[f"vm-{g['vmid']}"] = _prozent(platte.get("belegt"), platte.get("gesamt"))
|
||||
werte = {name: wert for name, wert in werte.items() if wert is not None}
|
||||
if werte:
|
||||
messreihen.schreiben(VERLAUF, werte, jetzt)
|
||||
except Exception:
|
||||
log.warning("speicher: Verlauf der Belegungen nicht geschrieben", exc_info=True)
|
||||
|
||||
|
||||
def platten_reihe(geraet_id: str) -> tuple[str, str, float] | None:
|
||||
"""Wo der Verlauf der Platte eines Geräts liegt: (Quelle, Name, voll bei %). Host und Container in den
|
||||
Minutenwerten; VMs im VERLAUF, denn ihre Platte kennt nur der Bericht (über den Gast-Agenten)."""
|
||||
if geraet_id == "pve":
|
||||
return "pve", "platte", prognose.VOLL_EXT4
|
||||
if re.fullmatch(r"ct-\d+", geraet_id):
|
||||
return geraet_id, "platte", prognose.VOLL_EXT4
|
||||
if re.fullmatch(r"vm-\d+", geraet_id):
|
||||
return VERLAUF, geraet_id, prognose.VOLL_EXT4
|
||||
return None
|
||||
|
||||
|
||||
def platten_vorhersage(geraet_id: str, jetzt: float | None = None) -> dict | None:
|
||||
reihe = platten_reihe(geraet_id)
|
||||
return prognose.aus_messreihe(*reihe, jetzt=jetzt) if reihe else None
|
||||
|
||||
|
||||
def _vorhersage(name: str, jetzt: float | None) -> dict:
|
||||
return prognose.aus_messreihe(VERLAUF, name, jetzt=jetzt)
|
||||
|
||||
|
||||
def pool_vorhersage(speicher: str, jetzt: float | None = None) -> tuple[dict, bool]:
|
||||
"""Die Vorhersage eines Pools — Daten oder Metadaten, was früher voll ist — und ob es die Metadaten sind."""
|
||||
daten, meta = _vorhersage(f"pool:{speicher}", jetzt), _vorhersage(f"pool-meta:{speicher}", jetzt)
|
||||
return (meta, True) if prognose.bis_voll(meta) < prognose.bis_voll(daten) else (daten, False)
|
||||
|
||||
|
||||
# --- Für Oberfläche und Wächter ----------------------------------------------------------------------------------
|
||||
|
||||
def lage(jetzt: float | None = None) -> dict:
|
||||
"""Für GET /api/homelab/speicher. Pools, Systemplatte, NAS und Sicherungsziele tragen seit 25.09.2026 je eine
|
||||
„vorhersage“ (kern/prognose.vorhersage). jetzt nur für Tests: Dann wird die Vorhersage nicht gemerkt."""
|
||||
b = _bericht()
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
return {"bericht": bool(b), "pools": pools(b), "systemplatte": systemplatte(b), "nas": nas(b),
|
||||
"sicherungen": sicherungen(b, jetzt), "empfehlungen": empfehlungen(b)}
|
||||
zeit = time.time() if jetzt is None else jetzt
|
||||
ergebnis = {"bericht": bool(b), "pools": pools(b), "systemplatte": systemplatte(b), "nas": nas(b),
|
||||
"sicherungen": sicherungen(b, zeit), "empfehlungen": empfehlungen(b)}
|
||||
for p in ergebnis["pools"]:
|
||||
p["vorhersage"] = pool_vorhersage(p["speicher"], jetzt)[0]
|
||||
if ergebnis["systemplatte"]:
|
||||
ergebnis["systemplatte"]["vorhersage"] = platten_vorhersage("pve", jetzt)
|
||||
for n in ergebnis["nas"]:
|
||||
n["vorhersage"] = None if n["problem"] else _vorhersage(f"nas:{n['ziel']}", jetzt)
|
||||
for z in ergebnis["sicherungen"]["ziele"]:
|
||||
z["vorhersage"] = _vorhersage(f"ziel:{z['speicher']}", jetzt)
|
||||
return ergebnis
|
||||
|
||||
|
||||
def pruefe_speicher(jetzt: float | None = None) -> list:
|
||||
"""Befunde für den Wächter. Ohne Bericht keine — das Schweigen des Ausführers meldet pruefe_ausfuehrer()."""
|
||||
"""Befunde für den Wächter. Ohne Bericht keine — das Schweigen des Ausführers meldet pruefe_ausfuehrer().
|
||||
Pool, Systemplatte und NAS melden sich auch nach dem Tempo (siehe oben); bestimmt die Vorhersage die Stufe, steht
|
||||
„in etwa N Tagen voll“ im Titel, sonst hängt der Text sie an."""
|
||||
from services.waechter import Befund # der Wächter registriert diese Prüfung, also erst hier
|
||||
b = _bericht()
|
||||
if not b:
|
||||
return []
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
zeit = time.time() if jetzt is None else jetzt
|
||||
befunde = []
|
||||
tipps = empfehlungen(b)
|
||||
platz_tipp = "; ".join([e["kurz"] for e in tipps if e.get("ort") == "pool"][:3])
|
||||
for p in pools(b):
|
||||
if p["stufe"] == "ok":
|
||||
v, meta = pool_vorhersage(p["speicher"], jetzt)
|
||||
stufe, kommend = prognose.schlimmer(p["stufe"], v)
|
||||
if stufe == "ok":
|
||||
continue
|
||||
anteil = max(p["anteil"], p["meta_anteil"] or 0)
|
||||
was = "Metadaten des Speicherpools" if (p["meta_anteil"] or 0) > p["anteil"] else "Speicherpool"
|
||||
befunde.append(Befund(
|
||||
id=f"pool:{p['speicher']}", stufe=p["stufe"],
|
||||
titel=f"{was} {p['speicher']} zu {min(100, round(anteil * 100))} % voll",
|
||||
text=(f"Noch {_groesse(p['frei'])} frei. Läuft er voll, stehen alle Gäste darauf still. "
|
||||
+ (f"Platz bringt: {platz_tipp}. " if platz_tipp else "")
|
||||
+ "Alles dazu im Homelab-Cockpit unter „Speicher und Sicherungen“."),
|
||||
quelle="pve"))
|
||||
rest = ("Läuft er voll, stehen alle Gäste darauf still. " + (f"Platz bringt: {platz_tipp}. " if platz_tipp else "")
|
||||
+ "Alles dazu im Homelab-Cockpit unter „Speicher und Sicherungen“.")
|
||||
if kommend:
|
||||
was = "Metadaten des Speicherpools" if meta else "Speicherpool"
|
||||
titel = f"{was} {p['speicher']} {prognose.in_tagen(v['tage'])} voll"
|
||||
text = f"{prognose.tempo(v)} Noch {_groesse(p['frei'])} frei. {rest}"
|
||||
else:
|
||||
anteil = max(p["anteil"], p["meta_anteil"] or 0)
|
||||
was = "Metadaten des Speicherpools" if (p["meta_anteil"] or 0) > p["anteil"] else "Speicherpool"
|
||||
titel = f"{was} {p['speicher']} zu {min(100, round(anteil * 100))} % voll"
|
||||
text = f"Noch {_groesse(p['frei'])} frei.{prognose.dazu(v)} {rest}"
|
||||
befunde.append(Befund(id=f"pool:{p['speicher']}", stufe=stufe, titel=titel, text=text, quelle="pve"))
|
||||
sp = systemplatte(b)
|
||||
if sp and sp["stufe"] != "ok":
|
||||
v = platten_vorhersage("pve", jetzt) if sp else None
|
||||
stufe, kommend = prognose.schlimmer(sp["stufe"], v) if sp else ("ok", False)
|
||||
if sp and stufe != "ok":
|
||||
kernel = next((e for e in tipps if e["art"] == "kernel"), None)
|
||||
befunde.append(Befund(
|
||||
id="host-platte", stufe=sp["stufe"],
|
||||
titel=f"Systemplatte des Proxmox-Hosts zu {min(100, round(sp['anteil'] * 100))} % voll",
|
||||
text=(f"Noch {_groesse(sp['frei'])} frei. Läuft sie voll, scheitern Updates und die Sicherungen vor Updates. "
|
||||
id="host-platte", stufe=stufe,
|
||||
titel=(f"Systemplatte des Proxmox-Hosts {prognose.in_tagen(v['tage'])} voll" if kommend
|
||||
else f"Systemplatte des Proxmox-Hosts zu {min(100, round(sp['anteil'] * 100))} % voll"),
|
||||
text=((f"{prognose.tempo(v)} " if kommend else "") + f"Noch {_groesse(sp['frei'])} frei."
|
||||
+ ("" if kommend else prognose.dazu(v))
|
||||
+ " Läuft sie voll, scheitern Updates und die Sicherungen vor Updates. "
|
||||
+ (f"Platz bringt: {kernel['kurz']} (Knopf im Homelab-Cockpit unter „Speicher und Sicherungen“)."
|
||||
if kernel else "Alte Sicherungen unter /var/lib/vz/dump und ISO-Abbilder prüfen.")),
|
||||
quelle="pve"))
|
||||
for n in nas(b):
|
||||
if n["stufe"] == "ok":
|
||||
v = None if n["problem"] else _vorhersage(f"nas:{n['ziel']}", jetzt)
|
||||
stufe, kommend = prognose.schlimmer(n["stufe"], v)
|
||||
if stufe == "ok":
|
||||
continue
|
||||
frei = _groesse((n["gesamt"] or 0) - (n["belegt"] or 0))
|
||||
platz = ("Die Sicherungen brauchen jede Nacht Platz: im PBS die Aufbewahrung kürzen (Datastore → Prune) oder "
|
||||
"alte Dateien auf dem NAS aufräumen.")
|
||||
if n["problem"]:
|
||||
titel, text = n["problem"], ("Die nächtlichen Sicherungen landen dort; bis das behoben ist, scheitern sie. "
|
||||
"Strom und Netz des NAS prüfen, dann in dessen Oberfläche nachsehen.")
|
||||
elif kommend:
|
||||
titel = f"NAS ({n['server']}) {prognose.in_tagen(v['tage'])} voll"
|
||||
text = f"{prognose.tempo(v)} Noch {frei} frei. {platz}"
|
||||
else:
|
||||
titel = f"NAS ({n['server']}) zu {min(100, round((n['anteil'] or 0) * 100))} % voll"
|
||||
text = (f"Noch {_groesse((n['gesamt'] or 0) - (n['belegt'] or 0))} frei. Die Sicherungen brauchen jede "
|
||||
"Nacht Platz: im PBS die Aufbewahrung kürzen (Datastore → Prune) oder alte Dateien auf dem NAS "
|
||||
"aufräumen.")
|
||||
befunde.append(Befund(id=f"nas:{n['ziel']}", stufe=n["stufe"], titel=titel, text=text, quelle="nas"))
|
||||
s = sicherungen(b, jetzt)
|
||||
text = f"Noch {frei} frei.{prognose.dazu(v)} {platz}"
|
||||
befunde.append(Befund(id=f"nas:{n['ziel']}", stufe=stufe, titel=titel, text=text, quelle="nas"))
|
||||
s = sicherungen(b, zeit)
|
||||
if s["stufe"] in ("gelb", "rot"):
|
||||
teile = list(s["probleme"])
|
||||
if s["fehlend"]:
|
||||
|
||||
@@ -25,7 +25,7 @@ from collections.abc import Callable
|
||||
|
||||
import psutil
|
||||
from config import MODELS_DIR
|
||||
from kern import messreihen
|
||||
from kern import messreihen, prognose
|
||||
|
||||
from services import system, token_stats
|
||||
|
||||
@@ -176,9 +176,11 @@ ZEITRAEUME = messreihen.ZEITRAEUME
|
||||
|
||||
|
||||
def abfrage(zeitraum: str, jetzt: float | None = None) -> dict:
|
||||
"""Für GET /api/messwerte: {"zeitraum", "schritt_s", "reihen": {…}, "aktuell": {…}}. aktuell ist der letzte
|
||||
Minutenpunkt, solange er höchstens AKTUELL_MAX_S alt ist, sonst lauter null."""
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
daten = messreihen.lesen(QUELLE, zeitraum, REIHEN, jetzt)
|
||||
letzter = messreihen.letzter_punkt(QUELLE, jetzt, AKTUELL_MAX_S) or {}
|
||||
return {**daten, "aktuell": {name: letzter.get(name) for name in REIHEN}}
|
||||
"""Für GET /api/messwerte: {"zeitraum", "schritt_s", "reihen": {…}, "aktuell": {…}, "prognose": {"platte": …}}.
|
||||
aktuell ist der letzte Minutenpunkt, solange er höchstens AKTUELL_MAX_S alt ist, sonst lauter null; prognose
|
||||
die Vorhersage „voll in etwa N Tagen“ des Modell-Laufwerks (kern/prognose.py, seit 25.09.2026)."""
|
||||
zeit = time.time() if jetzt is None else jetzt
|
||||
daten = messreihen.lesen(QUELLE, zeitraum, REIHEN, zeit)
|
||||
letzter = messreihen.letzter_punkt(QUELLE, zeit, AKTUELL_MAX_S) or {}
|
||||
return {**daten, "aktuell": {name: letzter.get(name) for name in REIHEN},
|
||||
"prognose": {"platte": prognose.aus_messreihe(QUELLE, "platte", jetzt=jetzt)}}
|
||||
|
||||
@@ -9,7 +9,7 @@ gesehen. Dieser Wächter schaut deshalb breiter hin:
|
||||
• Timer-Läufe Einmal-Dienste hinter Timern (projekte-sync, Sicherung)
|
||||
• Hermes-Jobs Status der Cron-Jobs + Werkzeugfehler im letzten Lauf (errors.log)
|
||||
• Kern Engine, Hirn, Hermes, Hör-Dienst, MC2, Gateway antworten per HTTP
|
||||
• Platte Füllstand des Modell-Laufwerks
|
||||
• Platte Füllstand des Modell-Laufwerks, seit 25.09.2026 auch das Tempo („in etwa N Tagen voll“)
|
||||
• Updates Bausteine, die der Sonntags-Lauf nach einem Fehlschlag festhält
|
||||
• Probe monatliche Probe-Wiederherstellung der Sicherung (rot oder zu alt = gelb)
|
||||
|
||||
@@ -44,7 +44,7 @@ from pathlib import Path
|
||||
import httpx
|
||||
import psutil
|
||||
from config import HERMES_API_KEY, HERMES_API_URL, HERMES_HOME, VOICE_SERVICE_URL
|
||||
from kern import partner
|
||||
from kern import partner, prognose
|
||||
from kern.einstellungen import einstellungen
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
@@ -454,19 +454,29 @@ def pruefe_kern() -> list[Befund]:
|
||||
|
||||
|
||||
def pruefe_platte() -> list[Befund]:
|
||||
"""Füllstand der eigenen Platte (gelb ab DISK_GELB_PCT, rot ab DISK_ROT_PCT). Auf der KI-Box seit 25.09.2026
|
||||
auch das Tempo des Modell-Laufwerks (kern/prognose.py): in weniger als 14 Tagen voll = gelb, unter 3 Tagen rot —
|
||||
als eigener Hinweis, wenn er ernster ist als der Füllstand."""
|
||||
befunde, stufe = [], "ok"
|
||||
try:
|
||||
pct = psutil.disk_usage(str(DATEN_DIR) if DATEN_DIR.exists() else os.getcwd()).percent
|
||||
except Exception:
|
||||
return []
|
||||
if pct >= DISK_ROT_PCT:
|
||||
stufe = "rot"
|
||||
elif pct >= DISK_GELB_PCT:
|
||||
stufe = "gelb"
|
||||
else:
|
||||
return []
|
||||
return [Befund(id="platte", stufe=stufe, titel=f"Die Platte ist zu {pct:.0f} % voll",
|
||||
text="Es wird eng für Modelle und Sicherungen. Alte Modelldateien löschen hilft am meisten.",
|
||||
quelle="platte", sofort=True)]
|
||||
pct = None
|
||||
if pct is not None and pct >= DISK_GELB_PCT:
|
||||
stufe = "rot" if pct >= DISK_ROT_PCT else "gelb"
|
||||
befunde.append(Befund(id="platte", stufe=stufe, titel=f"Die Platte ist zu {pct:.0f} % voll",
|
||||
text="Es wird eng für Modelle und Sicherungen. Alte Modelldateien löschen hilft am meisten.",
|
||||
quelle="platte", sofort=True))
|
||||
if ROLLE == "box":
|
||||
from services import messwerte
|
||||
v = prognose.aus_messreihe(messwerte.QUELLE, "platte")
|
||||
kuenftig, kommend = prognose.schlimmer(stufe, v)
|
||||
if kommend:
|
||||
befunde.append(Befund(
|
||||
id="platte:tempo", stufe=kuenftig, titel=f"Die Platte ist {prognose.in_tagen(v['tage'])} voll",
|
||||
text=f"{prognose.tempo(v)} Alte Modelldateien löschen hilft am meisten: KI-Box → Modelle → Aufräumen.",
|
||||
quelle="platte"))
|
||||
return befunde
|
||||
|
||||
|
||||
def pruefe_probe_wiederherstellung() -> list[Befund]:
|
||||
@@ -563,28 +573,38 @@ def _gb_komma(n: float) -> str:
|
||||
|
||||
def pruefe_gast_platten() -> list[Befund]:
|
||||
"""Homelab: Läuft die Platte eines Containers voll? Am 24.09.2026 stand AdGuard bei 100 %: Das Abfrageprotokoll
|
||||
schrieb nicht mehr, die Paketsuche scheiterte — und keine Anzeige sagte es."""
|
||||
from services.homelab import apps, kanal
|
||||
schrieb nicht mehr, die Paketsuche scheiterte — und keine Anzeige sagte es. Seit 25.09.2026 auch nach dem Tempo
|
||||
(kern/prognose.py): in weniger als 14 Tagen voll = gelb, unter 3 Tagen rot."""
|
||||
from services.homelab import apps, kanal, speicher
|
||||
daten = kanal.bericht()
|
||||
befunde = []
|
||||
for g in (daten or {}).get("bericht", {}).get("gaeste") or []:
|
||||
platte = g.get("platte") or {}
|
||||
gesamt, belegt = platte.get("gesamt"), platte.get("belegt") or 0
|
||||
if not gesamt or belegt / gesamt < GAST_PLATTE_GELB:
|
||||
if not gesamt:
|
||||
continue
|
||||
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
|
||||
name = app.name if app else str(g.get("name") or g["vmid"])
|
||||
anteil = belegt / gesamt
|
||||
# Seit 25.09.2026 kennt der Bericht auch die Platte der VMs (über den Gast-Agenten).
|
||||
vm = g.get("art") != "lxc"
|
||||
zid = f"{'vm' if vm else 'ct'}-{g['vmid']}"
|
||||
v = speicher.platten_vorhersage(zid)
|
||||
jetzt_stufe = "rot" if anteil >= GAST_PLATTE_ROT else "gelb" if anteil >= GAST_PLATTE_GELB else "ok"
|
||||
stufe, kommend = prognose.schlimmer(jetzt_stufe, v)
|
||||
if stufe == "ok":
|
||||
continue
|
||||
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
|
||||
name = app.name if app else str(g.get("name") or g["vmid"])
|
||||
vergroessern = ("VM → Hardware → Festplatte → Aktion → Größe ändern, danach in der VM das Dateisystem "
|
||||
"mitwachsen lassen" if vm else "Container → Ressourcen → Laufwerk → Größe ändern")
|
||||
befunde.append(Befund(
|
||||
id=f"gast-platte:{g['vmid']}", stufe="rot" if anteil >= GAST_PLATTE_ROT else "gelb",
|
||||
titel=f"Platte von {name} zu {min(100, round(anteil * 100))} % voll",
|
||||
text=(f"{_gb_komma(belegt)} von {_gb_komma(gesamt)} GB belegt ({'VM' if vm else 'Container'} {g['vmid']}). "
|
||||
f"Platz schaffen oder in Proxmox die Platte vergrößern: {vergroessern}."),
|
||||
quelle=f"{'vm' if vm else 'ct'}-{g['vmid']}"))
|
||||
id=f"gast-platte:{g['vmid']}", stufe=stufe,
|
||||
titel=(f"Platte von {name} {prognose.in_tagen(v['tage'])} voll" if kommend
|
||||
else f"Platte von {name} zu {min(100, round(anteil * 100))} % voll"),
|
||||
text=((f"{prognose.tempo(v)} " if kommend else "")
|
||||
+ f"{_gb_komma(belegt)} von {_gb_komma(gesamt)} GB belegt ({'VM' if vm else 'Container'} {g['vmid']})."
|
||||
+ ("" if kommend else prognose.dazu(v))
|
||||
+ f" Platz schaffen oder in Proxmox die Platte vergrößern: {vergroessern}."),
|
||||
quelle=zid))
|
||||
return befunde
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user