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:
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}` };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }> = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user