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)
### 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)
+2 -2
View File
@@ -503,13 +503,13 @@
},
"features": {
"default": {
"nesting": false,
"nesting": true,
"keyctl": false
},
"type": "object",
"properties": {
"nesting": {
"default": false,
"default": true,
"type": "boolean"
},
"keyctl": {
+1 -1
View File
@@ -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")
}
+2 -2
View File
@@ -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")}
+14 -5
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";
/**
@@ -53,10 +53,19 @@ export async function aufgabeAbwarten(
};
for (;;) {
const status = await client.get<TaskStatus>(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<TaskStatus>(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;
+4 -1
View File
@@ -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}`;
}
+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,
// 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) => {
+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),
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"]),
+28
View File
@@ -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");
});
});