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:
@@ -0,0 +1,34 @@
|
||||
import type { Server } from "@/generated/prisma/client";
|
||||
import type { NodeBefehl } from "@/engine/ablaeufe/template-bau";
|
||||
import type { VerbindungsBericht } from "@/engine/proxmox/erkundung";
|
||||
import { sshAusfuehren } from "@/engine/ssh/verbindung";
|
||||
import { entschluesseln } from "@/lib/geheimnisse";
|
||||
|
||||
/**
|
||||
* SSH zu einem Node – nur, wenn alles bestätigt ist: SSH eingeschaltet, Schlüssel vorhanden,
|
||||
* Host-Key des Nodes gepinnt. Sonst null, und die Engine weicht aus oder sagt, was fehlt.
|
||||
*/
|
||||
export function nodeBefehlFuer(server: Server, knoten: string): NodeBefehl | null {
|
||||
if (!server.sshAktiv || !server.sshPrivat) return null;
|
||||
const pin = (server.sshHostSchluessel as Record<string, string> | null)?.[knoten];
|
||||
if (!pin) return null;
|
||||
const bericht = server.letzterTest as VerbindungsBericht | null;
|
||||
const eintrag = bericht?.knoten.find((k) => k.name === knoten);
|
||||
const host =
|
||||
eintrag?.ssh?.host ??
|
||||
eintrag?.ip ??
|
||||
(bericht?.knoten.length === 1 ? new URL(server.url).hostname : knoten);
|
||||
const privat = entschluesseln(server.sshPrivat);
|
||||
return async (befehl, optionen = {}) =>
|
||||
await sshAusfuehren(
|
||||
{
|
||||
host,
|
||||
port: server.sshPort,
|
||||
benutzer: server.sshBenutzer,
|
||||
privatSchluessel: privat,
|
||||
hostSchluessel: pin,
|
||||
},
|
||||
befehl,
|
||||
{ eingabe: optionen.eingabe, zeitlimitMs: optionen.zeitlimitMs },
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user