Files
proxdeploy/src/server/services/templates.ts
T
florian.wildeandClaude Opus 5.5 00712dc6ce 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>
2026-10-12 14:20:32 +02:00

125 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}