diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d1b68..9c4fbe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,16 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.14.1] – 2026-09-18 + +### Behoben +- Auf Proxmox Backup Server las der Agent **keine einzige Angabe** aus: `proxmox-backup-manager subscription get` gibt eine Rahmentabelle aus, keine `feld: wert`-Zeilen. Jetzt wird für PBS `--output-format json` genutzt, und die Tabelle wird als Rückfall ausgewertet (#023) +- Die PBS-Version enthielt die laufende Fassung ein zweites Mal (#023) +- Der Agent meldete eine „neue Version“, auch wenn die installierte neuer war als die des Dashboards (#023) + +### Geändert +- Agent-Version 0.2.1; die Tests prüfen die Versionsangabe jetzt gegen die Konstante des Dashboards statt gegen eine abgeschriebene Zahl (#023) + ## [0.14.0] – 2026-09-18 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index 22bd419..101fa08 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 9 h 08 min +**Gesamt:** 9 h 43 min | Issue | Zeit | |---|---| @@ -28,6 +28,7 @@ | #021 Geheimnisse verschlüsseln | 0 h 20 min | | #018 Feldtest auf echten Hosts | 0 h 40 min | | #022 Cluster-Abgleich | 0 h 25 min | +| #023 Feldtest PBS | 0 h 35 min | | Meilenstein | Zeit | |---|---| @@ -50,7 +51,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.14.0 +**Version:** 0.14.1 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -133,6 +134,10 @@ - #022 abgeschlossen: Cluster-Abgleich – jeder Agent meldet die Mitglieder, das Dashboard warnt bei Knoten ohne Agent - **Fehler im eigenen Werkzeug gefunden:** Alle Handbuch-Bilder waren in Wahrheit die Anmeldeseite. Firefox verwirft eine selbst angelegte Cookie-Datei stillschweigend. Jetzt läuft ein lokaler Vermittler mit, und das Skript bricht ab, wenn alle Aufnahmen gleich groß sind. +- Feldtest auf einem echten Proxmox Backup Server (#023): der Agent hätte dort **nichts** gelesen – PBS gibt eine Rahmentabelle aus, kein `feld: wert`. Behoben, mit den echten Ausgaben als Testfälle +- Der erfundene PBS-Testfall hatte den Fehler verdeckt. Lehre: ausgedachte Testdaten bestätigen nur die eigene Annahme +- Uninstall zum ersten Mal auf einem echten Host geprüft – restlos sauber, damit ist #018 vollständig + ### Nächster Schritt - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline -- Für 1.0.0 fehlen noch: Feldtest auf PBS und PMG, Uninstall auf einem echten Host +- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) diff --git a/agent/proxledger-agent.sh b/agent/proxledger-agent.sh index 847e5a4..482ba7f 100755 --- a/agent/proxledger-agent.sh +++ b/agent/proxledger-agent.sh @@ -12,7 +12,7 @@ set -o nounset set -o pipefail -AGENT_VERSION="0.2.0" +AGENT_VERSION="0.2.1" CONFIG_DATEI="${PROXLEDGER_CONFIG:-/etc/proxledger/agent.conf}" SPOOL_VERZEICHNIS="${PROXLEDGER_SPOOL:-/var/lib/proxledger/spool}" SPOOL_MAXIMUM=30 @@ -106,15 +106,24 @@ produkt_erkennen() { } subscription_ausgabe() { + local ausgabe="" case "$1" in + # PVE kennt --output-format nicht (geprueft auf 9.2), liefert aber "feld: wert". pve) pvesubscription get 2>/dev/null ;; - pbs) proxmox-backup-manager subscription get 2>/dev/null ;; + # PBS gibt ohne --output-format eine Rahmentabelle aus, aus der sich schlecht lesen laesst. + # JSON kann es (geprueft auf 4.2); aeltere Fassungen faengt die Tabellenauswertung ab. + pbs) + ausgabe=$(proxmox-backup-manager subscription get --output-format json 2>/dev/null) + [ -n "$ausgabe" ] || ausgabe=$(proxmox-backup-manager subscription get 2>/dev/null) + printf '%s' "$ausgabe" + ;; pmg) pmgsubscription get 2>/dev/null ;; *) printf '' ;; esac } -# Liest ein Feld aus der Subscription-Ausgabe. Unterstützt "feld: wert" und JSON. +# Liest ein Feld aus der Subscription-Ausgabe. Unterstuetzt "feld: wert", JSON und die +# Rahmentabelle von PBS ("| feld | wert |" mit Unicode-Strichen). feld_lesen() { local feld="$1" text="$2" wert wert=$(printf '%s\n' "$text" | sed -n "s/^[[:space:]]*${feld}[[:space:]]*:[[:space:]]*//p" | head -n1) @@ -122,6 +131,15 @@ feld_lesen() { wert=$(printf '%s\n' "$text" | sed -n "s/.*\"${feld}\"[[:space:]]*:[[:space:]]*\"\{0,1\}\([^\",}]*\)\"\{0,1\}.*/\1/p" | head -n1) fi + if [ -z "$wert" ]; then + # Tabellenzeile: Trennzeichen ist U+2502; awk zerlegt daran und liefert die zweite Spalte. + wert=$(printf '%s\n' "$text" | + awk -F'\xe2\x94\x82' -v feld="$feld" ' + NF >= 3 { + name = $2; gsub(/^[ \t]+|[ \t]+$/, "", name) + if (name == feld) { wert = $3; gsub(/^[ \t]+|[ \t]+$/, "", wert); print wert; exit } + }') + fi # Umschließende Anführungszeichen und Leerzeichen entfernen. wert=${wert%\"} wert=${wert#\"} @@ -133,7 +151,9 @@ produkt_version() { # Der Kernel wird separat gemeldet, deshalb hier abschneiden. case "$1" in pve) pveversion 2>/dev/null | head -n1 | sed 's/ (running kernel.*//' ;; - pbs) proxmox-backup-manager version 2>/dev/null | head -n1 ;; + # PBS haengt die laufende Fassung an ("… 4.2.6-1 running version: 4.2.1"); + # gemeldet wird die installierte. + pbs) proxmox-backup-manager version 2>/dev/null | head -n1 | sed 's/ running version.*//' ;; pmg) pmgversion 2>/dev/null | head -n1 ;; *) printf '' ;; esac @@ -395,8 +415,15 @@ aktion_test() { [ "$(printf '%s' "$antwort" | tail -n1)" = "200" ] } +# Ist $1 eine neuere Fassung als $2? `sort -V` vergleicht Versionsnummern der Reihe nach. +version_neuer() { + [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ] +} + version_pruefen() { - if [ -n "${NEUESTE_VERSION:-}" ] && [ "$NEUESTE_VERSION" != "$AGENT_VERSION" ]; then + # Nur melden, wenn das Dashboard wirklich eine neuere Fassung kennt. Sonst warnt ein Agent, + # der einer Testinstallation vorauseilt, gegen sich selbst (im Feldtest #023 aufgefallen). + if [ -n "${NEUESTE_VERSION:-}" ] && version_neuer "$NEUESTE_VERSION" "$AGENT_VERSION"; then protokoll notice "Neue Agent-Version verfügbar: ${NEUESTE_VERSION} (installiert: ${AGENT_VERSION})." if [ "$AUTO_UPDATE" = "yes" ]; then aktion_update diff --git a/docs/anleitung/README.md b/docs/anleitung/README.md index c9394d7..913ac4b 100644 --- a/docs/anleitung/README.md +++ b/docs/anleitung/README.md @@ -113,6 +113,12 @@ Der Installer erkennt selbst, ob es sich um Proxmox VE, Backup Server oder Mail registriert den Host, legt die Konfiguration unter `/etc/proxledger/agent.conf` (Rechte 600) ab, richtet einen systemd-Timer ein und schickt sofort den ersten Report. +> **Stand der Erprobung:** Proxmox VE (9.2, mit und ohne Subscription, einzeln und im Cluster) und +> Proxmox Backup Server (4.2, ohne Subscription) sind auf echten Hosts geprüft. Für **Mail Gateway** +> stand bisher kein Host zur Verfügung – dort beruht die Auswertung noch auf Annahmen, und bei PBS hat +> sich genau so eine Annahme als falsch erwiesen. Bitte den ersten PMG-Host nach der Installation +> kurz gegenprüfen (`proxledger-agent --dry-run` zeigt, was gemeldet würde). + Nützliche Befehle auf dem Host: | Befehl | Zweck | diff --git a/docs/issues/018-agent-feldtest.md b/docs/issues/018-agent-feldtest.md index e4f8bf3..379e9c1 100644 --- a/docs/issues/018-agent-feldtest.md +++ b/docs/issues/018-agent-feldtest.md @@ -15,7 +15,7 @@ auf echten Hosts laufen. - [x] Ausgabeformate von `pvesubscription get`, `proxmox-backup-manager subscription get` und `pmgsubscription get` mit den Annahmen im Skript abgleichen - [x] Verhalten ohne Subscription geprüft (Status `notfound`, alle übrigen Felder fehlen und werden zu null) -- [x] Timer und Spool-Nachsendung geprüft; Uninstall steht noch aus (Agents laufen vorerst weiter) +- [x] Timer, Spool-Nachsendung und Uninstall geprüft (Uninstall am 18.09.2026 auf dem PBS-Host, siehe #023) - [x] Abweichungen als Testfälle in `tests/agent-skript.test.ts` übernehmen ## Akzeptanzkriterien @@ -60,4 +60,4 @@ funktioniert (Report gepuffert bei unerreichbarem Dashboard, beim nächsten Lauf 6. **Cluster:** Jeder Knoten hat eine eigene Subscription – daraus entstand #022. **Noch offen:** PBS und PMG – dafür ist #023 angelegt. PBS-Hosts stellt Florian bereit, allerdings nur -unlizenziert; für PMG gibt es derzeit keinen Host. Ebenfalls offen: ein echter Uninstall-Durchlauf. +unlizenziert; für PMG gibt es derzeit keinen Host. Der Uninstall-Durchlauf ist inzwischen erledigt (#023). diff --git a/docs/issues/023-feldtest-pbs-pmg.md b/docs/issues/023-feldtest-pbs-pmg.md index dcc378b..9c6d1c3 100644 --- a/docs/issues/023-feldtest-pbs-pmg.md +++ b/docs/issues/023-feldtest-pbs-pmg.md @@ -2,7 +2,7 @@ **Status:** offen **Meilenstein:** v1.0.0 **Labels:** agent, feldtest -**Geschätzt:** 2 h **Tatsächlich:** – +**Geschätzt:** 2 h **Tatsächlich:** 0 h 35 min (PBS erledigt, PMG offen) ## Ziel Der Feldtest in #018 lief auf zwei Proxmox-VE-Hosts. Für Backup Server und Mail Gateway beruht die @@ -16,15 +16,54 @@ falsch erwiesen (`--output-format json` gibt es dort nicht). Das gehört vor 1.0 - **PMG:** derzeit kein Host verfügbar. Der Zweig bleibt ungeprüft; das ist im Handbuch zu vermerken. ## Aufgaben -- [ ] `scripts/feldtest-erheben.sh` auf einem PBS-Host laufen lassen (liest nur, ändert nichts) -- [ ] Prüfen, ob `proxmox-backup-manager subscription get` Text oder JSON liefert und welche Felder +- [x] `scripts/feldtest-erheben.sh` auf einem PBS-Host laufen lassen (liest nur, ändert nichts) +- [x] Prüfen, ob `proxmox-backup-manager subscription get` Text oder JSON liefert und welche Felder ohne Subscription überhaupt erscheinen -- [ ] Ausgabe von `proxmox-backup-manager version` gegen `produkt_version()` prüfen -- [ ] Erhobene Ausgaben als Testfälle in `tests/agent-skript.test.ts` aufnehmen – der heutige +- [x] Ausgabe von `proxmox-backup-manager version` gegen `produkt_version()` prüfen +- [x] Erhobene Ausgaben als Testfälle in `tests/agent-skript.test.ts` aufnehmen – der heutige PBS-Testfall ist erfunden (JSON) und damit womöglich am echten Verhalten vorbei -- [ ] Agent auf einem PBS-Host installieren, einen Report abwarten, wieder deinstallieren +- [x] Agent auf einem PBS-Host installieren, einen Report abwarten, wieder deinstallieren - [ ] PMG offen lassen und die Lücke im Handbuch benennen ## Notizen Der Cluster-Abgleich aus #022 betrifft nur PVE: PBS und PMG kennen kein `pvecm`, dort bleibt die Mitgliederliste leer. + +## Ergebnis (18.09.2026, `bup-ste-pbs-01`, PBS 4.2 auf Debian 13, ohne Subscription) + +**Ein echter Fehler gefunden – und er war kein kleiner.** `proxmox-backup-manager subscription get` +gibt ohne weitere Angabe eine **Rahmentabelle** aus: + +``` +┌──────────┬────────────────────────────────┐ +│ Name │ Value │ +╞══════════╪════════════════════════════════╡ +│ status │ notfound │ +``` + +Der Agent suchte nach `feld: wert` oder JSON – beides passt nicht. Auf einem echten PBS hätte er +**alle Felder als null gemeldet, nicht einmal die Server-ID**. Der erfundene Testfall hatte genau das +verdeckt, weil er JSON unterstellte. + +Behoben in drei Schritten: + +1. Für PBS wird `--output-format json` verwendet. Anders als PVE **kann** PBS das (auf 4.2 geprüft). +2. Bleibt die Ausgabe leer, wird die Textform genommen und die Rahmentabelle ausgewertet + (`awk -F'│'`) – für Fassungen, die das JSON-Format nicht kennen. +3. `proxmox-backup-manager version` liefert „proxmox-backup-server 4.2.6-1 running version: 4.2.1“; + gemeldet wird jetzt die installierte Fassung ohne den Zusatz. + +Beide Formen sind als Testfälle hinterlegt, mit den echten Ausgaben des Hosts. + +**Nebenbefund:** Der Agent meldete „Neue Agent-Version verfügbar: 0.2.0 (installiert: 0.2.1)“ – er +verglich nur auf Ungleichheit. Jetzt wird mit `sort -V` geprüft, ob die Fassung wirklich neuer ist. + +**Ende-zu-Ende geprüft:** Installation → Registrierung → erster Report → Warnungen. Im Dashboard kamen +an: Produkt `PBS`, Version `proxmox-backup-server 4.2.6-1`, Server-ID `69AED6…`, 1 Sockel, 106 +ausstehende Pakete, leere Cluster-Liste (PBS kennt kein `pvecm`), Warnungen „Keine Subscription +gefunden“ und „Host ist keinem Kunden zugewiesen“. Anschließend `--uninstall`: Timer, Dienst, +`/usr/local/bin/proxledger-agent`, `/etc/proxledger` und `/var/lib/proxledger` restlos entfernt. Damit +ist auch der letzte offene Punkt aus #018 erledigt. + +**Weiterhin offen:** PMG (kein Host) und der Fall „aktive Subscription auf PBS“ (nur unlizenzierte +Hosts verfügbar). Dieses Issue bleibt deshalb offen. diff --git a/docs/issues/README.md b/docs/issues/README.md index de9698b..8ced41b 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -26,5 +26,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#020](020-gitea-issue-abgleich.md) | Issues nach Gitea übertragen | erledigt | v0.4.1 | 2 h | 0 h 10 min | | [#021](021-geheimnisse-verschluesseln.md) | Zugangsdaten verschlüsselt speichern | erledigt | v0.13.1 | 2 h | 0 h 20 min | | [#022](022-cluster-abgleich.md) | Cluster-Abgleich | erledigt | v0.14.0 | 3 h | 0 h 25 min | -| [#023](023-feldtest-pbs-pmg.md) | Feldtest auf PBS und PMG | offen | v1.0.0 | 2 h | – | -| | **Summe** | | | **121 h** | **9 h 08 min** | +| [#023](023-feldtest-pbs-pmg.md) | Feldtest auf PBS und PMG | offen | v1.0.0 | 2 h | 0 h 35 min (PBS erledigt, PMG offen) | +| | **Summe** | | | **121 h** | **9 h 43 min** | diff --git a/package.json b/package.json index 8d94e76..46677f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.14.0", + "version": "0.14.1", "private": true, "scripts": { "dev": "next dev", diff --git a/src/server/agent/http.ts b/src/server/agent/http.ts index c00b4cd..33a415e 100644 --- a/src/server/agent/http.ts +++ b/src/server/agent/http.ts @@ -4,7 +4,7 @@ import { tokenAusHeader } from "@/lib/tokens"; import { AgentApiFehler } from "./service"; /** Version des Agent-Skripts, die das Dashboard ausliefert (siehe #005). */ -export const AGENT_VERSION = "0.2.0"; +export const AGENT_VERSION = "0.2.1"; export function fehlerAntwort(status: number, code: string, nachricht: string, extra?: HeadersInit) { return Response.json({ ok: false, code, fehler: nachricht }, { status, headers: extra }); diff --git a/tests/agent-skript.test.ts b/tests/agent-skript.test.ts index 7e7978d..a55c0ad 100644 --- a/tests/agent-skript.test.ts +++ b/tests/agent-skript.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest"; import { POST as enrollPost } from "@/app/api/agent/v1/enroll/route"; +import { AGENT_VERSION } from "@/server/agent/http"; import { POST as reportPost } from "@/app/api/agent/v1/report/route"; import { agentRateLimit, enrollRateLimit } from "@/lib/ratelimit"; import { erzeugeToken } from "@/lib/tokens"; @@ -20,6 +21,36 @@ function befehlErsetzen(name: string, ausgabe: string) { chmodSync(datei, 0o755); } +/** + * Ahmt `proxmox-backup-manager` nach: Das Werkzeug beantwortet `version` anders als + * `subscription get`, und auf `--output-format json` antwortet es anders als ohne. + * Genau daran hängt die Auswertung im Agent. + */ +function pbsErsetzen(antworten: { json: string; tabelle: string; version: string }) { + const datei = path.join(stubVerzeichnis, "proxmox-backup-manager"); + writeFileSync( + datei, + [ + "#!/usr/bin/env bash", + 'if [ "$1" = "version" ]; then', + "cat <<'VERSION'", + antworten.version, + "VERSION", + 'elif [[ "$*" == *--output-format*json* ]]; then', + "cat <<'JSON'", + antworten.json, + "JSON", + "else", + "cat <<'TEXT'", + antworten.tabelle, + "TEXT", + "fi", + "", + ].join("\n"), + ); + chmodSync(datei, 0o755); +} + /** Führt den Agent mit den vorbereiteten Befehlen aus und liefert den Report. */ function reportErzeugen(): Record | Record { const ausgabe = execFileSync("bash", [AGENT, "--dry-run"], { @@ -43,6 +74,23 @@ nextduedate: 2026-09-24 sockets: 1 level: c`; +// Echte Ausgaben von einem Proxmox Backup Server 4.2 ohne Subscription (Feldtest #023). +const PBS_TABELLE = `┌──────────┬──────────────────────────────────────────────────────────┐ +│ Name │ Value │ +╞══════════╪══════════════════════════════════════════════════════════╡ +│ status │ notfound │ +├──────────┼──────────────────────────────────────────────────────────┤ +│ message │ There is no subscription key │ +├──────────┼──────────────────────────────────────────────────────────┤ +│ serverid │ 69AED6ABA8A34EBE9AE1075333BBF8FE │ +├──────────┼──────────────────────────────────────────────────────────┤ +│ url │ https://www.proxmox.com/en/proxmox-backup-server/pricing │ +└──────────┴──────────────────────────────────────────────────────────┘`; + +const PBS_VERSION = "proxmox-backup-server 4.2.6-1 running version: 4.2.1"; + +const PBS_JSON = `{"message":"There is no subscription key","serverid":"69AED6ABA8A34EBE9AE1075333BBF8FE","status":"notfound","url":"https://www.proxmox.com/en/proxmox-backup-server/pricing"}`; + describe("Agent-Skript", () => { beforeEach(() => { stubVerzeichnis = mkdtempSync(path.join(tmpdir(), "proxledger-agent-test-")); @@ -75,7 +123,7 @@ describe("Agent-Skript", () => { host: Record; }; - expect(report.agentVersion).toBe("0.2.0"); + expect(report.agentVersion).toBe(AGENT_VERSION); expect(report.reportId).toMatch(/.{8,}/); expect(report.erhobenAm).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/); expect(report.produkt).toEqual({ typ: "pve", version: "pve-manager/8.3.2/dc2b2f4e" }); @@ -144,6 +192,39 @@ productname: Proxmox "VE" \\ Subscription mit Tab`, }); }); + it("liest einen PBS ohne Subscription (echte Ausgaben, JSON bevorzugt)", () => { + pbsErsetzen({ json: PBS_JSON, tabelle: PBS_TABELLE, version: PBS_VERSION }); + + const report = reportErzeugen() as { + produkt: { typ: string }; + subscription: Record; + host: { clusterMitglieder: string[] }; + }; + + expect(report.produkt.typ).toBe("pbs"); + expect(report.subscription.status).toBe("notfound"); + expect(report.subscription.serverid).toBe("69AED6ABA8A34EBE9AE1075333BBF8FE"); + expect(report.subscription.key).toBeNull(); + expect(report.subscription.sockets).toBeNull(); + // Ohne pvecm bleibt die Mitgliederliste leer – PBS kennt keinen Cluster in diesem Sinne. + expect(report.host.clusterMitglieder).toEqual([]); + }); + + it("liest die Rahmentabelle, falls ein PBS kein JSON ausgibt", () => { + // Ältere Fassungen könnten das JSON-Format nicht kennen; dann bleibt nur die Tabelle. + befehlErsetzen("proxmox-backup-manager", PBS_TABELLE); + + const report = reportErzeugen() as { subscription: Record }; + expect(report.subscription.status).toBe("notfound"); + expect(report.subscription.serverid).toBe("69AED6ABA8A34EBE9AE1075333BBF8FE"); + }); + + it("meldet die PBS-Version ohne die laufende Fassung", () => { + pbsErsetzen({ json: PBS_JSON, tabelle: PBS_TABELLE, version: PBS_VERSION }); + const report = reportErzeugen() as { produkt: { version: string } }; + expect(report.produkt.version).toBe("proxmox-backup-server 4.2.6-1"); + }); + it("meldet die Cluster-Mitglieder", () => { befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION); befehlErsetzen( @@ -169,7 +250,7 @@ productname: Proxmox "VE" \\ Subscription mit Tab`, it("gibt die Version aus", () => { const ausgabe = execFileSync("bash", [AGENT, "--version"], { encoding: "utf8" }).trim(); - expect(ausgabe).toBe("0.2.0"); + expect(ausgabe).toBe(AGENT_VERSION); }); describe.runIf(datenbankVerfuegbar)("Zusammenspiel mit der Agent-API", () => {