feat(ausrollen): VM ausrollen mit Assistent, Dry-Run und CLI-Befehl

Eine VM aus dem ProxDeploy-Template ausrollen, vollständig per API:
Klonen, Hardware, cloud-init, Start, Guest-Agent und Provisionierung der
Profile über den Agent – ohne SSH. Der Fortschritt kommt aus der
Logdatei im Gast, je Profil eine Marke.

Der Dry-Run läuft durch dieselben Phasen mit einem Trocken-Client und
zeigt jeden geplanten Proxmox-Aufruf und den passenden CLI-Befehl. Der
Assistent hat 9 Schritte, den Zustand in der URL und bietet an, ein
fehlendes Template im selben Auftrag zu bauen.

Auf pve-demo abgenommen: pd-test01 (base, docker) per API, pd-test02
(base, zabbix-agent) über den Assistenten, beide per SSH geprüft. Dabei
gefunden: file-write scheitert an Umlauten – Skripte gehen jetzt
base64-kodiert hinüber.

Closes #008

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 11:34:15 +02:00
co-authored by Claude Opus 5.5
parent 4285785a4c
commit 9310164bc2
24 changed files with 2465 additions and 27 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
/** Meldet alle Abläufe beim worker an. Ausrollen (#008) und Aufräumen folgen. */
/** Meldet alle Abläufe beim worker an. Aufräumen folgt. */
import "./ablaeufe/ausrollen";
import "./ablaeufe/template";
+83
View File
@@ -0,0 +1,83 @@
import { phasenAusfuehren } from "@/engine/ablauf/ablauf";
import { sshBefehl, vmAusrollSchritte, type VmAusrollStand } from "@/engine/ablaeufe/vm-ausrollen";
import { ProxmoxClient } from "@/engine/proxmox/client";
import { prisma } from "@/lib/db";
import { melden } from "@/server/ereignisse/senke";
import { ausrollenVorbereiten, kopfzeile } from "@/server/services/ausrollen";
import { verbindungAus } from "@/server/services/server";
import { nodeBefehlFuer } from "@/server/services/ssh-zugang";
import { vmidReservieren } from "@/server/services/vmid";
import { ablaufRegistrieren } from "../ausfuehren";
import { aufrufLogger } from "../aufruf-log";
ablaufRegistrieren("AUSROLLEN", async ({ auftrag, senke, abbruchGewuenscht, angelegt }) => {
// Dieselbe Vorbereitung wie beim Anlegen – der Katalog könnte sich seitdem geändert haben.
const v = await ausrollenVorbereiten(auftrag.anfrage);
const stand: VmAusrollStand = {};
const kopfSetzen = async () => {
await prisma.auftrag.update({
where: { id: auftrag.id },
data: { kopf: kopfzeile(v, stand), knoten: stand.knoten ?? null },
});
await melden(auftrag.id);
};
await kopfSetzen();
const client = new ProxmoxClient(verbindungAus(v.server), { beobachter: aufrufLogger(senke) });
try {
const ergebnis = await phasenAusfuehren(
vmAusrollSchritte(
{
client,
anfrage: v.anfrage,
distribution: v.distribution,
profile: v.profile,
vmidReservieren: async (belegt, wunsch) => {
const vmid = await vmidReservieren({
serverId: v.server.id,
auftragId: auftrag.id,
belegt,
von: v.server.gastVmidVon,
wunsch,
});
await prisma.auftrag.update({ where: { id: auftrag.id }, data: { vmid } });
await kopfSetzen();
return vmid;
},
templateBau: v.anfrage.templateBauen
? {
sshFuer: (knoten) => nodeBefehlFuer(v.server, knoten),
auftragKurzId: auftrag.kurzId,
vmidReservieren: (belegt) =>
vmidReservieren({
serverId: v.server.id,
auftragId: auftrag.id,
belegt,
von: v.server.templateVmidVon,
bis: v.server.templateVmidBis,
}),
}
: undefined,
angelegt,
auftragKurzId: auftrag.kurzId,
},
stand,
),
senke,
{ abbruchGewuenscht },
);
await kopfSetzen();
if (!ergebnis.ok) return ergebnis;
return {
ok: true,
ergebnis: {
vmid: stand.vmid,
knoten: stand.knoten,
ip: stand.ip,
ssh: sshBefehl(v.anfrage.zugang.benutzer, stand.ip),
},
};
} finally {
await client.schliessen();
}
});