homelab: Speicherpool, NAS und Sicherungen überwachen – mit Empfehlungen, was Platz bringt
User-Wunsch 25.09.: Speicherpool im Wächter (gelb ab 80 %, rot bei 10 % frei) samt Empfehlungen, dazu das NAS (Backup-Ziel, per NFS auf dem Proxmox-Host). - Ausführer: Thin-Pools mit echter Belegung je Volume (lvs), ungenutzte und verwaiste Platten, VM-Belegung über den Gast-Agenten, Discard je VM, Netzlaufwerke (df mit Zeitlimit, hängender harter NFS-Mount hält nichts auf), Sicherungsaufträge und jüngste Sicherung je Gast. - Homelab-Teil: services/homelab/speicher.py (Auswertung, Empfehlungen, Wächter pruefe_speicher, NAS-Erreichbarkeit per TCP), GET /api/homelab/speicher; Gast-Plattenwächter nennt jetzt auch VMs richtig. - Oberfläche: Karte „Speicher und Sicherungen“ im Homelab-Cockpit. - deploy.sh und ausfuehrer-einrichten.sh warten, solange im Homelab ein Update oder „Alle aktualisieren“ läuft (der Neustart bräche den Lauf ab). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9867affc5a
commit
903fbbfbb9
@@ -0,0 +1,312 @@
|
||||
"""Speicher und Sicherungen des Homelabs (seit 25.09.2026, User-Wunsch).
|
||||
|
||||
Alles aus dem Bericht des Ausführers (alle 10 Minuten), dazu eine eigene Prüfung, ob das NAS antwortet:
|
||||
• Speicherpool (Thin-LVM, heute local-lvm): Läuft er voll, stehen alle Gäste darauf still. Gelb ab 80 %, rot ab
|
||||
90 % — also bei 10 % frei (User-Vorgabe) —, für Daten wie Metadaten. Dazu Empfehlungen, was Platz bringt,
|
||||
der größte Gewinn zuerst: Platz, den ein Gast längst freigegeben hat (TRIM), ungenutzte und verwaiste Platten,
|
||||
Snapshots.
|
||||
• NAS: jedes Netzlaufwerk des Proxmox-Hosts (heute das QNAP per NFS, Ziel der nächtlichen Sicherungen): antwortet,
|
||||
ist eingebunden, hängt nicht, hat Platz (80/90 % wie oben).
|
||||
• Sicherungen: Für jeden Gast, den ein Sicherungsauftrag erfasst, liegt auf dessen Ziel eine frische Sicherung
|
||||
(täglicher Auftrag: höchstens 26 Stunden alt, sonst 8 Tage), und das Ziel ist erreichbar.
|
||||
Hinweise gehen über den Wächter (pruefe_speicher), die Oberfläche zeigt alles als Karte „Speicher und Sicherungen“ (lage).
|
||||
"""
|
||||
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services.homelab import apps, kanal
|
||||
|
||||
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
|
||||
TAEGLICH_S, SONST_S = 26 * 3600, 8 * 86400
|
||||
PORTS = {"nfs": 2049, "nfs4": 2049, "cifs": 445, "smb3": 445}
|
||||
ANTWORT_CACHE_S = 30
|
||||
RANG = {"ok": 0, "gelb": 1, "rot": 2}
|
||||
|
||||
_antwort_cache: dict[tuple[str, int], tuple[float, bool]] = {}
|
||||
_antwort_lock = threading.Lock()
|
||||
|
||||
|
||||
def _stufe(anteil: float | None) -> str:
|
||||
if anteil is None:
|
||||
return "ok"
|
||||
return "rot" if anteil >= ROT else "gelb" if anteil >= GELB else "ok"
|
||||
|
||||
|
||||
def _schlimmer(*stufen: str) -> str:
|
||||
return max(stufen, key=lambda s: RANG.get(s, 0))
|
||||
|
||||
|
||||
def _gb(n: float | None) -> str:
|
||||
return "–" if n is None else f"{n / 1e9:.1f}".replace(".", ",")
|
||||
|
||||
|
||||
def _groesse(n: float | None) -> str:
|
||||
"""„125 GB“, „2,3 TB“ — für Texte."""
|
||||
if n is None:
|
||||
return "–"
|
||||
if n >= 1e12:
|
||||
return f"{n / 1e12:.1f}".replace(".", ",") + " TB"
|
||||
return f"{n / 1e9:.0f} GB" if n >= 10e9 else f"{_gb(n)} GB"
|
||||
|
||||
|
||||
def _name(g: dict) -> str:
|
||||
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
|
||||
return app.name if app else str(g.get("name") or g["vmid"])
|
||||
|
||||
|
||||
def _wo(g: dict) -> str:
|
||||
return f"{'Container' if g.get('art') == 'lxc' else 'VM'} {g['vmid']}"
|
||||
|
||||
|
||||
def _zid(g: dict) -> str:
|
||||
return f"{'ct' if g.get('art') == 'lxc' else 'vm'}-{g['vmid']}"
|
||||
|
||||
|
||||
def _datum(ts: float) -> str:
|
||||
return datetime.fromtimestamp(ts, LOCAL_TZ).strftime("%d.%m. %H:%M")
|
||||
|
||||
|
||||
def _bericht() -> dict:
|
||||
return (kanal.bericht() or {}).get("bericht") or {}
|
||||
|
||||
|
||||
# --- Speicherpool ----------------------------------------------------------------------------------------------
|
||||
|
||||
def pools(bericht: dict) -> list[dict]:
|
||||
ergebnis = []
|
||||
for p in (bericht.get("host") or {}).get("pools") or []:
|
||||
gesamt, belegt = int(p.get("gesamt") or 0), int(p.get("belegt") or 0)
|
||||
if not gesamt:
|
||||
continue
|
||||
anteil, meta = belegt / gesamt, p.get("meta_anteil")
|
||||
ergebnis.append({"speicher": p.get("speicher"), "belegt": belegt, "gesamt": gesamt, "frei": gesamt - belegt,
|
||||
"anteil": anteil, "meta_anteil": meta, "aktiv": p.get("aktiv", True),
|
||||
"stufe": _schlimmer(_stufe(anteil), _stufe(meta))})
|
||||
return ergebnis
|
||||
|
||||
|
||||
def empfehlungen(bericht: dict) -> list[dict]:
|
||||
"""Was im Speicherpool Platz bringt, der größte Gewinn zuerst; Snapshots (Größe unbekannt) danach.
|
||||
Je Eintrag: art, titel, text (was zu tun ist, mit Klickweg), kurz (für Meldungen), platz (Bytes oder None)."""
|
||||
gaeste = bericht.get("gaeste") or []
|
||||
liste: list[dict] = []
|
||||
container: list[tuple[dict, int]] = []
|
||||
for g in gaeste:
|
||||
platte, im_pool = g.get("platte") or {}, g.get("pool_belegt")
|
||||
if im_pool and platte.get("belegt") is not None:
|
||||
gewinn = int(im_pool) - int(platte["belegt"])
|
||||
if g.get("art") == "lxc":
|
||||
if gewinn >= TRIM_AB:
|
||||
container.append((g, gewinn))
|
||||
elif gewinn >= EMPFEHLUNG_AB:
|
||||
liste.append(_trim_vm(g, int(im_pool), int(platte["belegt"]), gewinn))
|
||||
for u in g.get("ungenutzt") or []:
|
||||
liste.append({
|
||||
"art": "ungenutzt", "platz": u.get("belegt"), "titel": f"{_name(g)}: ungenutzte Platte {u['volume']}",
|
||||
"kurz": f"ungenutzte Platte bei {_name(g)}",
|
||||
"text": (f"Hängt an keinem Laufwerk mehr. Wird sie nicht mehr gebraucht: in Proxmox {_wo(g)} → Hardware "
|
||||
"→ „Unbenutzte Festplatte“ → Entfernen (löscht sie endgültig).")})
|
||||
if (summe := sum(x for _, x in container)) >= EMPFEHLUNG_AB:
|
||||
container.sort(key=lambda t: -t[1])
|
||||
einzeln = ", ".join(f"{_name(g)} {_gb(x)} GB" for g, x in container)
|
||||
befehle = "; ".join(f"pct fstrim {g['vmid']} --ignore-mountpoints" for g, _ in container)
|
||||
liste.append({
|
||||
"art": "trim", "platz": summe, "titel": "Container: freigegebenen Platz an den Pool zurückgeben",
|
||||
"kurz": f"{_groesse(summe)} per TRIM in den Containern",
|
||||
"text": (f"Die Container belegen im Pool {_gb(summe)} GB mehr, als sie nutzen ({einzeln}). In Proxmox: pve → "
|
||||
f"Shell → {befehle} — im laufenden Betrieb und ohne Verlust.")})
|
||||
for p in (bericht.get("host") or {}).get("pools") or []:
|
||||
for v in p.get("verwaist") or []:
|
||||
liste.append({
|
||||
"art": "verwaist", "platz": v.get("belegt"), "titel": f"Verwaiste Platte {v['volume']}",
|
||||
"kurz": f"verwaiste Platte {v['volume']}",
|
||||
"text": (f"Gehört zu keinem Gast mehr. Wird sie nicht mehr gebraucht: in Proxmox pve → {p.get('speicher')} "
|
||||
"→ Datenträger → Entfernen (löscht sie endgültig).")})
|
||||
liste.sort(key=lambda e: -(e["platz"] or 0))
|
||||
for g in gaeste:
|
||||
for s in g.get("snapshots") or []:
|
||||
m = re.fullmatch(r"mc2-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})\d{2}", s)
|
||||
wann = f"vom {m.group(3)}.{m.group(2)}. {m.group(4)}:{m.group(5)}" if m else "von Hand angelegt"
|
||||
liste.append({
|
||||
"art": "snapshot", "platz": None, "titel": f"{_name(g)}: Snapshot {s}", "kurz": f"Snapshot bei {_name(g)}",
|
||||
"text": ((f"Rückweg des Updates {wann}. Läuft {_name(g)} seitdem gut, kann er weg" if m else
|
||||
f"Snapshot {wann}. Wird er nicht mehr gebraucht, kann er weg")
|
||||
+ f": in Proxmox {_wo(g)} → Snapshots → {s} → Entfernen. Wie viel er belegt, sagt Proxmox "
|
||||
"nicht — bei Containern meist wenig, er wächst mit jeder Änderung seitdem.")})
|
||||
return liste
|
||||
|
||||
|
||||
def _trim_vm(g: dict, im_pool: int, genutzt: int, gewinn: int) -> dict:
|
||||
kopf = f"Im Pool belegt die VM {_gb(im_pool)} GB, genutzt sind darin {_gb(genutzt)} GB."
|
||||
if g.get("discard"):
|
||||
text = (f"{kopf} In der VM „sudo fstrim -av“ ausführen; Linux-Gäste tun das meist ohnehin jede Woche "
|
||||
"(fstrim.timer).")
|
||||
else:
|
||||
text = (f"{kopf} Discard ist aus, deshalb kommt Platz, den die VM freigibt, nie im Pool an. In Proxmox: "
|
||||
f"VM {g['vmid']} → Hardware → Festplatte → Bearbeiten → „Discard“ anhaken, dann die VM einmal neu "
|
||||
"starten (die Dienste darin sind kurz weg). Danach gibt sie freien Platz jede Woche von selbst zurück; "
|
||||
"sofort mit „sudo fstrim -av“ in der VM.")
|
||||
return {"art": "trim-vm", "platz": gewinn, "titel": f"{_name(g)}: {_groesse(gewinn)} an den Pool zurückgeben",
|
||||
"kurz": f"{_groesse(gewinn)} bei {_name(g)} per TRIM" + ("" if g.get("discard") else " (Discard einschalten)"),
|
||||
"text": text}
|
||||
|
||||
|
||||
# --- NAS --------------------------------------------------------------------------------------------------------
|
||||
|
||||
def antwortet(server: str, port: int) -> bool:
|
||||
"""Nimmt das NAS auf dem Port seines Dienstes Verbindungen an? (30 s gemerkt: Wächter und Oberfläche fragen oft.)"""
|
||||
jetzt = time.monotonic()
|
||||
with _antwort_lock:
|
||||
if (e := _antwort_cache.get((server, port))) and jetzt - e[0] < ANTWORT_CACHE_S:
|
||||
return e[1]
|
||||
try:
|
||||
with socket.create_connection((server, port), timeout=3):
|
||||
ok = True
|
||||
except OSError:
|
||||
ok = False
|
||||
with _antwort_lock:
|
||||
_antwort_cache[(server, port)] = (jetzt, ok)
|
||||
return ok
|
||||
|
||||
|
||||
def nas(bericht: dict, pruefen=None) -> list[dict]:
|
||||
pruefen = pruefen or antwortet
|
||||
ergebnis = []
|
||||
for m in (bericht.get("host") or {}).get("netzlaufwerke") or []:
|
||||
server, port = m.get("server"), PORTS.get(str(m.get("art")))
|
||||
erreichbar = pruefen(server, port) if server and port else None
|
||||
platz = m.get("platz") or {}
|
||||
gesamt, belegt = platz.get("gesamt"), platz.get("belegt")
|
||||
anteil = belegt / gesamt if gesamt and belegt is not None else None
|
||||
eintrag = {"server": server, "ziel": m.get("ziel"), "quelle": m.get("quelle"), "art": m.get("art"),
|
||||
"eingebunden": bool(m.get("eingebunden")), "zustand": m.get("zustand"), "erreichbar": erreichbar,
|
||||
"belegt": belegt, "gesamt": gesamt, "anteil": anteil, "stufe": "ok", "problem": None}
|
||||
if erreichbar is False:
|
||||
eintrag.update(stufe="rot", problem=f"Das NAS ({server}) antwortet nicht.")
|
||||
elif not m.get("eingebunden"):
|
||||
eintrag.update(stufe="rot", problem=f"{m.get('ziel')} ist auf dem Proxmox-Host nicht eingebunden.")
|
||||
elif m.get("zustand") == "haengt":
|
||||
eintrag.update(stufe="rot", problem=f"Die Einbindung {m.get('ziel')} hängt: Der Proxmox-Host bekommt keine "
|
||||
"Antwort vom NAS.")
|
||||
elif m.get("zustand") == "fehler":
|
||||
eintrag.update(stufe="gelb", problem=f"Die Belegung von {m.get('ziel')} ließ sich nicht lesen.")
|
||||
else:
|
||||
eintrag["stufe"] = _stufe(anteil)
|
||||
ergebnis.append(eintrag)
|
||||
return ergebnis
|
||||
|
||||
|
||||
# --- Sicherungen ------------------------------------------------------------------------------------------------
|
||||
|
||||
def max_alter_s(zeitplan: object) -> int:
|
||||
"""Wie alt die jüngste Sicherung höchstens sein darf: täglich (nur Uhrzeit, etwa „02:30“) 26 Stunden, bei
|
||||
Wochentagen oder allem Ausgefalleneren 8 Tage."""
|
||||
return TAEGLICH_S if re.fullmatch(r"\s*(\d{1,2}|\*)(:\d{2})?\s*", str(zeitplan or "")) else SONST_S
|
||||
|
||||
|
||||
def _erfasst(job: dict, vmid: int) -> bool:
|
||||
"""Sichert dieser Auftrag den Gast? (alle außer den ausgenommenen, oder die aufgezählten)"""
|
||||
if vmid in (job.get("ausgenommen") or []):
|
||||
return False
|
||||
return bool(job.get("alle")) or vmid in (job.get("vmids") or [])
|
||||
|
||||
|
||||
def sicherungen(bericht: dict, jetzt: float) -> dict:
|
||||
host = bericht.get("host") or {}
|
||||
jobs = [j for j in host.get("sicherung_jobs") or [] if j.get("an")]
|
||||
ziele = host.get("sicherungsziele") or {}
|
||||
probleme: list[str] = []
|
||||
for name in sorted({j.get("speicher") for j in jobs if j.get("speicher")}):
|
||||
z = ziele.get(name) or {}
|
||||
if not z.get("aktiv"):
|
||||
probleme.append(f"Das Sicherungsziel {name} ist nicht erreichbar.")
|
||||
elif z.get("fehler"):
|
||||
probleme.append(f"Die Sicherungen auf {name} ließen sich nicht lesen.")
|
||||
erfasst, fehlend, zuletzt = 0, [], None
|
||||
for g in bericht.get("gaeste") or []:
|
||||
eigene = [j for j in jobs if _erfasst(j, g["vmid"])]
|
||||
if not eigene:
|
||||
continue
|
||||
erfasst += 1
|
||||
stand = [((ziele.get(j.get("speicher")) or {}).get("neueste") or {}).get(str(g["vmid"])) for j in eigene]
|
||||
juengste = max((t for t in stand if t), default=None)
|
||||
if juengste:
|
||||
zuletzt = max(zuletzt or 0, juengste)
|
||||
if not any(t and jetzt - t <= max_alter_s(j.get("zeitplan")) for t, j in zip(stand, eigene, strict=True)):
|
||||
fehlend.append({"ziel": _zid(g), "name": _name(g), "zuletzt": juengste})
|
||||
stufe = "keine" if not jobs else "ok"
|
||||
if fehlend:
|
||||
stufe = "rot" if len(fehlend) == erfasst else "gelb"
|
||||
if probleme:
|
||||
stufe = "rot"
|
||||
return {"jobs": [{"zeitplan": j.get("zeitplan"), "speicher": j.get("speicher"), "alle": j.get("alle")} for j in jobs],
|
||||
"ziele": [{"speicher": n, "art": z.get("art"), "aktiv": z.get("aktiv"), "belegt": z.get("belegt"),
|
||||
"gesamt": z.get("gesamt")} for n, z in sorted(ziele.items())],
|
||||
"erfasst": erfasst, "fehlend": fehlend, "zuletzt": zuletzt, "probleme": probleme, "stufe": stufe}
|
||||
|
||||
|
||||
# --- Für Oberfläche und Wächter ----------------------------------------------------------------------------------
|
||||
|
||||
def lage(jetzt: float | None = None) -> dict:
|
||||
b = _bericht()
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
return {"bericht": bool(b), "pools": pools(b), "nas": nas(b), "sicherungen": sicherungen(b, jetzt),
|
||||
"empfehlungen": empfehlungen(b)}
|
||||
|
||||
|
||||
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()."""
|
||||
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
|
||||
befunde = []
|
||||
tipps = empfehlungen(b)
|
||||
platz_tipp = "; ".join(e["kurz"] for e in tipps[:3])
|
||||
for p in pools(b):
|
||||
if p["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"))
|
||||
for n in nas(b):
|
||||
if n["stufe"] == "ok":
|
||||
continue
|
||||
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.")
|
||||
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)
|
||||
if s["stufe"] in ("gelb", "rot"):
|
||||
teile = list(s["probleme"])
|
||||
if s["fehlend"]:
|
||||
namen = ", ".join(f"{f['name']} ({'zuletzt ' + _datum(f['zuletzt']) if f['zuletzt'] else 'noch nie'})"
|
||||
for f in s["fehlend"][:6])
|
||||
teile.append(f"Keine frische Sicherung: {namen}.")
|
||||
befunde.append(Befund(
|
||||
id="sicherungen", stufe=s["stufe"],
|
||||
titel=("Sicherungen scheitern" if s["stufe"] == "rot" else f"Sicherung fehlt bei {len(s['fehlend'])} Gerät"
|
||||
+ ("en" if len(s["fehlend"]) != 1 else "")),
|
||||
text=" ".join(teile) + " In Proxmox: Rechenzentrum → Backup und die Aufgaben-Liste unten zeigen, woran es lag.",
|
||||
quelle="pve"))
|
||||
return befunde
|
||||
@@ -580,12 +580,16 @@ def pruefe_gast_platten() -> list[Befund]:
|
||||
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"
|
||||
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 (Container {g['vmid']}). Platz schaffen "
|
||||
"oder in Proxmox die Platte vergrößern: Container → Ressourcen → Laufwerk → Größe ändern."),
|
||||
quelle=f"ct-{g['vmid']}"))
|
||||
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']}"))
|
||||
return befunde
|
||||
|
||||
|
||||
@@ -599,6 +603,8 @@ PRUEFUNGEN = {
|
||||
PRUEFUNGEN += (__import__("services.homelab.pflege").homelab.pflege.pruefe_paketlisten,) if ROLLE == "homelab" else ()
|
||||
# Messwerte des Proxmox-Hosts (seit 24.09.2026): zehn Minuten über 95 % RAM oder 90 °C = gelb.
|
||||
PRUEFUNGEN += (__import__("services.homelab.messwerte").homelab.messwerte.pruefe_host,) if ROLLE == "homelab" else ()
|
||||
# Speicherpool, NAS und Sicherungen (seit 25.09.2026): gelb ab 80 %, rot bei 10 % frei; Sicherung älter als erlaubt.
|
||||
PRUEFUNGEN += (__import__("services.homelab.speicher").homelab.speicher.pruefe_speicher,) if ROLLE == "homelab" else ()
|
||||
BETREFF_PROBLEM, BETREFF_OK = {"box": ("[Box-Problem]", "[Box wieder ok]"),
|
||||
"homelab": ("[Homelab-Problem]", "[Homelab wieder ok]")}[ROLLE]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user