import type { Distribution, Profil } from "@/engine/katalog/schema"; import { profileAufloesen, type AufgeloestesProfil } from "@/engine/profile/aufloesen"; 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.`); } await templateProfileAufloesen(anfrage.profile, (distribution.definition as Distribution).family); return await auftragAnlegen( { art: "TEMPLATE_BAUEN", serverId: server.id, knoten: anfrage.knoten, gastTyp: "VM", distributionId: distribution.id, name: `Template ${distribution.name}`, anfrage, }, aufrufer, ip, ); } /** * Profile fürs Template: aufgelöst mit Standardwerten. Ein Profil mit Pflichtvariable (z. B. der * Zabbix-Server) ist gerätespezifisch und gehört nicht ins Template. */ export async function templateProfileAufloesen( auswahl: string[], familie: string, ): Promise { if (auswahl.length === 0) return []; const verfuegbar = new Map( (await prisma.profil.findMany({ where: { aktiv: true, entferntAm: null } })).map((p) => [ p.id, p.definition as Profil, ]), ); const aufgeloest = profileAufloesen(auswahl, verfuegbar, familie); if (!aufgeloest.ok) { throw new AnwendungsFehler( 400, "ungueltig", aufgeloest.fehler.map((f) => `${f.profil}: ${f.meldung}`).join(" "), "Profile mit Pflichtvariablen gehören nicht ins Template – sie laufen beim Ausrollen.", ); } return aufgeloest.profile; } export type TemplateSicht = { vmid: number; name: string; knoten: string; distributionId: string | null; gebautAm: string | null; imageDatei: string | null; profile: string[]; }; /** * 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 { 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" && !t.startsWith("profil-")) ?? null, gebautAm: db?.gebautAm.toISOString() ?? null, imageDatei: db?.imageDatei ?? null, profile: tags.filter((t) => t.startsWith("profil-")).map((t) => t.slice("profil-".length)), }; }) .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(); } }