fix(ablauf): angelegte ID merken, bevor auf Proxmox gewartet wird

Brach das Warten auf Klonen oder Anlegen ab, legte Proxmox den Gast trotzdem fertig an – und „Aufräumen“ wusste nichts von ihm (CT 114 im Feldtest).

Refs #020

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 16:02:57 +02:00
co-authored by Claude Opus 5.5
parent bc13b94c7e
commit ca9d6de446
4 changed files with 64 additions and 50 deletions
+1
View File
@@ -14,6 +14,7 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
- 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) - 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
- „Aufräumen“ kannte einen Gast nicht, wenn das Warten auf Klonen bzw. Anlegen abbrach, Proxmox ihn aber fertig anlegte – die ID wird jetzt gemerkt, sobald Proxmox den Auftrag angenommen hat (#020)
- 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) - 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)
+23 -24
View File
@@ -245,31 +245,30 @@ export function lxcAusrollSchritte(e: LxcAusrollEingabe, stand: LxcAusrollStand
ausfuehren: async () => { ausfuehren: async () => {
if (stand.weg === "gast") bereitstellung = await e.bereitstellung(); if (stand.weg === "gast") bereitstellung = await e.bereitstellung();
const featureListe = [features.nesting ? "nesting=1" : null].filter(Boolean).join(","); const featureListe = [features.nesting ? "nesting=1" : null].filter(Boolean).join(",");
await aufgabeAbwarten( const upid = await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, {
client, vmid: stand.vmid!,
await client.post<string>(pfad`/nodes/${stand.knoten!}/lxc`, { hostname: a.name,
vmid: stand.vmid!, ostemplate: stand.ostemplate!,
hostname: a.name, ostype: lxc!.ostype,
ostemplate: stand.ostemplate!, rootfs: `${a.storage}:${a.diskGb}`,
ostype: lxc!.ostype, cores: a.cores,
rootfs: `${a.storage}:${a.diskGb}`, memory: a.speicherMb,
cores: a.cores, swap: a.swapMb,
memory: a.speicherMb, unprivileged: a.unprivilegiert,
swap: a.swapMb, ...(featureListe ? { features: featureListe } : {}),
unprivileged: a.unprivilegiert, net0: lxcNetz(a.netz),
...(featureListe ? { features: featureListe } : {}), ...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}),
net0: lxcNetz(a.netz), ...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}),
...(a.netz.dns.length > 0 ? { nameserver: a.netz.dns.join(" ") } : {}), // Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel.
...(a.netz.suchdomain ? { searchdomain: a.netz.suchdomain } : {}), ...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}),
// Nur der Bereitstellungsschlüssel kommt an root – der Benutzer bekommt seine eigenen Schlüssel. onboot: a.startBeiBoot,
...(bereitstellung ? { "ssh-public-keys": bereitstellung.oeffentlich } : {}), tags: ["proxdeploy", d.id, ...a.tags].join(";"),
onboot: a.startBeiBoot, description: `Ausgerollt mit ProxDeploy (Auftrag ${e.auftragKurzId})\nDistribution: ${d.name}\nProfile: ${e.profile.map((p) => p.profil.id).join(", ") || "–"}`,
tags: ["proxdeploy", d.id, ...a.tags].join(";"), });
description: `Ausgerollt mit ProxDeploy (Auftrag ${e.auftragKurzId})\nDistribution: ${d.name}\nProfile: ${e.profile.map((p) => p.profil.id).join(", ") || "–"}`, // Sofort merken, nicht erst nach dem Warten: bricht das Warten ab, legt Proxmox den
}), // Container trotzdem fertig an – „Aufräumen“ muss ihn dann kennen.
{ zeitlimitMs: 15 * 60 * 1000 },
);
await e.angelegt({ vmid: stand.vmid }); await e.angelegt({ vmid: stand.vmid });
await aufgabeAbwarten(client, upid, { zeitlimitMs: 15 * 60 * 1000 });
if (features.keyctl) { if (features.keyctl) {
const ssh = e.sshFuer(stand.knoten!)!; const ssh = e.sshFuer(stand.knoten!)!;
if (!client.trocken) { if (!client.trocken) {
+25 -26
View File
@@ -234,32 +234,32 @@ export function vmAusrollSchritte(e: VmAusrollEingabe, stand: VmAusrollStand = {
titel: "Klonen", titel: "Klonen",
ausfuehren: async (k) => { ausfuehren: async (k) => {
const tpl = stand.template!; const tpl = stand.template!;
const klonen = async (vmid: number) => const klonen = async (vmid: number) => {
await aufgabeAbwarten( const upid = await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, {
client, newid: vmid,
await client.post<string>(pfad`/nodes/${tpl.knoten}/qemu/${tpl.vmid}/clone`, { name: a.name,
newid: vmid, // Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört.
name: a.name, description: beschreibung(),
// Schon beim Klonen: daran erkennt „Aufräumen“, dass die VM zu diesem Auftrag gehört. full: true,
description: beschreibung(), target: stand.knoten!,
full: true, storage: a.storage,
target: stand.knoten!, });
storage: a.storage, // Sofort merken, nicht erst nach dem Warten: der Klon läuft auf dem Node weiter, auch
}), // wenn das Warten abbricht – „Aufräumen“ muss die VM dann kennen.
{ await e.angelegt({ vmid });
zeitlimitMs: 60 * 60 * 1000, await aufgabeAbwarten(client, upid, {
intervallMs: 2000, zeitlimitMs: 60 * 60 * 1000,
// „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon. intervallMs: 2000,
beiLog: (zeilen) => { // „transferred 1.2 GiB of 3.5 GiB (35.16%)“ – auf langsamen Storages dauert ein Voll-Klon.
const letzte = zeilen beiLog: (zeilen) => {
.map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z)) const letzte = zeilen
.filter(Boolean) .map((z) => /transferred (.+) of (.+) \((\d+(?:\.\d+)?)%\)/.exec(z))
.at(-1); .filter(Boolean)
if (letzte) .at(-1);
void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`); if (letzte) void k.detail(`${letzte[1]} von ${letzte[2]} (${Math.round(Number(letzte[3]))} %)`);
},
}, },
); });
};
try { try {
await klonen(stand.vmid!); await klonen(stand.vmid!);
} catch (fehler) { } catch (fehler) {
@@ -273,7 +273,6 @@ export function vmAusrollSchritte(e: VmAusrollEingabe, stand: VmAusrollStand = {
await k.log(`VMID war inzwischen belegt – neuer Versuch mit ${stand.vmid}`, "warn"); await k.log(`VMID war inzwischen belegt – neuer Versuch mit ${stand.vmid}`, "warn");
await klonen(stand.vmid); await klonen(stand.vmid);
} }
await e.angelegt({ vmid: stand.vmid });
return { detail: `full clone ${tpl.vmid} → ${stand.vmid}` }; return { detail: `full clone ${tpl.vmid} → ${stand.vmid}` };
}, },
}, },
+15
View File
@@ -127,6 +127,21 @@ async function eingabe(
} }
describe("LXC ausrollen", () => { describe("LXC ausrollen", () => {
it("merkt sich die CTID schon nach dem Absenden – auch wenn die Anlage danach scheitert", async () => {
const { mock, client } = await aufbauen({ templateLokal: ["debian-13-standard_13.6-1_amd64.tar.zst"] });
mock.route("GET", /\/tasks\/[^/]+vzcreate[^/]*\/status$/, () => ({
data: { status: "stopped", exitstatus: "unable to create CT 130 - storage full" },
}));
const { e } = await eingabe(client);
const gemerkt: Array<Record<string, unknown>> = [];
const ergebnis = await phasenAusfuehren(
lxcAusrollSchritte({ ...e, angelegt: async (x) => void gemerkt.push(x) }),
stumm,
);
expect(ergebnis.ok).toBe(false);
expect(gemerkt).toContainEqual({ vmid: 130 });
});
it("mit Node-SSH: neuestes Template laden, anlegen mit nesting (docker), DHCP-IP, pct exec mit Skript auf stdin", async () => { it("mit Node-SSH: neuestes Template laden, anlegen mit nesting (docker), DHCP-IP, pct exec mit Skript auf stdin", async () => {
const { mock, client } = await aufbauen({ dhcpNachAbfragen: 2 }); const { mock, client } = await aufbauen({ dhcpNachAbfragen: 2 });
const befehle: Array<{ befehl: string; eingabe?: string }> = []; const befehle: Array<{ befehl: string; eingabe?: string }> = [];