Merge branch 'worktree-agent-a1c799645a0ade657' into wartung/restarbeiten
This commit is contained in:
@@ -11,6 +11,7 @@ gesehen. Dieser Wächter schaut deshalb breiter hin:
|
||||
• Kern Engine, Hirn, Hermes, Hör-Dienst, MC2, Gateway antworten per HTTP
|
||||
• Platte Füllstand des Modell-Laufwerks
|
||||
• Updates Bausteine, die der Sonntags-Lauf nach einem Fehlschlag festhält
|
||||
• Probe monatliche Probe-Wiederherstellung der Sicherung (rot oder zu alt = gelb)
|
||||
|
||||
Jeder Befund wird zum Hinweis mit Stufe (rot = jetzt, gelb = bei Gelegenheit), Beginn und
|
||||
Knöpfen. Einfaches behebt er selbst (User-Entscheid 23.09.): Ein ABGESTÜRZTER Dienst
|
||||
@@ -45,8 +46,9 @@ import psutil
|
||||
from config import HERMES_API_KEY, HERMES_API_URL, HERMES_HOME, VOICE_SERVICE_URL
|
||||
from kern import partner
|
||||
from kern.einstellungen import einstellungen
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services import announce, llamaswap, maintenance, update_verlauf
|
||||
from services import announce, llamaswap, maintenance, probe_wiederherstellung, update_verlauf
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
ROLLE = einstellungen().rolle
|
||||
@@ -98,6 +100,9 @@ TIMER_DIENSTE: dict[str, tuple[str, bool]] = {
|
||||
"mc2-morgenmeldung": ("Morgenmeldung", True),
|
||||
# Seit 24.09.2026 ein eigener Timer statt Hermes-Cron (Hermes startet sich beim Update selbst neu).
|
||||
"mc2-autoupdate": ("Updates am Sonntag", True),
|
||||
# Seit 24.09.2026 im Repo (vom 21.08. an aus, weil sie rot lief und niemand es sah). Gelb: Die tägliche
|
||||
# Sicherung (mc2-backup) bleibt die erste Schicht, der PBS ist die Kopie auf dem QNAP. Ausgeschaltet kein Befund.
|
||||
"pbs-backup": ("Sicherung auf den PBS", False),
|
||||
}
|
||||
|
||||
HERMES_JOBS_PATH = HERMES_HOME / "cron" / "jobs.json"
|
||||
@@ -386,17 +391,52 @@ def _hermes_kopf() -> dict[str, str]:
|
||||
return {"Authorization": f"Bearer {HERMES_API_KEY}"} if HERMES_API_KEY else {}
|
||||
|
||||
|
||||
# Lucys Hirn gezielt (seit 24.09.2026): Ein Ladevorgang ist kein Ausfall — aber nur bis zu dieser Frist. llama-swap
|
||||
# bricht einen Start nach healthCheckTimeout (300 s) selbst ab; wer danach immer noch „lädt“, hängt oder startet
|
||||
# immer wieder neu (dann wechseln sich „lädt“ und „fehlt“ ab, und ohne Frist käme nie ein Hinweis zustande).
|
||||
HIRN_LADEN_MAX_S = int(os.environ.get("MC_WAECHTER_HIRN_LADEN_S", "600"))
|
||||
_hirn: dict[str, float | None] = {"fehlt_seit": None, "geprueft": None}
|
||||
|
||||
|
||||
def _hirn_befund(jetzt: float | None = None) -> Befund | None:
|
||||
"""Ist Lucys Hirn (Rolle hermes) geladen? Bis 24.09.2026 galt es als bereit, sobald irgendein Modell lief —
|
||||
ein abgestürztes Hirn neben einem geladenen Coder blieb so unbemerkt. Lädt es gerade, ist das kein Befund;
|
||||
alles andere zählt wie jeder Befund erst nach FAIL_AFTER Takten."""
|
||||
jetzt = time.monotonic() if jetzt is None else jetzt
|
||||
zuletzt, _hirn["geprueft"] = _hirn["geprueft"], jetzt
|
||||
if zuletzt is None or jetzt - zuletzt > 5 * 60:
|
||||
_hirn["fehlt_seit"] = None # lange nicht geprüft (Update, Motor weg): die Frist beginnt neu
|
||||
st = llamaswap.hirn_zustand()
|
||||
name, zustand = st.get("modell"), st.get("zustand")
|
||||
if zustand == "bereit":
|
||||
_hirn["fehlt_seit"] = None
|
||||
return None
|
||||
if _hirn["fehlt_seit"] is None:
|
||||
_hirn["fehlt_seit"] = jetzt
|
||||
dauer = jetzt - _hirn["fehlt_seit"]
|
||||
if zustand == "laedt" and dauer < HIRN_LADEN_MAX_S:
|
||||
return None
|
||||
if not name:
|
||||
text = "Kein Modell trägt die Rolle „hermes“. Ohne sie hat Lucy kein Hirn; die Rolle vergibt die Modelle-Seite."
|
||||
elif zustand == "laedt":
|
||||
text = (f"{name} lädt seit über {int(dauer // 60)} Minuten und wird nicht fertig. Vermutlich startet es immer "
|
||||
"wieder neu; das Protokoll des Motors sagt, warum.")
|
||||
elif zustand == "unbekannt":
|
||||
text = "Der Motor sagt nicht, welche Modelle laufen (llama-swap /running antwortet nicht)."
|
||||
else:
|
||||
text = f"{name} läuft nicht. Der Re-Warm-Wächter lädt es nach; das Protokoll des Motors sagt, warum es fehlt."
|
||||
return Befund(id="kern:hirn", stufe="rot",
|
||||
titel="Lucys Hirn lädt nicht fertig" if zustand == "laedt" else "Lucys Hirn ist nicht geladen",
|
||||
text=text, quelle="hirn", aktionen=[_aktion("protokoll", "Protokoll des Motors", dienst="llama-swap")])
|
||||
|
||||
|
||||
def pruefe_kern() -> list[Befund]:
|
||||
"""HTTP-Proben: läuft der Dienst UND antwortet er? (Der Dienst-Check sieht nur systemd.)"""
|
||||
proben: list[tuple[str, str, str, bool]] = [] # (id, Titel, Text, ok)
|
||||
engine_ok = llamaswap.engine_reachable()
|
||||
proben.append(("kern:engine", "Der Motor antwortet nicht",
|
||||
"llama-swap reagiert nicht. Ohne ihn laufen keine Modelle.", engine_ok))
|
||||
if engine_ok:
|
||||
st = llamaswap.brain_status()
|
||||
hirn_ok = bool(st.get("ready")) or bool(llamaswap.get_running_models())
|
||||
proben.append(("kern:hirn", "Lucys Hirn ist nicht geladen",
|
||||
"Das Hirn-Modell lädt nicht. Der Re-Warm-Wächter versucht es weiter.", hirn_ok))
|
||||
hirn = _hirn_befund() if engine_ok else None
|
||||
proben.append(("kern:hermes", "Hermes antwortet nicht",
|
||||
"Der Agent-Dienst reagiert nicht. Telegram und Lucys Werkzeuge gehen gerade nicht.",
|
||||
_reach(HERMES_API_URL, "/v1/models", _hermes_kopf())))
|
||||
@@ -409,8 +449,9 @@ def pruefe_kern() -> list[Befund]:
|
||||
"Die Oberfläche und Lucys Sprach-Schnittstellen hängen.", _reach(MC2_URL, "/api/health")))
|
||||
proben.append(("kern:gateway", "Der Modell-Gateway antwortet nicht",
|
||||
"Anfragen von Lucy und OpenChamber an die Modelle hängen.", _reach(GATEWAY_URL, "/gw/health")))
|
||||
return [Befund(id=i, stufe="rot", titel=t, text=x, quelle=i.split(":", 1)[1])
|
||||
for (i, t, x, ok) in proben if not ok]
|
||||
befunde = [Befund(id=i, stufe="rot", titel=t, text=x, quelle=i.split(":", 1)[1])
|
||||
for (i, t, x, ok) in proben if not ok]
|
||||
return befunde + ([hirn] if hirn else [])
|
||||
|
||||
|
||||
def pruefe_platte() -> list[Befund]:
|
||||
@@ -429,6 +470,39 @@ def pruefe_platte() -> list[Befund]:
|
||||
quelle="platte", sofort=True)]
|
||||
|
||||
|
||||
def pruefe_probe_wiederherstellung() -> list[Befund]:
|
||||
"""Monatliche Probe-Wiederherstellung (services/probe_wiederherstellung.py, seit 24.09.2026): gelb, wenn die
|
||||
letzte Probe rot war oder älter als WARN_TAGE ist. Gab es noch keine, erst, wenn ihr Timer eingerichtet ist."""
|
||||
einheit = probe_wiederherstellung.EINHEIT
|
||||
aktionen = [_aktion("neustart", "Jetzt prüfen", dienst=einheit), _aktion("protokoll", "Protokoll", dienst=einheit)]
|
||||
stand = probe_wiederherstellung.lese_stand()
|
||||
if stand is None:
|
||||
z = _systemctl_show(f"{einheit}.timer", False)
|
||||
if not z or z.get("LoadState") in ("not-found", ""):
|
||||
return []
|
||||
return [Befund(id="probe:wiederherstellung", stufe="gelb",
|
||||
titel="Die Sicherung wurde noch nie probeweise ausgepackt",
|
||||
text=("Die Probe läuft am ersten Montag im Monat um 05:15. „Jetzt prüfen“ startet sie sofort; "
|
||||
"sie braucht Sekunden und fasst nichts Lebendes an."),
|
||||
quelle=einheit, aktionen=aktionen, sofort=True)]
|
||||
datum = datetime.fromtimestamp(float(stand["zeit"]), LOCAL_TZ).strftime("%d.%m.%Y")
|
||||
if stand.get("ergebnis") != "gruen":
|
||||
fehler = [str(f) for f in stand.get("fehler") or []] or ["ohne Begründung"]
|
||||
weitere = f" (und {len(fehler) - 1} weitere)" if len(fehler) > 1 else ""
|
||||
return [Befund(id="probe:wiederherstellung", stufe="gelb",
|
||||
titel="Die letzte Probe-Wiederherstellung war rot",
|
||||
text=f"Probe vom {datum} ({stand.get('sicherung') or 'keine Sicherung'}): {fehler[0]}{weitere}"[:400],
|
||||
quelle=einheit, aktionen=aktionen, sofort=True)]
|
||||
tage = (time.time() - float(stand["zeit"])) / 86400
|
||||
if tage > probe_wiederherstellung.WARN_TAGE:
|
||||
return [Befund(id="probe:wiederherstellung", stufe="gelb",
|
||||
titel=f"Die Sicherung wurde seit {int(tage)} Tagen nicht mehr probeweise ausgepackt",
|
||||
text=(f"Die letzte Probe am {datum} war grün. Sie soll am ersten Montag im Monat laufen — "
|
||||
f"ist {einheit}.timer eingeschaltet?"),
|
||||
quelle=einheit, aktionen=aktionen, sofort=True)]
|
||||
return []
|
||||
|
||||
|
||||
def pruefe_festgehalten() -> list[Befund]:
|
||||
"""Festgehaltene Bausteine (Pin-Register von autoupdate.sh). Vom 06. bis 17.09.2026 hielt
|
||||
die Box Motor und Hermes fest, ohne dass es jemand sah — elf Tage ohne Updates."""
|
||||
@@ -484,7 +558,7 @@ def pruefe_gaeste() -> list[Befund]:
|
||||
# den Ausführer auf dem Proxmox-Host und seine Gäste.
|
||||
PRUEFUNGEN = {
|
||||
"box": (pruefe_dienste, pruefe_timer_dienste, pruefe_hermes_jobs, pruefe_kern, pruefe_platte,
|
||||
pruefe_festgehalten, pruefe_partner),
|
||||
pruefe_festgehalten, pruefe_partner, pruefe_probe_wiederherstellung),
|
||||
"homelab": (pruefe_platte, pruefe_partner, pruefe_ausfuehrer, pruefe_gaeste),
|
||||
}[ROLLE]
|
||||
PRUEFUNGEN += (__import__("services.homelab.pflege").homelab.pflege.pruefe_paketlisten,) if ROLLE == "homelab" else ()
|
||||
|
||||
Reference in New Issue
Block a user