feat(templates): Profile ins Template einbacken

Beim Template-Bau lassen sich Profile wählen (nur solche ohne
Pflichtvariablen). Nach dem Vorbereitungsstart laufen sie über den
Guest-Agent; danach werden Maschinen-ID, SSH-Host-Keys, Docker-Engine-ID
und apt-Cache entfernt. Ausrollen nimmt das Template mit den meisten
passenden Profilen und überspringt, was schon eingebacken ist.

Auf pve-demo: Provisionierung mit base+docker 141 s ohne, ~30 s mit
Template. Dabei gefunden: docker nimmt den Benutzer per guest.user in
die Gruppe auf – Profile mit guest.* laufen deshalb beim Deploy noch
einmal (idempotent, schnell).

Closes #022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 14:20:32 +02:00
co-authored by Claude Opus 5.5
parent ebcf63100a
commit 00712dc6ce
16 changed files with 541 additions and 154 deletions
+60
View File
@@ -300,3 +300,63 @@ describe("Template-Bau", () => {
]);
});
});
describe("Profile einbacken (#022)", () => {
it("startet die VM, provisioniert über den Agent, räumt auf, fährt herunter – Name und Tags nennen die Profile", async () => {
const { mock, client } = await aufbauen({ version: "9.2.1" });
const dateien: string[] = [];
let exec = 0;
mock
.route("POST", /\/agent\/ping$/, () => ({ data: { result: {} } }))
.route("POST", /\/agent\/file-write$/, (a) => {
dateien.push(Buffer.from(a.rumpf.get("content")!, "base64").toString("utf8"));
return { data: null };
})
.route("POST", /\/agent\/exec$/, () => ({ data: { pid: ++exec } }))
.route("GET", /\/agent\/exec-status$/, () => ({ data: { exited: 1, exitcode: 0 } }))
.route("GET", /\/agent\/file-read$/, () => ({ data: { content: "" } }))
.route("POST", /\/status\/shutdown$/, () => ({ data: UPID("qmshutdown") }));
const katalog = await katalogLaden("catalog");
const { profileAufloesen } = await import("@/engine/profile/aufloesen");
const aufgeloest = profileAufloesen(
["docker"],
new Map(katalog.profile.map((p) => [p.eintrag.id, p.eintrag])),
"debian",
);
if (!aufgeloest.ok) throw new Error("Profile");
const ergebnis = await phasenAusfuehren(
templateBauSchritte({
client,
distribution: await debian13(),
profile: aufgeloest.profile,
knoten: "pve01",
storage: "local-zfs",
bridge: "vmbr0",
ssh: sshAufzeichnung().ssh,
auftragKurzId: "t",
holen: async () => `${HASH} debian-13-genericcloud-amd64.qcow2`,
vmidReservieren: async () => 9002,
warten: async () => {},
}),
stumm,
);
expect(ergebnis).toEqual({ ok: true });
const anlegen = Object.fromEntries(
mock.anfragen.find((a) => a.methode === "POST" && a.pfad === "/nodes/pve01/qemu")!.rumpf,
);
expect(anlegen).toMatchObject({
name: "tpl-debian-13-docker",
tags: "proxdeploy-template;debian-13;profil-base;profil-docker",
});
expect(dateien.join("")).toContain("docker-compose-plugin");
expect(dateien.join("")).toContain("truncate -s 0 /etc/machine-id");
expect(dateien.join("")).toContain("rm -f /etc/ssh/ssh_host_*");
const reihenfolge = schreibend(mock.anfragen).filter((z) => /status\/(start|shutdown)|template$/.test(z));
expect(reihenfolge).toEqual([
"POST /nodes/pve01/qemu/9002/status/start",
"POST /nodes/pve01/qemu/9002/status/start",
"POST /nodes/pve01/qemu/9002/status/shutdown",
"POST /nodes/pve01/qemu/9002/template",
]);
});
});