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:
@@ -43,6 +43,9 @@ type Werte = {
|
||||
templateVmidVon: string;
|
||||
templateVmidBis: string;
|
||||
gastVmidVon: string;
|
||||
bauIp: string;
|
||||
bauGateway: string;
|
||||
bauDns: string;
|
||||
};
|
||||
|
||||
function anfangswerte(server?: ServerSicht): Werte {
|
||||
@@ -60,6 +63,9 @@ function anfangswerte(server?: ServerSicht): Werte {
|
||||
templateVmidVon: String(server?.templateVmidVon ?? 9000),
|
||||
templateVmidBis: String(server?.templateVmidBis ?? 9999),
|
||||
gastVmidVon: String(server?.gastVmidVon ?? 100),
|
||||
bauIp: server?.bauNetz?.ip ?? "",
|
||||
bauGateway: server?.bauNetz?.gateway ?? "",
|
||||
bauDns: server?.bauNetz?.dns.join(", ") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,12 +123,16 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
|
||||
setSpeichert(true);
|
||||
setFelder({});
|
||||
setMeldung(null);
|
||||
const { bauIp, bauGateway, bauDns, ...ohneBau } = werte;
|
||||
const rumpf = {
|
||||
...werte,
|
||||
...ohneBau,
|
||||
sshPort: Number(werte.sshPort),
|
||||
templateVmidVon: Number(werte.templateVmidVon),
|
||||
templateVmidBis: Number(werte.templateVmidBis),
|
||||
gastVmidVon: Number(werte.gastVmidVon),
|
||||
bauNetz: bauIp.trim()
|
||||
? { ip: bauIp.trim(), gateway: bauGateway.trim(), dns: bauDns.split(/[\s,;]+/).filter(Boolean) }
|
||||
: null,
|
||||
};
|
||||
try {
|
||||
const gespeichert = bearbeiten
|
||||
@@ -361,6 +371,44 @@ export function ServerFormular({ server }: { server?: ServerSicht }) {
|
||||
</Feld>
|
||||
</div>
|
||||
|
||||
<fieldset className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-medium">Bau-Adresse (optional)</legend>
|
||||
<p className="text-muted text-sm">
|
||||
Feste Adresse für den Vorbereitungsstart beim Template-Bau. Nur nötig ohne SSH zum Node:
|
||||
ProxDeploy meldet sich darüber kurz in der VM an und installiert den Guest-Agent. Braucht
|
||||
Internetzugang.
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<Feld id="bauIp" label="IP/Präfix" fehler={felder["bauNetz.ip"]}>
|
||||
<input
|
||||
id="bauIp"
|
||||
className={`${EINGABE_KLASSE} font-mono`}
|
||||
value={werte.bauIp}
|
||||
onChange={(e) => setze("bauIp", e.target.value)}
|
||||
placeholder="10.0.10.250/24"
|
||||
/>
|
||||
</Feld>
|
||||
<Feld id="bauGateway" label="Gateway" fehler={felder["bauNetz.gateway"]}>
|
||||
<input
|
||||
id="bauGateway"
|
||||
className={`${EINGABE_KLASSE} font-mono`}
|
||||
value={werte.bauGateway}
|
||||
onChange={(e) => setze("bauGateway", e.target.value)}
|
||||
placeholder="10.0.10.1"
|
||||
/>
|
||||
</Feld>
|
||||
<Feld id="bauDns" label="DNS" fehler={felder["bauNetz.dns"]}>
|
||||
<input
|
||||
id="bauDns"
|
||||
className={`${EINGABE_KLASSE} font-mono`}
|
||||
value={werte.bauDns}
|
||||
onChange={(e) => setze("bauDns", e.target.value)}
|
||||
placeholder="10.0.10.1"
|
||||
/>
|
||||
</Feld>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<Schalter
|
||||
id="sshAktiv"
|
||||
label="SSH zu den Nodes nutzen"
|
||||
|
||||
@@ -6,6 +6,9 @@ import { aufgabeAbwarten } from "@/engine/proxmox/aufgaben";
|
||||
import { pfad, type ProxmoxClient } from "@/engine/proxmox/client";
|
||||
import { mindestens, versionLesen } from "@/engine/proxmox/faehigkeiten";
|
||||
import { q } from "@/engine/shell";
|
||||
import { SshFehler, sshAusfuehren } from "@/engine/ssh/verbindung";
|
||||
import type { BauNetz } from "@/lib/schemas/server";
|
||||
import type { Bereitstellungsschluessel } from "./lxc-ausrollen";
|
||||
import { adapterFuer } from "@/engine/adapter";
|
||||
import type { AufgeloestesProfil } from "@/engine/profile/aufloesen";
|
||||
import { profileRendern } from "@/engine/profile/rendern";
|
||||
@@ -36,6 +39,14 @@ export type TemplateBauEingabe = {
|
||||
distribution: Distribution;
|
||||
/** Eingebackene Profile (#022), aufgelöst mit Standardwerten; leer = nur Guest-Agent. */
|
||||
profile?: AufgeloestesProfil[];
|
||||
/** Bau-Adresse (#021): Guest-Agent per SSH in die VM einbauen, wenn es kein Node-SSH gibt. */
|
||||
bauNetz?: BauNetz | null;
|
||||
bereitstellung?: () => Promise<Bereitstellungsschluessel>;
|
||||
/** Nur für Tests: SSH in die Vorbereitungs-VM ersetzen. */
|
||||
gastSsh?: (
|
||||
ziel: { host: string; benutzer: string; schluessel: Bereitstellungsschluessel },
|
||||
skript: string,
|
||||
) => Promise<number | null>;
|
||||
knoten: string;
|
||||
/** Ziel für Disk, EFI- und cloud-init-Laufwerk (Inhaltstyp images). */
|
||||
storage: string;
|
||||
@@ -56,6 +67,8 @@ export type TemplateBauEingabe = {
|
||||
|
||||
export type TemplateBauStand = {
|
||||
vmid?: number;
|
||||
/** Wie der Guest-Agent hineinkommt: Snippet (Node-SSH) oder SSH in die VM über die Bau-Adresse. */
|
||||
agentWeg?: "snippet" | "baunetz";
|
||||
weg?: "api" | "ssh";
|
||||
importStorage?: string;
|
||||
snippetStorage?: string;
|
||||
@@ -131,6 +144,126 @@ power_state:
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guest-Agent ohne Node-SSH (#021): Vorbereitungsstart mit fester Bau-Adresse und dem
|
||||
* Bereitstellungsschlüssel für den Standardbenutzer des Images, dann SSH in die VM. Danach wird
|
||||
* der Schlüssel entfernt und die cloud-init-Angaben zurückgesetzt – im Template bleibt nichts davon.
|
||||
*/
|
||||
export const BAUNETZ_SKRIPT = `set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
cloud-init status --wait >/dev/null 2>&1 || true
|
||||
apt-get -q update
|
||||
apt-get -y -q -o DPkg::Lock::Timeout=600 install qemu-guest-agent
|
||||
systemctl enable qemu-guest-agent
|
||||
rm -f /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys
|
||||
truncate -s 0 /etc/machine-id
|
||||
rm -f /var/lib/dbus/machine-id
|
||||
cloud-init clean --logs
|
||||
sync
|
||||
`;
|
||||
|
||||
async function agentUeberBauNetz(
|
||||
e: TemplateBauEingabe,
|
||||
vmPfad: string,
|
||||
benutzer: string,
|
||||
k: PhasenKontext,
|
||||
warten: (ms: number) => Promise<void>,
|
||||
): Promise<{ detail: string }> {
|
||||
const { client } = e;
|
||||
const netz = e.bauNetz!;
|
||||
const schluessel = await e.bereitstellung!();
|
||||
const host = netz.ip.split("/")[0]!;
|
||||
await client.put(`${vmPfad}/config`, {
|
||||
ciuser: benutzer,
|
||||
sshkeys: encodeURIComponent(schluessel.oeffentlich).replace(
|
||||
/[!'()*]/g,
|
||||
(z) => `%${z.charCodeAt(0).toString(16).toUpperCase()}`,
|
||||
),
|
||||
ipconfig0: `ip=${netz.ip},gw=${netz.gateway}`,
|
||||
...(netz.dns.length > 0 ? { nameserver: netz.dns.join(" ") } : {}),
|
||||
});
|
||||
try {
|
||||
await aufgabeAbwarten(client, await client.post<string>(`${vmPfad}/status/start`), {
|
||||
zeitlimitMs: 5 * 60 * 1000,
|
||||
});
|
||||
if (client.trocken) return { detail: `über Bau-Adresse ${host}` };
|
||||
await k.detail(`Vorbereitungsstart über Bau-Adresse ${host} …`);
|
||||
const code = await (e.gastSsh ?? gastSshBau)(
|
||||
{ host, benutzer, schluessel },
|
||||
BAUNETZ_SKRIPT,
|
||||
warten,
|
||||
(z) => void k.log(z, "debug"),
|
||||
);
|
||||
if (code !== 0) {
|
||||
throw new AblaufFehler(
|
||||
`Einbau des Guest-Agents über ${host} mit Code ${code} abgebrochen.`,
|
||||
"Hat die Bau-Adresse Internetzugang (Gateway, DNS)? Detail-Log ansehen.",
|
||||
);
|
||||
}
|
||||
await aufgabeAbwarten(client, await client.post<string>(`${vmPfad}/status/shutdown`, { timeout: 180 }), {
|
||||
zeitlimitMs: 5 * 60 * 1000,
|
||||
});
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
if ((await client.get<{ status: string }>(`${vmPfad}/status/current`)).status === "stopped") break;
|
||||
await warten(3000);
|
||||
}
|
||||
} finally {
|
||||
// Bau-Adresse und Schlüssel gehören nicht ins Template.
|
||||
await client
|
||||
.put(`${vmPfad}/config`, { ipconfig0: "ip=dhcp", delete: "sshkeys,ciuser,nameserver" })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
return { detail: `qemu-guest-agent über Bau-Adresse ${host} installiert · Maschinen-ID geleert` };
|
||||
}
|
||||
|
||||
async function gastSshBau(
|
||||
ziel: { host: string; benutzer: string; schluessel: Bereitstellungsschluessel },
|
||||
skript: string,
|
||||
warten: (ms: number) => Promise<void>,
|
||||
beiZeile: (z: string) => void,
|
||||
): Promise<number | null> {
|
||||
// Den Host-Key der frischen VM kann niemand vorher kennen (TOFU, nur für diese Verbindung).
|
||||
const beginn = Date.now();
|
||||
for (;;) {
|
||||
try {
|
||||
const ergebnis = await sshAusfuehren(
|
||||
{
|
||||
host: ziel.host,
|
||||
port: 22,
|
||||
benutzer: ziel.benutzer,
|
||||
privatSchluessel: ziel.schluessel.privat,
|
||||
hostSchluessel: null,
|
||||
},
|
||||
"sudo -n bash -s",
|
||||
{
|
||||
erkunden: true,
|
||||
eingabe: skript,
|
||||
zeitlimitMs: 20 * 60 * 1000,
|
||||
beiAusgabe: (t) => t.split("\n").filter(Boolean).forEach(beiZeile),
|
||||
},
|
||||
);
|
||||
return ergebnis.code;
|
||||
} catch (fehler) {
|
||||
if (
|
||||
fehler instanceof SshFehler &&
|
||||
(fehler.art === "verbindung" || fehler.art === "anmeldung" || fehler.art === "zeitueberschreitung") &&
|
||||
Date.now() - beginn < 4 * 60 * 1000
|
||||
) {
|
||||
// Bootet noch oder cloud-init hat den Schlüssel noch nicht eingetragen.
|
||||
await warten(5000);
|
||||
continue;
|
||||
}
|
||||
if (fehler instanceof SshFehler) {
|
||||
throw new AblaufFehler(
|
||||
`SSH in die Vorbereitungs-VM (${ziel.host}) fehlgeschlagen: ${fehler.ursache}`,
|
||||
"Ist die Bau-Adresse aus dem Netz von ProxDeploy erreichbar? Stimmen Bridge/VLAN?",
|
||||
);
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function templateBauSchritte(e: TemplateBauEingabe, stand: TemplateBauStand = {}): PhasenSchritt[] {
|
||||
const { client, distribution: d, knoten } = e;
|
||||
const vm = d.vm;
|
||||
@@ -215,13 +348,17 @@ export function templateBauSchritte(e: TemplateBauEingabe, stand: TemplateBauSta
|
||||
: "SSH zum Node einrichten oder auf PVE 8.4+ aktualisieren.",
|
||||
);
|
||||
}
|
||||
if (!vm.guest_agent_preinstalled && !(e.ssh && stand.snippetStorage)) {
|
||||
throw new AblaufFehler(
|
||||
"Der Guest-Agent muss ins Template eingebaut werden – dafür braucht ProxDeploy SSH zum Node und einen Storage mit Inhaltstyp „Snippets“.",
|
||||
e.ssh
|
||||
? "Einem Dir-Storage den Inhaltstyp „Snippets“ geben."
|
||||
: "SSH in den Server-Einstellungen einrichten.",
|
||||
);
|
||||
if (!vm.guest_agent_preinstalled) {
|
||||
if (e.ssh && stand.snippetStorage) stand.agentWeg = "snippet";
|
||||
else if (e.bauNetz && e.bereitstellung) stand.agentWeg = "baunetz";
|
||||
else {
|
||||
throw new AblaufFehler(
|
||||
"Der Guest-Agent muss ins Template eingebaut werden – dafür braucht ProxDeploy SSH zum Node (mit einem Snippets-Storage) oder eine Bau-Adresse.",
|
||||
e.ssh
|
||||
? "Einem Dir-Storage den Inhaltstyp „Snippets“ geben oder in den Server-Einstellungen eine Bau-Adresse eintragen."
|
||||
: "SSH in den Server-Einstellungen einrichten oder dort eine Bau-Adresse (feste IP für die Vorbereitungs-VM) eintragen.",
|
||||
);
|
||||
}
|
||||
}
|
||||
const netze = await client.get<Array<{ iface: string; type: string }>>(
|
||||
pfad`/nodes/${knoten}/network`,
|
||||
@@ -375,6 +512,8 @@ export function templateBauSchritte(e: TemplateBauEingabe, stand: TemplateBauSta
|
||||
if (vm.guest_agent_preinstalled) return { uebersprungen: true, detail: "im Image enthalten" };
|
||||
const vmid = stand.vmid!;
|
||||
const vmPfad = pfad`/nodes/${knoten}/qemu/${vmid}`;
|
||||
if (stand.agentWeg === "baunetz")
|
||||
return await agentUeberBauNetz(e, vmPfad, vm.default_user, k, warten);
|
||||
const snippet = `${stand.snippetStorage}:snippets/${snippetDatei}`;
|
||||
await ssh(`set -e; ziel="$(pvesm path ${q(snippet)})"; install -D -m 0644 /dev/stdin "$ziel"`, k, {
|
||||
eingabe: vorbereitungsSnippet(),
|
||||
|
||||
@@ -119,7 +119,7 @@ async function versuchen<T>(warnungen: string[], text: string, aufruf: () => Pro
|
||||
|
||||
export async function erkunden(
|
||||
client: ProxmoxClient,
|
||||
optionen: { serverHost: string; ssh: SshZugang | null; jetzt?: () => Date },
|
||||
optionen: { serverHost: string; ssh: SshZugang | null; bauNetz?: boolean; jetzt?: () => Date },
|
||||
): Promise<VerbindungsBericht> {
|
||||
const warnungen: string[] = [];
|
||||
const version = await client.get<{ version: string; release: string }>("/version");
|
||||
@@ -313,6 +313,7 @@ export async function erkunden(
|
||||
storages: alleStorages,
|
||||
ssh: sshGesamt,
|
||||
rechteFehlen: fehlendeRechte.some((r) => r.pflicht),
|
||||
bauNetz: optionen.bauNetz,
|
||||
}),
|
||||
warnungen,
|
||||
};
|
||||
|
||||
@@ -230,6 +230,8 @@ export function faehigkeitenAbleiten(eingabe: {
|
||||
storages: StorageInfo[];
|
||||
ssh: SshZustand;
|
||||
rechteFehlen: boolean;
|
||||
/** Bau-Adresse gesetzt (#021) – Guest-Agent einbauen geht dann auch ohne SSH zum Node. */
|
||||
bauNetz?: boolean;
|
||||
}): Faehigkeiten {
|
||||
const { version, storages, ssh } = eingabe;
|
||||
const sshOk = ssh === "ok";
|
||||
@@ -237,10 +239,19 @@ export function faehigkeitenAbleiten(eingabe: {
|
||||
const snippetSpeicher = storages.filter((s) => s.aktiv && s.inhalte.includes("snippets"));
|
||||
|
||||
let templateBau: Strategie;
|
||||
if (mindestens(version, 8, 4) && importSpeicher.length > 0) {
|
||||
const agentEinbauen = (sshOk && snippetSpeicher.length > 0) || Boolean(eingabe.bauNetz);
|
||||
if (mindestens(version, 8, 4) && importSpeicher.length > 0 && !agentEinbauen) {
|
||||
templateBau = {
|
||||
weg: "nicht_moeglich",
|
||||
grund:
|
||||
"Image-Import ginge per API, aber der Guest-Agent muss ins Template: SSH zum Node (mit Snippets-Storage) oder eine Bau-Adresse in den Server-Einstellungen.",
|
||||
};
|
||||
} else if (mindestens(version, 8, 4) && importSpeicher.length > 0) {
|
||||
templateBau = {
|
||||
weg: "api",
|
||||
grund: `download-url und import-from über Storage ${importSpeicher.map((s) => s.storage).join(", ")}`,
|
||||
grund: `download-url und import-from über Storage ${importSpeicher.map((s) => s.storage).join(", ")}${
|
||||
sshOk && snippetSpeicher.length > 0 ? "" : "; Guest-Agent über die Bau-Adresse"
|
||||
}`,
|
||||
};
|
||||
} else if (sshOk) {
|
||||
templateBau = {
|
||||
|
||||
@@ -30,6 +30,35 @@ export const ServerUrl = z
|
||||
return `${url.protocol}//${url.host}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Bau-Adresse (#021): eine feste Adresse für den Vorbereitungsstart beim Template-Bau, wenn es
|
||||
* kein SSH zum Node gibt. ProxDeploy meldet sich darüber per SSH in der VM an.
|
||||
*/
|
||||
export const BauNetz = z
|
||||
.object({
|
||||
ip: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/, "Format: 10.0.10.250/24"),
|
||||
gateway: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^(\d{1,3}\.){3}\d{1,3}$/, "Format: 10.0.10.1"),
|
||||
dns: z
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^(\d{1,3}\.){3}\d{1,3}$/),
|
||||
)
|
||||
.max(3)
|
||||
.default([]),
|
||||
})
|
||||
.nullable()
|
||||
.default(null);
|
||||
|
||||
export type BauNetz = NonNullable<z.infer<typeof BauNetz>>;
|
||||
|
||||
const Vmid = z.coerce.number().int("Ganze Zahl erwartet.").min(100, "Mindestens 100.").max(999_999_999);
|
||||
|
||||
const Basis = z.object({
|
||||
@@ -52,6 +81,7 @@ const Basis = z.object({
|
||||
templateVmidVon: Vmid.default(9000),
|
||||
templateVmidBis: Vmid.default(9999),
|
||||
gastVmidVon: Vmid.default(100),
|
||||
bauNetz: BauNetz,
|
||||
});
|
||||
|
||||
function bereicheGueltig<T extends { templateVmidVon: number; templateVmidBis: number }>(wert: T) {
|
||||
@@ -112,6 +142,7 @@ export type ServerSicht = {
|
||||
templateVmidVon: number;
|
||||
templateVmidBis: number;
|
||||
gastVmidVon: number;
|
||||
bauNetz: BauNetz | null;
|
||||
erreichbar: boolean | null;
|
||||
pveVersion: string | null;
|
||||
geprueftAm: string | null;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,9 @@ import { templateBauSchritte, templateName, type TemplateBauStand } from "@/engi
|
||||
import type { Distribution } from "@/engine/katalog/schema";
|
||||
import { ProxmoxClient } from "@/engine/proxmox/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import type { BauNetz } from "@/lib/schemas/server";
|
||||
import { TemplateBauAnfrage } from "@/lib/schemas/template";
|
||||
import { bereitstellungsschluessel } from "@/server/services/bereitstellung";
|
||||
import { melden } from "@/server/ereignisse/senke";
|
||||
import { verbindungAus } from "@/server/services/server";
|
||||
import { nodeBefehlFuer } from "@/server/services/ssh-zugang";
|
||||
@@ -43,6 +45,8 @@ ablaufRegistrieren("TEMPLATE_BAUEN", async ({ auftrag, senke, abbruchGewuenscht,
|
||||
bridge: anfrage.bridge,
|
||||
vlan: anfrage.vlan,
|
||||
ssh: nodeBefehlFuer(server, anfrage.knoten),
|
||||
bauNetz: (server.bauNetz as BauNetz | null) ?? null,
|
||||
bereitstellung: bereitstellungsschluessel,
|
||||
auftragKurzId: auftrag.kurzId,
|
||||
vmidReservieren: async (belegt) => {
|
||||
const vmid = await vmidReservieren({
|
||||
|
||||
Reference in New Issue
Block a user