From bc13b94c7ea09e1d7ee491d198e4076af5925489 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Fri, 23 Oct 2026 15:46:15 +0200 Subject: [PATCH] fix(proxmox): Task-Status unter Last weiter abfragen, nesting an MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Feldtest wurde ein LXC fertig angelegt, während ProxDeploy nach drei gescheiterten Statusabfragen aufgab (Verbindungsaufbau > 10 s bei 76 % I/O-Last). Statusabfragen werden jetzt bis zum Task-Zeitlimit wiederholt, der Verbindungsaufbau darf 30 s dauern. nesting ist für Container standardmäßig an wie in der Proxmox-Oberfläche – Proxmox warnt sonst bei systemd 249/252. Refs #020 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ cli/api/openapi.json | 4 ++-- cli/internal/befehle/ausrollen.go | 2 +- src/app/(app)/ausrollen/assistent.tsx | 4 ++-- src/engine/proxmox/aufgaben.ts | 19 +++++++++++++----- src/engine/proxmox/client.ts | 5 ++++- src/engine/proxmox/tls.ts | 7 ++++++- src/lib/schemas/ausrollen.ts | 6 ++++-- tests/engine/proxmox-client.test.ts | 28 +++++++++++++++++++++++++++ 9 files changed, 63 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e370b1..84c6197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). - Standardwerte je Server (Node, Storage, Bridge) im Server-Formular; der Assistent belegt sie vor, die CLI nutzt sie für kurze Befehle (#023) ### Geändert +- LXC: `nesting` ist für Container standardmäßig an, wie in der Proxmox-Oberfläche – Debian 12 und Ubuntu 22.04 (systemd 252/249) brauchen es; abwählbar im Assistenten und mit `--nesting=false` (#020) ### Behoben +- Warten auf Proxmox-Tasks bricht bei vorübergehend fehlschlagenden Statusabfragen nicht mehr ab, sondern fragt bis zum Zeitlimit des Tasks weiter; Verbindungsaufbau darf 30 s statt 10 s dauern (#020) - Gast-Benutzer, der wie eine vorhandene Gruppe heißt (z. B. „admin“ auf Ubuntu 22.04), ließ die Provisionierung mit Code 9 abbrechen; die Gruppe wird jetzt Primärgruppe (#020) - `pnpm test:container` lief seit pnpm 12 nicht mehr (`pnpm -s` gibt es dort nicht) (#020) - Auf ausgelasteten Nodes brachen Aufträge beim ersten Verbindungswackler ab („Broken pipe“, „other side closed“, Zeitüberschreitung). Lesende Proxmox-Aufrufe werden jetzt bis zu dreimal wiederholt, schreibende nur, wenn die Anfrage sicher nicht ankam; Schreibaufrufe dürfen 120 s dauern; Wiederholungen stehen im Detail-Log (#020) diff --git a/cli/api/openapi.json b/cli/api/openapi.json index ae9935c..b8bb00d 100644 --- a/cli/api/openapi.json +++ b/cli/api/openapi.json @@ -503,13 +503,13 @@ }, "features": { "default": { - "nesting": false, + "nesting": true, "keyctl": false }, "type": "object", "properties": { "nesting": { - "default": false, + "default": true, "type": "boolean" }, "keyctl": { diff --git a/cli/internal/befehle/ausrollen.go b/cli/internal/befehle/ausrollen.go index 7d0638d..bc97ef3 100644 --- a/cli/internal/befehle/ausrollen.go +++ b/cli/internal/befehle/ausrollen.go @@ -49,7 +49,7 @@ func (g *gastFlags) registrieren(f *pflag.FlagSet, typ string) { if typ == "lxc" { f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)") f.BoolVar(&g.privilegiert, "privileged", false, "privilegierter Container (Standard: unprivilegiert)") - f.BoolVar(&g.nesting, "nesting", false, "Feature nesting (das Profil docker setzt es selbst)") + f.BoolVar(&g.nesting, "nesting", true, "Feature nesting (Standard an wie in Proxmox; --nesting=false schaltet es ab)") } else { f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen") } diff --git a/src/app/(app)/ausrollen/assistent.tsx b/src/app/(app)/ausrollen/assistent.tsx index b1dd138..4bedbd0 100644 --- a/src/app/(app)/ausrollen/assistent.tsx +++ b/src/app/(app)/ausrollen/assistent.tsx @@ -324,7 +324,7 @@ export function Assistent({ storage: vor.storage, swapMb: "512", unprivilegiert: true, - nesting: false, + nesting: true, bridge: vor.bridge, vlan: "", modus: "dhcp", @@ -817,7 +817,7 @@ export function Assistent({ hilfe={ werte.profile.includes("docker") ? "Das Profil docker schaltet es ohnehin ein." - : "Für Docker oder systemd-Dienste mit eigenem Namensraum." + : "Standard an, wie in Proxmox: neuere systemd-Versionen brauchen es im unprivilegierten Container." } checked={werte.nesting || werte.profile.includes("docker")} disabled={werte.profile.includes("docker")} diff --git a/src/engine/proxmox/aufgaben.ts b/src/engine/proxmox/aufgaben.ts index 8c09fa2..0f4ffbc 100644 --- a/src/engine/proxmox/aufgaben.ts +++ b/src/engine/proxmox/aufgaben.ts @@ -1,4 +1,4 @@ -import { istTrockenUpid, pfad, type ProxmoxClient } from "./client"; +import { istTrockenUpid, pfad, stoerung, type ProxmoxClient } from "./client"; import { ProxmoxFehler } from "./fehler"; /** @@ -53,10 +53,19 @@ export async function aufgabeAbwarten( }; for (;;) { - const status = await client.get(pfad`/nodes/${node}/tasks/${upid}/status`); - if (optionen.beiLog) { - const neu = await logLesen(); - if (neu.length > 0) optionen.beiLog(neu); + // Nachsehen ist harmlos: scheitert eine Abfrage vorübergehend (ausgelasteter Node), wird bis + // zum Zeitlimit des Tasks weiter gefragt – der Task läuft auf dem Node ja weiter. + let status: TaskStatus; + try { + status = await client.get(pfad`/nodes/${node}/tasks/${upid}/status`); + if (optionen.beiLog) { + const neu = await logLesen(); + if (neu.length > 0) optionen.beiLog(neu); + } + } catch (fehler) { + if (stoerung(fehler) === null || Date.now() - beginn > zeitlimit) throw fehler; + await warten(Math.max(intervall, 5000)); + continue; } if (status.status === "stopped") { if (status.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return; diff --git a/src/engine/proxmox/client.ts b/src/engine/proxmox/client.ts index e27b68a..f4cb9a1 100644 --- a/src/engine/proxmox/client.ts +++ b/src/engine/proxmox/client.ts @@ -54,6 +54,9 @@ const VERSUCHE = 3; export function stoerung(fehler: unknown): "nicht_angekommen" | "unklar" | null { if (fehler instanceof ProxmoxFehler) { const status = fehler.details.status; + // Schon übersetzte Netzwerkfehler (ohne HTTP-Status); TLS und Fingerprint sind nie vorübergehend. + if (status === undefined && (fehler.art === "zeitueberschreitung" || fehler.art === "verbindung")) + return "unklar"; if (status === 595 || status === 596) return "unklar"; if ( (status === 500 || status === 502 || status === 503) && @@ -122,7 +125,7 @@ export class ProxmoxClient { ); } this.basis = verbindung.url.replace(/\/+$/, "") + "/api2/json"; - this.agent = new Agent({ connect: tlsConnector(verbindung), keepAliveTimeout: 2_000 }); + this.agent = new Agent({ connect: tlsConnector(verbindung), keepAliveTimeout: 4_000 }); this.kopf = `PVEAPIToken=${verbindung.tokenId}=${verbindung.tokenGeheimnis}`; } diff --git a/src/engine/proxmox/tls.ts b/src/engine/proxmox/tls.ts index 5245faa..3ec80f6 100644 --- a/src/engine/proxmox/tls.ts +++ b/src/engine/proxmox/tls.ts @@ -104,7 +104,12 @@ export function tlsConnector(einstellung: TlsEinstellung): buildConnector.connec } // Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit, // und der Pin ließe sich dann nicht prüfen. - const basis = buildConnector({ rejectUnauthorized: einstellung.tlsPruefen, maxCachedSessions: 0 }); + // 30 s statt der 10 s von undici: ein ausgelasteter Node nimmt neue Verbindungen nur zögerlich an. + const basis = buildConnector({ + rejectUnauthorized: einstellung.tlsPruefen, + maxCachedSessions: 0, + timeout: 30_000, + }); if (!pin) return basis; return (optionen, rueckruf) => { diff --git a/src/lib/schemas/ausrollen.ts b/src/lib/schemas/ausrollen.ts index 3d210e8..0fce542 100644 --- a/src/lib/schemas/ausrollen.ts +++ b/src/lib/schemas/ausrollen.ts @@ -98,8 +98,10 @@ export const AusrollAnfrage = z swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512), unprivilegiert: z.boolean().default(true), features: z - .object({ nesting: z.boolean().default(false), keyctl: z.boolean().default(false) }) - .default({ nesting: false, keyctl: false }), + // nesting an wie in der Proxmox-Oberfläche: neuere systemd-Versionen brauchen es im + // unprivilegierten Container („Systemd 252 detected. You may need to enable nesting.“). + .object({ nesting: z.boolean().default(true), keyctl: z.boolean().default(false) }) + .default({ nesting: true, keyctl: false }), netz: Netz, zugang: Zugang, profile: z.array(z.string()).default(["base"]), diff --git a/tests/engine/proxmox-client.test.ts b/tests/engine/proxmox-client.test.ts index d75213f..be98531 100644 --- a/tests/engine/proxmox-client.test.ts +++ b/tests/engine/proxmox-client.test.ts @@ -275,3 +275,31 @@ describe("Proxmox-Tasks (UPID)", () => { expect(fehler.hinweis).toContain("TASK ERROR"); }); }); + +describe("Task abwarten unter Last", () => { + it("fragt nach vorübergehenden Fehlern der Statusabfrage weiter, bis der Task fertig ist", async () => { + let n = 0; + mock.route("GET", /\/nodes\/pve09\/tasks\/[^/]+\/status$/, () => { + n += 1; + if (n <= 4) return { status: 596, statusText: "Broken pipe" }; + return { data: { status: "stopped", exitstatus: "WARNINGS: 1" } }; + }); + await aufgabeAbwarten(client(), "UPID:pve09:00000001:00000002:00000003:vzcreate:114:root@pam:", { + warten: async () => {}, + }); + expect(n).toBe(5); + }); + + it("bricht bei fachlichen Fehlern sofort ab", async () => { + mock.route("GET", /\/nodes\/pve08\/tasks\/[^/]+\/status$/, () => ({ + status: 403, + statusText: "Permission check failed (/nodes/pve08, Sys.Audit)", + })); + const fehler = await fehlerVon( + aufgabeAbwarten(client(), "UPID:pve08:00000001:00000002:00000003:vzcreate:114:root@pam:", { + warten: async () => {}, + }), + ); + expect(fehler.art).toBe("recht"); + }); +});