Files
mission-control-v2/backend/tests/test_homelab_karenz.py
T
HitonabiandClaude Opus 5.5 f2733f44fe homelab: Wartezeit nach Skriptaenderung, PBS mit Sicherung statt Snapshot, woechentliches Suchen
- karenz.py: ct/<app>.sh juenger als MC_HOMELAB_KARENZ_H (Standard 48 h) -> Baustein "neu" ohne Knopf,
  updates.starten lehnt mit demselben Satz ab (Berliner Zeit). GitHub stumm -> nicht blockieren, die
  Rueckfrage sagt es.
- Ausfuehrer: neue Aktionen sichern, sicherung_zurueck, sicherung_loeschen. vzdump auf den ersten lokalen
  Speicher mit Inhalt backup (oder sicherung_speicher aus /etc/mc2-ausfuehrer.json), nie auf pbs; vorher
  Platz pruefen (frei > belegt x 1,2); Notiz mc2-sicherung, nur solche werden zurueckgespielt/geloescht;
  Rueckweg: stoppen, pct restore --force auf den bisherigen rootfs-Speicher, starten. Bericht mit
  host.sicherung, sicherung_moeglich/_grund, eigenen Sicherungen und nur_lesen. Unerwartete Fehler werden
  beantwortet statt verschluckt; vzdump/restore beim Zeitlimit erst SIGTERM.
- updates.py: Sicherung, wo kein Snapshot geht; bei Rot zurueckspielen, nach Gruen aeltere Sicherungen
  weg. Scheitert Snapshot oder Sicherung, beginnt das Update nicht (nicht dringend gemeldet).
- pflege.py: "suchen" fuer Gaeste mit Paketlisten aelter als 7 Tage, nachts 02-05 Uhr (sonst nachholen),
  einmal je Gast und Tag, nie neben einem Update oder offenen Auftrag; gelber Waechter-Hinweis, wenn es
  zweimal hintereinander scheitert. Eine Registrierungszeile in waechter.py.
- kanal.py: Auftragsliste unter flock, weil jetzt auch der Steward Auftraege anlegt.
- inventar.py: Rueckweg und Rueckfrage fuer die Sicherung; Gaeste mitten im Update-Lauf nicht in der
  Webpruefung des Waechters (sonst zweiter Alarm beim Zurueckspielen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:22:39 +02:00

111 lines
5.5 KiB
Python

"""Wartezeit nach einer Änderung am Update-Skript (24.09.2026): ct/<kennung>.sh kommt ungepinnt von GitHub und
läuft als root im Gast — frisch Geändertes erst nach MC_HOMELAB_KARENZ_H Stunden."""
import json
from datetime import datetime
from pathlib import Path
import pytest
from kern import einstellungen as einstellungen_mod
from kern.zeit import LOCAL_TZ
from services.homelab import inventar, kanal, karenz, updates
BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8"))
# 23.09.2026 14:05 in Berlin = 12:05 UTC, so wie GitHub es schreibt.
GEAENDERT = "2026-09-23T12:05:11Z"
GEAENDERT_TS = datetime(2026, 9, 23, 14, 5, 11, tzinfo=LOCAL_TZ).timestamp()
def _github(datum: str = GEAENDERT, pfade: list | None = None):
def antwort(pfad, timeout=8):
if pfade is not None:
pfade.append(pfad)
return [{"sha": "abc", "commit": {"author": {"date": "2026-09-01T08:00:00Z"},
"committer": {"date": datum}}}]
return antwort
def test_frische_aenderung_sperrt_mit_berliner_zeiten(monkeypatch):
pfade: list[str] = []
monkeypatch.setattr(karenz, "github_json", _github(pfade=pfade))
monkeypatch.delenv("MC_HOMELAB_KARENZ_H", raising=False)
stand = karenz.pruefen("gitea", jetzt=GEAENDERT_TS + 10 * 3600)
assert stand == {"gesperrt": "Das Update-Skript wurde am 23.09. geändert; zur Sicherheit erst ab 25.09. 14:05.",
"geprueft": True}
# Das Datum, an dem die Änderung auf main landete (committer), nicht das des Autors.
assert pfade == ["repos/community-scripts/ProxmoxVE/commits?path=ct/gitea.sh&sha=main&per_page=1"]
assert karenz.pruefen("gitea", jetzt=GEAENDERT_TS + 48 * 3600) == {"gesperrt": None, "geprueft": True}
def test_wartezeit_einstellbar(monkeypatch):
monkeypatch.setattr(karenz, "github_json", _github())
monkeypatch.setenv("MC_HOMELAB_KARENZ_H", "0")
assert karenz.pruefen("gitea", jetzt=GEAENDERT_TS + 60)["gesperrt"] is None
monkeypatch.setenv("MC_HOMELAB_KARENZ_H", "72")
assert "erst ab 26.09. 14:05" in karenz.pruefen("gitea", jetzt=GEAENDERT_TS + 50 * 3600)["gesperrt"]
monkeypatch.setenv("MC_HOMELAB_KARENZ_H", "viel")
assert karenz.karenz_s() == 48 * 3600
def test_ohne_antwort_von_github_nicht_blockieren(monkeypatch):
def kaputt(pfad, timeout=8):
raise RuntimeError("403 rate limit")
monkeypatch.setattr(karenz, "github_json", kaputt)
assert karenz.pruefen("gitea") == {"gesperrt": None, "geprueft": False}
monkeypatch.setattr(karenz, "github_json", lambda pfad, timeout=8: []) # Skript gibt es nicht
assert karenz.pruefen("gitea")["geprueft"] is False
assert karenz.pruefen("../../etc")["geprueft"] is False # fremde Kennung: keine Abfrage
# --- Übersicht und Knopf -------------------------------------------------------------------------
@pytest.fixture
def gitea_neu(tmp_path, monkeypatch):
"""Gitea 1.27.2 installiert, 1.27.3 verfügbar (der Stand vor dem ersten echten Lauf am 24.09.)."""
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
monkeypatch.setattr(inventar, "neueste_version", lambda repo: {
"tag": "v1.27.3", "tag_roh": "v1.27.3", "version": "1.27.3" if repo == "go-gitea/gitea" else "0.0.1",
"datum": "2026-09-20"})
monkeypatch.setattr(inventar, "erreichbar", lambda url: True)
monkeypatch.setattr(inventar.arcane, "app_version", lambda url: None)
monkeypatch.delenv("MC_HOMELAB_KARENZ_H", raising=False)
kanal.bericht_speichern(BERICHT)
yield
einstellungen_mod.einstellungen.cache_clear()
def _gitea_app() -> dict:
ziele = {z["id"]: z for z in inventar.ziele()["ziele"]}
return {b["id"]: b for b in ziele["ct-104"]["bausteine"]}["app"]
def test_frisch_geaendertes_skript_neu_aber_ohne_knopf(gitea_neu, monkeypatch):
jetzt = datetime.now(LOCAL_TZ).timestamp()
monkeypatch.setattr(karenz, "skript_geaendert", lambda kennung: jetzt - 3600)
app = _gitea_app()
assert (app["zustand"], app["kurz"], app["aktion"]) == ("neu", "1.27.2 → 1.27.3", None)
assert app["grund"].startswith("Das Update-Skript wurde am ") and "zur Sicherheit erst ab" in app["grund"]
# Der Knopf fehlt nicht nur: auch die Schnittstelle lehnt mit demselben Satz ab, und kein Lauf beginnt.
antwort = updates.starten("ct-104", "app")
assert antwort == {"ok": False, "detail": app["grund"]}
assert updates.laufende_ziele() == set() and kanal.liste() == []
def test_altes_skript_knopf_wie_bisher(gitea_neu, monkeypatch):
monkeypatch.setattr(karenz, "skript_geaendert", lambda kennung: 0.0)
app = _gitea_app()
assert app["aktion"]["frage"] == ("Gitea jetzt aktualisieren? Vorher wird ein Snapshot angelegt; ist die Prüfung "
"danach rot, geht es von selbst zurück.")
def test_ungeprueft_sagt_es_die_rueckfrage(gitea_neu, monkeypatch):
def kaputt(kennung):
raise RuntimeError("GitHub antwortet nicht")
monkeypatch.setattr(karenz, "skript_geaendert", kaputt)
app = _gitea_app()
assert app["aktion"] is not None and app["aktion"]["frage"].endswith(karenz.NICHT_GEPRUEFT)
assert app["aktion"]["frage"] == ("Gitea jetzt aktualisieren? Vorher wird ein Snapshot angelegt; ist die Prüfung "
"danach rot, geht es von selbst zurück. Ob das Skript kürzlich geändert wurde, "
"ließ sich nicht prüfen.")