feat(templates): Template-Bau ohne SSH über eine Bau-Adresse

Ohne SSH zum Node kommt der Guest-Agent jetzt über eine feste
Bau-Adresse ins Template: cloud-init setzt Adresse und den
Bereitstellungsschlüssel für den Standardbenutzer, ProxDeploy meldet
sich per SSH in der VM an, installiert den Agent und entfernt danach
Schlüssel, Adresse und cloud-init-Angaben.

Dabei korrigiert: der Verbindungstest meldete „Template-Bau per API“
auch ohne Weg, den Agent einzubauen. Im Protokoll erscheinen
unveränderte JSON-Felder nicht mehr als geändert.

Refs #021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 14:36:38 +02:00
co-authored by Claude Opus 5.5
parent 426d3ec95a
commit 21edff2141
14 changed files with 366 additions and 28 deletions
+8 -1
View File
@@ -8,6 +8,7 @@ import { profileAufloesen, type AufgeloestesProfil } from "@/engine/profile/aufl
import { ProxmoxClient, type Aufruf } from "@/engine/proxmox/client";
import { prisma } from "@/lib/db";
import { AusrollAnfrage, cliBefehl } from "@/lib/schemas/ausrollen";
import type { BauNetz } from "@/lib/schemas/server";
import { vorlageAnwenden } from "@/lib/schemas/vorlage";
import { vorlageHolen } from "@/server/services/vorlagen";
import type { Aufrufer } from "@/server/auth/zugang";
@@ -162,7 +163,13 @@ export function ausrollSchritte(
profile: v.profile,
vmidReservieren: o.vmidReservieren,
templateBau: v.anfrage.templateBauen
? { sshFuer, auftragKurzId: o.auftragKurzId, vmidReservieren: o.templateVmidReservieren }
? {
sshFuer,
auftragKurzId: o.auftragKurzId,
vmidReservieren: o.templateVmidReservieren,
bauNetz: (v.server.bauNetz as BauNetz | null) ?? null,
bereitstellung: bereitstellungsschluessel,
}
: undefined,
angelegt: o.angelegt,
auftragKurzId: o.auftragKurzId,
+13 -2
View File
@@ -1,5 +1,5 @@
import ssh2 from "ssh2";
import type { Server } from "@/generated/prisma/client";
import { Prisma, type Server } from "@/generated/prisma/client";
import { ProxmoxClient, type ProxmoxVerbindung } from "@/engine/proxmox/client";
import { erkunden, type VerbindungsBericht } from "@/engine/proxmox/erkundung";
import { ProxmoxFehler } from "@/engine/proxmox/fehler";
@@ -11,6 +11,7 @@ import {
ServerAendern,
ServerAnlegen,
ServerPruefen,
type BauNetz,
type ServerSicht,
} from "@/lib/schemas/server";
import { AnwendungsFehler, nichtGefunden } from "@/server/fehler";
@@ -32,6 +33,7 @@ export function serverSicht(server: Server): ServerSicht {
templateVmidVon: server.templateVmidVon,
templateVmidBis: server.templateVmidBis,
gastVmidVon: server.gastVmidVon,
bauNetz: (server.bauNetz as BauNetz | null) ?? null,
erreichbar: server.erreichbar,
pveVersion: server.pveVersion,
geprueftAm: server.geprueftAm?.toISOString() ?? null,
@@ -83,6 +85,7 @@ export async function serverAnlegen(
const angelegt = await tx.server.create({
data: {
...eingabe,
bauNetz: eingabe.bauNetz ?? Prisma.JsonNull,
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
tokenGeheimnis: verschluesseln(eingabe.tokenGeheimnis),
},
@@ -117,12 +120,19 @@ export async function serverAendern(
const { tokenGeheimnis, ...rest } = eingabe;
const daten = {
...rest,
bauNetz: rest.bauNetz ?? Prisma.JsonNull,
fingerabdruck: fingerabdruckOderFehler(eingabe.fingerabdruck),
...(tokenGeheimnis ? { tokenGeheimnis: verschluesseln(tokenGeheimnis) } : {}),
};
// Welche Felder sich geändert haben, steht im Protokoll – die Werte von Geheimnissen nie.
// JSON-Felder (bauNetz) vergleichen nach Inhalt; Prisma.JsonNull steht für „leer“ wie null.
const vergleichbar = (wert: unknown) => JSON.stringify(wert === Prisma.JsonNull ? null : (wert ?? null));
const geaendert = Object.entries(daten)
.filter(([feld, wert]) => feld === "tokenGeheimnis" || (bisher as Record<string, unknown>)[feld] !== wert)
.filter(
([feld, wert]) =>
feld === "tokenGeheimnis" ||
vergleichbar((bisher as Record<string, unknown>)[feld]) !== vergleichbar(wert),
)
.map(([feld]) => feld);
const server = await prisma.$transaction(async (tx) => {
@@ -287,6 +297,7 @@ export async function serverTesten(
try {
const bericht = await erkunden(client, {
serverHost: new URL(server.url).hostname,
bauNetz: Boolean(server.bauNetz),
ssh: server.sshAktiv
? {
benutzer: server.sshBenutzer,