Merge branch 'worktree-agent-ab032f7900d8a5a63' into wartung/snapshots-reiter
This commit is contained in:
@@ -7,7 +7,10 @@ Container des Homelab-Teils braucht keinen Proxmox-Schlüssel.
|
||||
|
||||
• Bericht alle 10 Minuten (oder auf Anfrage): Host-Updates, Gäste mit Status, App-Version,
|
||||
OS-Updates, Snapshots und ob ein Snapshot überhaupt geht; wo nicht, ob eine Sicherung
|
||||
geht, und die Sicherungen des Orchestrators. Nur lesend.
|
||||
geht, und die Sicherungen des Orchestrators. Nur lesend. Seit 25.09.2026 je Gast auch
|
||||
snapshot_details (Zeitpunkt, Beschreibung, ob vom Orchestrator) und sicherung_details
|
||||
(Zeitpunkt, Größe) für die Seite Homelab → Snapshots; die Namenslisten snapshots und
|
||||
sicherungen bleiben, wie sie waren.
|
||||
• Aufträge eine feste Liste von Aktionen (AKTIONEN); alles andere wird abgelehnt. Welche Gäste
|
||||
angefasst werden dürfen, entscheidet dieser Host selbst anhand der Etiketten:
|
||||
community-script oder watcher, aber nicht watcher-aus. Dem Server wird dabei nicht
|
||||
@@ -55,6 +58,10 @@ ABFRAGE_ALLE_S = 5
|
||||
ERLAUBT = {"community-script", "watcher"}
|
||||
AUSGENOMMEN = "watcher-aus"
|
||||
SNAPSHOT_NAME = re.compile(r"^mc2-\d{8}-\d{6}$")
|
||||
# Beschreibung neuer Snapshots in Proxmox (seit 25.09.2026). Der Server wählt nur den Anlass, den Text legt dieser
|
||||
# Host fest. „knopf“ = „Snapshot anlegen“ auf der Seite Homelab → Snapshots; solche räumt kein Update weg.
|
||||
SNAPSHOT_ANLASS = {"update": "Vor einem Update durch den Homelab Orchestrator",
|
||||
"knopf": "Auf Knopfdruck im Homelab Orchestrator angelegt"}
|
||||
# Speicherarten, auf denen Proxmox Snapshots kann.
|
||||
SNAPSHOT_SPEICHER = {"lvmthin", "zfspool", "rbd", "btrfs", "cephfs"}
|
||||
|
||||
@@ -340,13 +347,43 @@ def _ip_vm(vmid: int) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _snapshots(art: str, vmid: int) -> list[str]:
|
||||
def _snapshots(art: str, vmid: int) -> list[dict]:
|
||||
"""Die Snapshots eines Gasts, wie pvesh sie liefert (samt „current“, dem laufenden Stand); leer bei Fehlern."""
|
||||
try:
|
||||
return [s["name"] for s in _pvesh(f"/nodes/{NODE}/{art}/{vmid}/snapshot") if s.get("name") != "current"]
|
||||
return [s for s in _pvesh(f"/nodes/{NODE}/{art}/{vmid}/snapshot") or [] if isinstance(s, dict)]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _ganzzahl(wert: object) -> int | None:
|
||||
try:
|
||||
return int(wert)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def snapshot_details(eintraege: object) -> list[dict]:
|
||||
"""Die Snapshots für die Seite Homelab → Snapshots, ältester zuerst, ohne „current“: Name, Zeitpunkt (snaptime,
|
||||
Unix-Sekunden; None, wo Proxmox keinen nennt), Beschreibung und ob er vom Orchestrator ist (nur solche setzt dieser
|
||||
Host zurück oder löscht er). Eine Belegung steht bewusst nicht dabei: Wie viel ein Snapshot auf LVM-Thin allein
|
||||
belegt, sagt Proxmox nicht — lvs zählt auch die Blöcke, die er mit dem Original teilt."""
|
||||
details = []
|
||||
for s in eintraege if isinstance(eintraege, list) else []:
|
||||
if not isinstance(s, dict) or not s.get("name") or s["name"] == "current":
|
||||
continue
|
||||
name = str(s["name"])
|
||||
details.append({"name": name, "zeit": _ganzzahl(s.get("snaptime")),
|
||||
"beschreibung": str(s.get("description") or "").strip() or None,
|
||||
"eigen": bool(SNAPSHOT_NAME.match(name))})
|
||||
return sorted(details, key=lambda d: (d["zeit"] or 0, d["name"]))
|
||||
|
||||
|
||||
def sicherung_details(eintraege: list[dict]) -> list[dict]:
|
||||
"""Die eigenen Sicherungen eines Gasts für die Seite Homelab → Snapshots: Archiv, Zeitpunkt (ctime), Größe in Bytes."""
|
||||
return [{"volid": e["volid"], "zeit": _ganzzahl(e.get("ctime")), "groesse": _ganzzahl(e.get("size"))}
|
||||
for e in eintraege]
|
||||
|
||||
|
||||
def _eigene_sicherungen(speicher: str, vmid: int | None = None) -> list[dict]:
|
||||
parameter = ["--content", "backup"] + (["--vmid", str(vmid)] if vmid is not None else [])
|
||||
return eigene_sicherungen(_pvesh(f"/nodes/{NODE}/storage/{speicher}/content", *parameter), vmid)
|
||||
@@ -579,12 +616,16 @@ def _speicherlage(speicher: list[dict], gaeste: list[dict]) -> dict:
|
||||
return lage
|
||||
|
||||
|
||||
def _gast(eintrag: dict, arten: dict[str, str], sicherung: dict, sicherungen: dict[int, list[str]]) -> dict:
|
||||
def _gast(eintrag: dict, arten: dict[str, str], sicherung: dict, sicherungen: dict[int, list[dict]]) -> dict:
|
||||
art, vmid = eintrag["type"], int(eintrag["vmid"])
|
||||
etiketten = _etiketten(eintrag.get("tags"))
|
||||
snapshots = _snapshots(art, vmid)
|
||||
eigene = sicherungen.get(vmid, [])
|
||||
gast = {"vmid": vmid, "art": art, "name": eintrag.get("name"), "status": eintrag.get("status"),
|
||||
"etiketten": etiketten, "erlaubt": erlaubt(etiketten), "uptime": eintrag.get("uptime"),
|
||||
"snapshots": _snapshots(art, vmid), "sicherungen": sicherungen.get(vmid, [])}
|
||||
"snapshots": [str(s["name"]) for s in snapshots if s.get("name") and s["name"] != "current"],
|
||||
"snapshot_details": snapshot_details(snapshots),
|
||||
"sicherungen": [e["volid"] for e in eigene], "sicherung_details": sicherung_details(eigene)}
|
||||
if art == "lxc" and gast["status"] == "running" and eintrag.get("maxdisk"):
|
||||
# Belegung der rootfs (Proxmox liest sie bei laufenden Containern mit). Am 24.09.2026 stand AdGuard bei
|
||||
# 100 % — das Abfrageprotokoll schrieb nicht mehr, die Paketsuche scheiterte, und niemand sah es.
|
||||
@@ -719,11 +760,11 @@ def bericht() -> dict:
|
||||
speicher = _speicher_liste()
|
||||
arten = {s["storage"]: s.get("type") for s in speicher}
|
||||
sicherung = sicherung_stand(speicher, SICHERUNG_SPEICHER)
|
||||
sicherungen: dict[int, list[str]] = {}
|
||||
sicherungen: dict[int, list[dict]] = {}
|
||||
if sicherung.get("speicher"):
|
||||
try:
|
||||
for e in _eigene_sicherungen(sicherung["speicher"]):
|
||||
sicherungen.setdefault(int(SICHERUNG_ARCHIV.match(e["volid"]).group(2)), []).append(e["volid"])
|
||||
sicherungen.setdefault(int(SICHERUNG_ARCHIV.match(e["volid"]).group(2)), []).append(e)
|
||||
except Exception:
|
||||
pass
|
||||
gaeste = []
|
||||
@@ -758,9 +799,11 @@ def _werkzeug(art: str) -> str:
|
||||
|
||||
def a_snapshot(p: dict) -> tuple[int, str]:
|
||||
art, vmid = _gast_pruefen(p.get("vmid"))
|
||||
beschreibung = SNAPSHOT_ANLASS.get(str(p.get("anlass") or "update"))
|
||||
if beschreibung is None:
|
||||
raise ValueError("unbekannter Anlass für einen Snapshot (update oder knopf)")
|
||||
name = "mc2-" + datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
code, text = _laufen([_werkzeug(art), "snapshot", str(vmid), name, "--description",
|
||||
"Vor einem Update durch den Homelab Orchestrator"], 600)
|
||||
code, text = _laufen([_werkzeug(art), "snapshot", str(vmid), name, "--description", beschreibung], 600)
|
||||
return code, f"snapshot={name}\n{text}" if code == 0 else text
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user