- 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>
111 lines
5.5 KiB
Python
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.")
|