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
+90
View File
@@ -0,0 +1,90 @@
import type { Distribution } from "@/engine/katalog/schema";
import { ProxmoxClient } from "@/engine/proxmox/client";
import { ProxmoxFehler } from "@/engine/proxmox/fehler";
import { prisma } from "@/lib/db";
import { TemplateBauAnfrage } from "@/lib/schemas/template";
import type { Aufrufer } from "@/server/auth/zugang";
import { AnwendungsFehler } from "@/server/fehler";
import { auftragAnlegen } from "@/server/services/auftraege";
import { serverHolen, verbindungAus } from "@/server/services/server";
export async function templateBauStarten(
serverName: string,
roh: unknown,
aufrufer: Aufrufer,
ip: string | null,
) {
const server = await serverHolen(serverName);
const anfrage = TemplateBauAnfrage.parse(roh);
const distribution = await prisma.distribution.findUnique({ where: { id: anfrage.distribution } });
if (!distribution || distribution.entferntAm || !distribution.aktiv) {
throw new AnwendungsFehler(
400,
"ungueltig",
`Die Distribution „${anfrage.distribution}“ ist nicht verfügbar.`,
);
}
if (!(distribution.definition as Distribution).vm) {
throw new AnwendungsFehler(400, "ungueltig", `${distribution.name} bietet kein VM-Image.`);
}
return await auftragAnlegen(
{
art: "TEMPLATE_BAUEN",
serverId: server.id,
knoten: anfrage.knoten,
gastTyp: "VM",
distributionId: distribution.id,
name: `Template ${distribution.name}`,
anfrage,
},
aufrufer,
ip,
);
}
export type TemplateSicht = {
vmid: number;
name: string;
knoten: string;
distributionId: string | null;
gebautAm: string | null;
imageDatei: string | null;
};
/**
* Templates eines Servers – Proxmox ist maßgeblich (Tag `proxdeploy-template`), die Datenbank
* ergänzt, woraus gebaut wurde. Ein in Proxmox gelöschtes Template verschwindet so auch hier.
*/
export async function templatesListe(serverName: string): Promise<TemplateSicht[]> {
const server = await serverHolen(serverName);
const client = new ProxmoxClient(verbindungAus(server), { zeitlimitMs: 10_000 });
try {
const ressourcen = await client.get<
Array<{ vmid: number; name?: string; node: string; template?: number; tags?: string }>
>("/cluster/resources", { type: "vm" });
const bekannt = new Map(
(await prisma.template.findMany({ where: { serverId: server.id } })).map((t) => [t.vmid, t]),
);
return ressourcen
.filter((r) => r.template === 1 && (r.tags ?? "").split(/[;, ]/).includes("proxdeploy-template"))
.map((r) => {
const db = bekannt.get(r.vmid);
const tags = (r.tags ?? "").split(/[;, ]/);
return {
vmid: r.vmid,
name: r.name ?? `VM ${r.vmid}`,
knoten: r.node,
distributionId: db?.distributionId ?? tags.find((t) => t !== "proxdeploy-template") ?? null,
gebautAm: db?.gebautAm.toISOString() ?? null,
imageDatei: db?.imageDatei ?? null,
};
})
.sort((a, b) => a.vmid - b.vmid);
} catch (fehler) {
if (fehler instanceof ProxmoxFehler)
throw new AnwendungsFehler(502, "proxmox", fehler.ursache, fehler.hinweis);
throw fehler;
} finally {
await client.schliessen();
}
}