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>
125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
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<AufgeloestesProfil[]> {
|
||
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<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" && !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();
|
||
}
|
||
}
|