homelab: Update-Weg je App (Knopf nur, wo das Community-Script die App wirklich aktualisiert), Version von PVE Scripts Local aus der App selbst

Nachgelesen in ct/<app>.sh: AdGuard und PVE Scripts Local verweisen auf ihren eingebauten Updater,
Gitea/NetBird/NPMplus aktualisieren wirklich, PBS ueber die Pakete. /root/.proxmoxve-local stand noch
auf 0.5.8, die App selbst schon auf der neuesten Veroeffentlichung (v1.2.1, untagged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 19:20:04 +02:00
co-authored by Claude Opus 5.5
parent 56df849455
commit 528198de60
8 changed files with 51 additions and 11 deletions
+2 -1
View File
@@ -41,4 +41,5 @@ def neueste_version(repo: str) -> dict | None:
return None
tag, name = str(daten["tag_name"]), str(daten.get("name") or "")
roh = tag if _VERSION.match(tag) or not _VERSION.match(name) else name
return {"tag": roh, "version": roh.lstrip("vV"), "datum": str(daten.get("published_at") or "")[:10] or None}
return {"tag": roh, "tag_roh": tag, "version": roh.lstrip("vV"),
"datum": str(daten.get("published_at") or "")[:10] or None}
+13 -4
View File
@@ -1,6 +1,13 @@
"""Was in welchem Container steckt (Community-Scripts-Kennung aus /usr/bin/update im Gast).
Stand der Bestandsaufnahme vom 24.09.2026: sechs Container, alle mit dem Etikett community-script.
Wie eine App aktualisiert wird, ist je Community-Script verschieden (nachgelesen am 24.09. in ct/<app>.sh):
skript `update` im Gast spielt die App wirklich neu ein (Gitea: neues Programm von GitHub,
NetBird: apt aus dem NetBird-Repository, NPMplus: neues Docker-Image)
eigene-oberflaeche das Skript verweist auf den eingebauten Updater der App (AdGuard, PVE Scripts Local)
pakete die App kommt als Paket; das Update der Pakete im Gast ist ihr Update (PBS)
Nur beim Weg „skript“ bietet die Übersicht „Jetzt updaten“ für die App an.
Neue Container mit dem Etikett erscheinen von selbst; fehlt ihre Kennung hier, zeigt die Übersicht
sie ohne Versionsvergleich und ohne Webprüfung — nachtragen genügt.
"""
@@ -17,20 +24,22 @@ class App:
port: int | None = None # Weboberfläche; None = keine (z. B. NetBird-Client)
https: bool = False
pfad: str = "/"
weg: str = "skript" # „skript“, „eigene-oberflaeche“ oder „pakete“ (siehe oben)
KATALOG: dict[str, App] = {a.kennung: a for a in (
App("adguard", "AdGuard Home", "AdguardTeam/AdGuardHome", port=8080),
App("adguard", "AdGuard Home", "AdguardTeam/AdGuardHome", port=8080, weg="eigene-oberflaeche"),
App("npmplus", "NPMplus", "ZoeyVid/NPMplus", vergleich="datum", port=81, https=True),
App("netbird", "NetBird", "netbirdio/netbird"),
App("pve-scripts-local", "PVE Scripts Local", "community-scripts/ProxmoxVE-Local", port=3000),
App("pve-scripts-local", "PVE Scripts Local", "community-scripts/ProxmoxVE-Local", port=3000,
weg="eigene-oberflaeche"),
App("gitea", "Gitea", "go-gitea/gitea", port=3000),
App("proxmox-backup-server", "Proxmox Backup Server", port=8007, https=True),
App("proxmox-backup-server", "Proxmox Backup Server", port=8007, https=True, weg="pakete"),
)}
# Gäste, die keine Community-Scripts sind, erkennt man am Namen.
NACH_NAME: dict[str, App] = {
"arcane": App("arcane", "Arcane (Docker)", port=3552),
"arcane": App("arcane", "Arcane (Docker)", port=3552, weg="eigene-oberflaeche"),
}
+10
View File
@@ -83,6 +83,13 @@ def _app_baustein(gast: dict, app: apps.App, zid: str) -> BausteinStand:
stand.zustand, stand.grund = "unbekannt", "Die neueste Version ist gerade nicht abrufbar (GitHub)."
return stand
stand.verfuegbar = neu["version"]
if installiert and installiert.startswith("untagged-"):
# Manche Projekte veröffentlichen ohne Versions-Tag; die App kennt dann nur dieses Tag (PVE Scripts Local).
if installiert == neu.get("tag_roh"):
stand.installiert = neu["version"]
return stand
stand.zustand, stand.grund = "unbekannt", "Die installierte Fassung lässt sich keiner Version zuordnen."
return stand
if app.vergleich == "datum":
# Docker-Image ohne Versionsnummer: neuer, wenn die Veröffentlichung jünger ist als das Image.
bild = re.search(r"\d{4}-\d{2}-\d{2}", installiert or "")
@@ -91,6 +98,9 @@ def _app_baustein(gast: dict, app: apps.App, zid: str) -> BausteinStand:
ist_neuer = neuer(neu["version"], installiert) if installiert else None
if ist_neuer is None:
stand.zustand, stand.grund = "unbekannt", "Die installierte Version ließ sich nicht lesen."
elif ist_neuer and app.weg != "skript":
stand.zustand, stand.kurz = "neu", f"{installiert} → {neu['version']}"
stand.grund = f"{app.name} aktualisiert sich über die eigene Oberfläche; das Community-Script tut es nicht."
elif ist_neuer:
stand.zustand, stand.kurz = "neu", f"{installiert} → {neu['version']}"
rueckweg = ("Vorher wird ein Snapshot angelegt; ist die Prüfung danach rot, geht es von selbst zurück."
+3
View File
@@ -244,6 +244,9 @@ def starten(ziel_id: str, baustein: str) -> dict:
return {"ok": False, "detail": "Dieses Gerät ist nicht freigegeben (Etikett watcher fehlt)."}
app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name"))
name = app.name if app else str(gast.get("name"))
if baustein == "app" and (not app or app.weg != "skript"):
return {"ok": False, "detail": f"{name} aktualisiert sich nicht über das Community-Script "
"(eigene Oberfläche oder Pakete)."}
lauf = _neuer_lauf(ziel_id, baustein, name)
ziel = _host_lauf if gast is None else (lambda lf: _gast_lauf(lf, gast))
threading.Thread(target=ziel, args=(lauf,), name=f"homelab-{ziel_id}", daemon=True).start()
+1 -1
View File
@@ -138,7 +138,7 @@
"ip": "192.168.178.149",
"app": {
"kennung": "pve-scripts-local",
"version": "0.5.8"
"version": "untagged-80028680f0b29a379726"
},
"os_updates": {
"anzahl": 0,
+17 -3
View File
@@ -27,9 +27,10 @@ def daten(tmp_path, monkeypatch):
# Keine Netzabfragen: neueste Versionen und Weboberflächen sind fest.
neueste = {"AdguardTeam/AdGuardHome": "0.107.79", "go-gitea/gitea": "1.27.3", "netbirdio/netbird": "0.79.0",
"community-scripts/ProxmoxVE-Local": "1.2.1"}
roh = {"community-scripts/ProxmoxVE-Local": "untagged-80028680f0b29a379726"}
monkeypatch.setattr(inventar, "neueste_version", lambda repo: {
"tag": neueste.get(repo, "2026-07-24-r1"), "version": neueste.get(repo, "2026-07-24-r1"),
"datum": "2026-07-24"})
"tag": neueste.get(repo, "2026-07-24-r1"), "tag_roh": roh.get(repo, neueste.get(repo, "2026-07-24-r1")),
"version": neueste.get(repo, "2026-07-24-r1"), "datum": "2026-07-24"})
monkeypatch.setattr(inventar, "erreichbar", lambda url: True)
monkeypatch.setattr(inventar.arcane, "app_version",
lambda url: {"installiert": "2.12.0", "neu": "2.13.1", "update": True})
@@ -106,7 +107,9 @@ def test_ziele_aus_dem_echten_bericht(daten):
assert (pbs["os"]["zustand"], pbs["os"]["kurz"]) == ("neu", "58 Pakete")
assert "Bind-Mount" in z["ct-105"]["rueckweg"]
assert {b["id"]: b for b in z["ct-103"]["bausteine"]}["app"]["kurz"] == "0.5.8 → 1.2.1"
# PVE Scripts Local steht auf der neuesten (untagged) Veröffentlichung: aktuell, angezeigt als 1.2.1.
pvesl = {b["id"]: b for b in z["ct-103"]["bausteine"]}["app"]
assert (pvesl["zustand"], pvesl["installiert"], pvesl["aktion"]) == ("aktuell", "1.2.1", None)
assert {b["id"]: b for b in z["ct-101"]["bausteine"]}["app"]["zustand"] == "aktuell" # Image jünger
# NetBird hat keine Weboberfläche: erreichbar heißt hier „läuft“.
assert z["ct-102"]["erreichbar"] is True
@@ -334,3 +337,14 @@ def test_hinweise_des_homelab_waechters(daten, monkeypatch):
encoding="utf-8")
antwort = _client().get("/api/homelab/hinweise").json()
assert [h["titel"] for h in antwort["hinweise"]] == ["Gitea antwortet nicht"]
def test_apps_mit_eigenem_updater_bekommen_keinen_knopf(daten, monkeypatch):
"""AdGuard und PVE Scripts Local aktualisiert das Community-Script nicht (ct/<app>.sh, 24.09.2026)."""
bericht = json.loads(json.dumps(BERICHT))
next(g for g in bericht["gaeste"] if g["vmid"] == 100)["app"]["version"] = "0.107.70"
kanal.bericht_speichern(bericht)
adguard = {b["id"]: b for b in {z["id"]: z for z in inventar.ziele()["ziele"]}["ct-100"]["bausteine"]}["app"]
assert adguard["zustand"] == "neu" and adguard["aktion"] is None and "eigene Oberfläche" in adguard["grund"]
antwort = updates.starten("ct-103", "app")
assert antwort["ok"] is False and "Community-Script" in antwort["detail"]
+2 -1
View File
@@ -46,7 +46,8 @@ def test_github_antworten_werden_gemerkt_fehler_nicht(monkeypatch):
monkeypatch.setattr(github.httpx, "get", lambda url, **kw: aufrufe.append(url) or antworten.pop(0))
with pytest.raises(httpx.HTTPStatusError):
github.github_json("repos/a/b/releases/latest")
assert github.neueste_version("a/b") == {"tag": "v258", "version": "258", "datum": "2026-09-20"}
assert github.neueste_version("a/b") == {"tag": "v258", "tag_roh": "v258", "version": "258",
"datum": "2026-09-20"}
assert github.github_json("repos/a/b/releases/latest")["tag_name"] == "v258"
assert len(aufrufe) == 2
assert maintenance._github_json is github.github_json
+3 -1
View File
@@ -50,7 +50,9 @@ VERSION_PROBEN = {
"proxmox-backup-server": "dpkg-query -W proxmox-backup-server",
"npmplus": "docker image inspect zoeyvid/npmplus:latest --format '{{.Created}}'",
}
VERSIONSDATEIEN = {"pve-scripts-local": "/root/.proxmoxve-local"}
# PVE Scripts Local aktualisiert sich über den eingebauten Updater; /root/.proxmoxve-local bleibt dabei auf dem
# Stand der Erstinstallation stehen (24.09.2026: „0.5.8“, tatsächlich die neueste Veröffentlichung).
VERSIONSDATEIEN = {"pve-scripts-local": "/opt/ProxmoxVE-Local/VERSION"}
log = logging.getLogger("ausfuehrer")