fix(proxmox): Task-Status unter Last weiter abfragen, nesting an

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:46:15 +02:00
co-authored by Claude Opus 5.5
parent db57818eb3
commit bc13b94c7e
9 changed files with 63 additions and 14 deletions
+2
View File
@@ -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) - 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 ### 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 ### 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) - 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) - `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) - 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)
+2 -2
View File
@@ -503,13 +503,13 @@
}, },
"features": { "features": {
"default": { "default": {
"nesting": false, "nesting": true,
"keyctl": false "keyctl": false
}, },
"type": "object", "type": "object",
"properties": { "properties": {
"nesting": { "nesting": {
"default": false, "default": true,
"type": "boolean" "type": "boolean"
}, },
"keyctl": { "keyctl": {
+1 -1
View File
@@ -49,7 +49,7 @@ func (g *gastFlags) registrieren(f *pflag.FlagSet, typ string) {
if typ == "lxc" { if typ == "lxc" {
f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)") f.IntVar(&g.swap, "swap", 0, "Swap in MB (Standard: 512)")
f.BoolVar(&g.privilegiert, "privileged", false, "privilegierter Container (Standard: unprivilegiert)") 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 { } else {
f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen") f.BoolVar(&g.templateBauen, "build-template", false, "fehlt das Template, vorher bauen")
} }
+2 -2
View File
@@ -324,7 +324,7 @@ export function Assistent({
storage: vor.storage, storage: vor.storage,
swapMb: "512", swapMb: "512",
unprivilegiert: true, unprivilegiert: true,
nesting: false, nesting: true,
bridge: vor.bridge, bridge: vor.bridge,
vlan: "", vlan: "",
modus: "dhcp", modus: "dhcp",
@@ -817,7 +817,7 @@ export function Assistent({
hilfe={ hilfe={
werte.profile.includes("docker") werte.profile.includes("docker")
? "Das Profil docker schaltet es ohnehin ein." ? "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")} checked={werte.nesting || werte.profile.includes("docker")}
disabled={werte.profile.includes("docker")} disabled={werte.profile.includes("docker")}
+11 -2
View File
@@ -1,4 +1,4 @@
import { istTrockenUpid, pfad, type ProxmoxClient } from "./client"; import { istTrockenUpid, pfad, stoerung, type ProxmoxClient } from "./client";
import { ProxmoxFehler } from "./fehler"; import { ProxmoxFehler } from "./fehler";
/** /**
@@ -53,11 +53,20 @@ export async function aufgabeAbwarten(
}; };
for (;;) { for (;;) {
const status = await client.get<TaskStatus>(pfad`/nodes/${node}/tasks/${upid}/status`); // 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<TaskStatus>(pfad`/nodes/${node}/tasks/${upid}/status`);
if (optionen.beiLog) { if (optionen.beiLog) {
const neu = await logLesen(); const neu = await logLesen();
if (neu.length > 0) optionen.beiLog(neu); 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.status === "stopped") {
if (status.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return; if (status.exitstatus === "OK" || status.exitstatus?.startsWith("WARNINGS")) return;
const letzte = optionen.beiLog ? [] : (await logLesen()).slice(-5); const letzte = optionen.beiLog ? [] : (await logLesen()).slice(-5);
+4 -1
View File
@@ -54,6 +54,9 @@ const VERSUCHE = 3;
export function stoerung(fehler: unknown): "nicht_angekommen" | "unklar" | null { export function stoerung(fehler: unknown): "nicht_angekommen" | "unklar" | null {
if (fehler instanceof ProxmoxFehler) { if (fehler instanceof ProxmoxFehler) {
const status = fehler.details.status; 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 === 595 || status === 596) return "unklar";
if ( if (
(status === 500 || status === 502 || status === 503) && (status === 500 || status === 502 || status === 503) &&
@@ -122,7 +125,7 @@ export class ProxmoxClient {
); );
} }
this.basis = verbindung.url.replace(/\/+$/, "") + "/api2/json"; 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}`; this.kopf = `PVEAPIToken=${verbindung.tokenId}=${verbindung.tokenGeheimnis}`;
} }
+6 -1
View File
@@ -104,7 +104,12 @@ export function tlsConnector(einstellung: TlsEinstellung): buildConnector.connec
} }
// Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit, // Ohne Sitzungs-Cache: eine wiederaufgenommene TLS-Sitzung schickt kein Zertifikat mit,
// und der Pin ließe sich dann nicht prüfen. // 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; if (!pin) return basis;
return (optionen, rueckruf) => { return (optionen, rueckruf) => {
+4 -2
View File
@@ -98,8 +98,10 @@ export const AusrollAnfrage = z
swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512), swapMb: z.coerce.number().int().min(0).max(1_048_576).default(512),
unprivilegiert: z.boolean().default(true), unprivilegiert: z.boolean().default(true),
features: z features: z
.object({ nesting: z.boolean().default(false), keyctl: z.boolean().default(false) }) // nesting an wie in der Proxmox-Oberfläche: neuere systemd-Versionen brauchen es im
.default({ nesting: false, keyctl: false }), // 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, netz: Netz,
zugang: Zugang, zugang: Zugang,
profile: z.array(z.string()).default(["base"]), profile: z.array(z.string()).default(["base"]),
+28
View File
@@ -275,3 +275,31 @@ describe("Proxmox-Tasks (UPID)", () => {
expect(fehler.hinweis).toContain("TASK ERROR"); 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");
});
});