feat(templates): Template-Bau für VMs mit eingebautem Guest-Agent

Cloud-Image laden (download-url mit Prüfsumme, ab PVE 8.4 per API, davor
per SSH), VM mit q35/UEFI/virtio-scsi-single anlegen, Disk per
import-from importieren, Guest-Agent mit einem einmaligen
Vorbereitungsstart einbauen und als Template markieren.

Auf pve-demo (PVE 9.2.2) gebaut: Template 9000 aus Debian 13 in
5 min 26 s, Snippet und cicustom danach entfernt. Der Weg ohne SSH über
eine Bau-Adresse ist als #021 ausgegliedert.

Templates-Seite mit Liste (Proxmox ist maßgeblich, Tag
proxdeploy-template) und Formular; Abfrageschleifen erscheinen im
Detail-Log nur noch bei Fehlern.

Closes #007

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 10:57:11 +02:00
co-authored by Claude Opus 5.5
parent 76bc0e4269
commit 4285785a4c
14 changed files with 1252 additions and 17 deletions
+78
View File
@@ -0,0 +1,78 @@
import { phasenAusfuehren } from "@/engine/ablauf/ablauf";
import { templateBauSchritte, templateName, type TemplateBauStand } from "@/engine/ablaeufe/template-bau";
import type { Distribution } from "@/engine/katalog/schema";
import { ProxmoxClient } from "@/engine/proxmox/client";
import { prisma } from "@/lib/db";
import { TemplateBauAnfrage } from "@/lib/schemas/template";
import { melden } from "@/server/ereignisse/senke";
import { verbindungAus } from "@/server/services/server";
import { nodeBefehlFuer } from "@/server/services/ssh-zugang";
import { vmidReservieren } from "@/server/services/vmid";
import { ablaufRegistrieren } from "../ausfuehren";
import { aufrufLogger } from "../aufruf-log";
ablaufRegistrieren("TEMPLATE_BAUEN", async ({ auftrag, senke, abbruchGewuenscht, angelegt }) => {
const anfrage = TemplateBauAnfrage.parse(auftrag.anfrage);
const server = await prisma.server.findUniqueOrThrow({ where: { id: auftrag.serverId } });
const zeile = await prisma.distribution.findUniqueOrThrow({ where: { id: anfrage.distribution } });
const distribution = zeile.definition as Distribution;
await prisma.auftrag.update({
where: { id: auftrag.id },
data: { kopf: `Server ${server.name} · Node ${anfrage.knoten} · Template ${distribution.name}` },
});
await melden(auftrag.id);
const client = new ProxmoxClient(verbindungAus(server), { beobachter: aufrufLogger(senke) });
const stand: TemplateBauStand = {};
try {
const ergebnis = await phasenAusfuehren(
templateBauSchritte(
{
client,
distribution,
knoten: anfrage.knoten,
storage: anfrage.storage,
bridge: anfrage.bridge,
vlan: anfrage.vlan,
ssh: nodeBefehlFuer(server, anfrage.knoten),
auftragKurzId: auftrag.kurzId,
vmidReservieren: async (belegt) => {
const vmid = await vmidReservieren({
serverId: server.id,
auftragId: auftrag.id,
belegt,
von: server.templateVmidVon,
bis: server.templateVmidBis,
});
await prisma.auftrag.update({ where: { id: auftrag.id }, data: { vmid } });
await angelegt({ vmid, knoten: anfrage.knoten, typ: "VM" });
return vmid;
},
},
stand,
),
senke,
{ abbruchGewuenscht },
);
if (!ergebnis.ok) return ergebnis;
await prisma.template.create({
data: {
serverId: server.id,
knoten: anfrage.knoten,
vmid: stand.vmid!,
distributionId: distribution.id,
storage: anfrage.storage,
imageDatei: stand.imageDatei!,
pruefsumme: stand.pruefsumme!,
gebautVonAuftragId: auftrag.id,
},
});
return {
ok: true,
ergebnis: { vmid: stand.vmid, template: templateName(distribution), knoten: anfrage.knoten },
};
} finally {
await client.schliessen();
}
});