Files
mission-control-v2/backend/services/platten.py
T
HitonabiandClaude Opus 5.5 904598031a Später · 18 Plattengesundheit: Proxmox-Host, NAS über SNMP, NVMe der KI-Box
- kern/platten.py: SMART aus pvesh, smartctl und SNMP in einer Form; rot bei SMART FAILED, kritischer Warnung,
  aufgebrauchter Reserve, ≥ 90 % verbraucht; gelb bei ≥ 80 %, Hitze, gewachsenen Fehlerzählern (Ausgangslage je
  Platte, „Zur Kenntnis genommen“ setzt sie neu)
- Ausführer: stündlich disks/list + disks/smart im Bericht (nur lesend)
- services/homelab/nas_snmp.py: kleiner SNMPv2c-GetBulk-Leser ohne neue Abhängigkeit, QNAP hdTable/diskTable,
  Zugang 0600, Einstellungen „Verbindung testen und speichern“
- services/platten.py: NVMe der KI-Box über sudo smartctl (smartmontools am 26.09. nachinstalliert)
- Wächter beider Rollen, Plattenfeld in beiden Cockpits, Doku

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

90 lines
3.1 KiB
Python

"""Plattengesundheit der KI-Box (Ausbauplan „Später“, Punkt 18, seit 26.09.2026): smartctl (Paket smartmontools,
seit 26.09.2026 auf der Box) je Platte aus lsblk, höchstens stündlich, bewertet mit kern/platten.py. smartctl braucht
root (sudo -n); fehlt es oder verweigert sudo, gibt es schlicht keine Werte — nie einen Absturz (Windows, Tests).
"""
import json
import logging
import shutil
import subprocess
import threading
import time
from kern import platten as kern_platten
log = logging.getLogger(__name__)
CACHE_S = 3600
_lock = threading.Lock()
_cache: dict = {"zeit": 0.0, "platten": None}
def _laufen(befehl: list[str], zeitlimit: int = 30) -> tuple[int, str]:
try:
r = subprocess.run(befehl, capture_output=True, text=True, timeout=zeitlimit)
except (OSError, subprocess.SubprocessError):
return 127, ""
return r.returncode, r.stdout
def geraete() -> list[str]:
code, aus = _laufen(["lsblk", "-J", "-d", "-o", "NAME,TYPE"], 10)
try:
return [f"/dev/{d['name']}" for d in json.loads(aus).get("blockdevices") or [] if d.get("type") == "disk"
and not str(d.get("name", "")).startswith(("zram", "loop"))] if code == 0 else []
except (ValueError, AttributeError):
return []
def lesen() -> list[dict]:
if not shutil.which("sudo"): # Windows, Tests ohne sudo: keine Werte
return []
ergebnis = []
for geraet in geraete():
_, aus = _laufen(["sudo", "-n", "smartctl", "-j", "-a", geraet])
try:
daten = json.loads(aus)
except ValueError:
continue
# smartctl setzt Bits im Exit-Code auch für „Fehler im Protokoll“ — gelesen ist gelesen.
if not isinstance(daten, dict) or not (daten.get("model_name") or daten.get("nvme_smart_health_information_log")):
continue
ergebnis.append(kern_platten.aus_smartctl(daten, geraet, "KI-Box"))
return ergebnis
def platten() -> list[dict]:
with _lock:
if _cache["platten"] is not None and time.time() - _cache["zeit"] < CACHE_S:
return _cache["platten"]
werte = lesen()
with _lock:
_cache.update(zeit=time.time(), platten=werte)
return werte
def lage() -> dict:
"""Für GET /api/platten."""
return {"platten": kern_platten.bewertet(platten())}
def hinnehmen(pid: str) -> dict:
return kern_platten.hinnehmen(pid, platten())
def pruefe_platten_gesundheit() -> list:
"""Für den Wächter (Rolle box)."""
from services.waechter import Befund, _aktion
befunde = []
for p in lage()["platten"]:
if p["stufe"] == "ok":
continue
fehler = any("→" in s for s in p["saetze"])
befunde.append(Befund(
id=f"platte:{p['id']}", stufe=p["stufe"], titel=kern_platten.befund_titel(p),
text="; ".join(p["saetze"]) + ". Wichtiges sichern; wird es schlechter, die Platte ersetzen.",
quelle="platte", aktionen=[_aktion("hinnehmen", "Zur Kenntnis genommen", platte=p["id"])] if fehler else [],
sofort=True))
return befunde