feat(auftraege): worker, Phasen und Live-Fortschritt per SSE
Aufträge laufen als pg-boss-Jobs im worker. Der Phasen-Runner der Engine kennt weder Proxmox noch Datenbank; eine Senke schreibt Phasen und Log in die DB und meldet per NOTIFY, die app streamt per SSE an Web und CLI (Last-Event-ID, Herzschlag, Polling-Fallback im Browser). VMIDs vergibt eine Advisory-Sperre je Server zusammen mit einer Reservierungstabelle; zehn parallele Reservierungen im Test bekommen zehn verschiedene IDs. Der Proxmox-Client hat einen Trockenmodus für den Dry-Run: Lesendes echt, Schreibendes nur aufgezeichnet. Live-Ansicht im Aufbau der CLI-Ausgabe. Im echten Betrieb aufgefallen: ssh2 und pg sind CommonJS und brauchen im worker Default-Importe. Closes #006 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "GastTyp" AS ENUM ('VM', 'LXC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AuftragsArt" AS ENUM ('AUSROLLEN', 'TEMPLATE_BAUEN', 'AUFRAEUMEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AuftragsStatus" AS ENUM ('WARTEND', 'LAEUFT', 'ERFOLGREICH', 'FEHLGESCHLAGEN', 'ABGEBROCHEN', 'AUFGERAEUMT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PhasenStatus" AS ENUM ('OFFEN', 'LAEUFT', 'ERLEDIGT', 'FEHLER', 'UEBERSPRUNGEN');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Auftrag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kurzId" TEXT NOT NULL,
|
||||
"art" "AuftragsArt" NOT NULL,
|
||||
"status" "AuftragsStatus" NOT NULL DEFAULT 'WARTEND',
|
||||
"serverId" TEXT NOT NULL,
|
||||
"knoten" TEXT,
|
||||
"gastTyp" "GastTyp",
|
||||
"vmid" INTEGER,
|
||||
"name" TEXT,
|
||||
"distributionId" TEXT,
|
||||
"profile" TEXT[],
|
||||
"anfrage" JSONB NOT NULL,
|
||||
"plan" JSONB,
|
||||
"kopf" TEXT,
|
||||
"ergebnis" JSONB,
|
||||
"angelegt" JSONB,
|
||||
"fehler" JSONB,
|
||||
"quelle" "Quelle" NOT NULL,
|
||||
"erstelltVonId" TEXT NOT NULL,
|
||||
"wiederholtVonId" TEXT,
|
||||
"bezugId" TEXT,
|
||||
"abbrechen" BOOLEAN NOT NULL DEFAULT false,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"gestartetAm" TIMESTAMP(3),
|
||||
"beendetAm" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Auftrag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Phase" (
|
||||
"id" TEXT NOT NULL,
|
||||
"auftragId" TEXT NOT NULL,
|
||||
"reihenfolge" INTEGER NOT NULL,
|
||||
"schluessel" TEXT NOT NULL,
|
||||
"titel" TEXT NOT NULL,
|
||||
"status" "PhasenStatus" NOT NULL DEFAULT 'OFFEN',
|
||||
"detail" TEXT,
|
||||
"ursache" TEXT,
|
||||
"hinweis" TEXT,
|
||||
"gestartetAm" TIMESTAMP(3),
|
||||
"beendetAm" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Phase_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Logeintrag" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"auftragId" TEXT NOT NULL,
|
||||
"zeit" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"stufe" TEXT NOT NULL,
|
||||
"phase" TEXT,
|
||||
"nachricht" TEXT NOT NULL,
|
||||
"daten" JSONB,
|
||||
|
||||
CONSTRAINT "Logeintrag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VmidReservierung" (
|
||||
"serverId" TEXT NOT NULL,
|
||||
"vmid" INTEGER NOT NULL,
|
||||
"auftragId" TEXT NOT NULL,
|
||||
"ablaufAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "VmidReservierung_pkey" PRIMARY KEY ("serverId","vmid")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Auftrag_kurzId_key" ON "Auftrag"("kurzId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Auftrag_serverId_erstelltAm_idx" ON "Auftrag"("serverId", "erstelltAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Auftrag_status_idx" ON "Auftrag"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Auftrag_erstelltAm_idx" ON "Auftrag"("erstelltAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Phase_auftragId_reihenfolge_idx" ON "Phase"("auftragId", "reihenfolge");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Phase_auftragId_schluessel_key" ON "Phase"("auftragId", "schluessel");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Logeintrag_auftragId_id_idx" ON "Logeintrag"("auftragId", "id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "VmidReservierung_auftragId_idx" ON "VmidReservierung"("auftragId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Auftrag" ADD CONSTRAINT "Auftrag_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Auftrag" ADD CONSTRAINT "Auftrag_erstelltVonId_fkey" FOREIGN KEY ("erstelltVonId") REFERENCES "Benutzer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Phase" ADD CONSTRAINT "Phase_auftragId_fkey" FOREIGN KEY ("auftragId") REFERENCES "Auftrag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Logeintrag" ADD CONSTRAINT "Logeintrag_auftragId_fkey" FOREIGN KEY ("auftragId") REFERENCES "Auftrag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Template" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"knoten" TEXT NOT NULL,
|
||||
"vmid" INTEGER NOT NULL,
|
||||
"distributionId" TEXT NOT NULL,
|
||||
"storage" TEXT NOT NULL,
|
||||
"imageDatei" TEXT NOT NULL,
|
||||
"pruefsumme" TEXT NOT NULL,
|
||||
"gebautVonAuftragId" TEXT,
|
||||
"gebautAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Template_serverId_distributionId_idx" ON "Template"("serverId", "distributionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Template_serverId_vmid_key" ON "Template"("serverId", "vmid");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Template" ADD CONSTRAINT "Template_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -38,6 +38,7 @@ model Benutzer {
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
protokoll Protokoll[]
|
||||
auftraege Auftrag[]
|
||||
}
|
||||
|
||||
/// Audit-Log. Nur anhängen – ein Trigger verweigert UPDATE und DELETE.
|
||||
@@ -99,6 +100,8 @@ model Server {
|
||||
geprueftAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
auftraege Auftrag[]
|
||||
templates Template[]
|
||||
}
|
||||
|
||||
enum Herkunft {
|
||||
@@ -139,3 +142,138 @@ model Profil {
|
||||
geaendertVonId String?
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum GastTyp {
|
||||
VM
|
||||
LXC
|
||||
}
|
||||
|
||||
enum AuftragsArt {
|
||||
AUSROLLEN
|
||||
TEMPLATE_BAUEN
|
||||
AUFRAEUMEN
|
||||
}
|
||||
|
||||
enum AuftragsStatus {
|
||||
WARTEND
|
||||
LAEUFT
|
||||
ERFOLGREICH
|
||||
FEHLGESCHLAGEN
|
||||
ABGEBROCHEN
|
||||
AUFGERAEUMT
|
||||
}
|
||||
|
||||
enum PhasenStatus {
|
||||
OFFEN
|
||||
LAEUFT
|
||||
ERLEDIGT
|
||||
FEHLER
|
||||
UEBERSPRUNGEN
|
||||
}
|
||||
|
||||
/// Ein Hintergrundjob: Ausrollen, Template bauen oder Aufräumen. Läuft im worker.
|
||||
model Auftrag {
|
||||
id String @id @default(cuid())
|
||||
/// Kurz und tippbar für CLI und URLs: „d-7f3k2“.
|
||||
kurzId String @unique
|
||||
art AuftragsArt
|
||||
status AuftragsStatus @default(WARTEND)
|
||||
serverId String
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Restrict)
|
||||
knoten String?
|
||||
gastTyp GastTyp?
|
||||
vmid Int?
|
||||
name String?
|
||||
distributionId String?
|
||||
profile String[]
|
||||
/// Validierte Anfrage – Grundlage für „Wiederholen“.
|
||||
anfrage Json
|
||||
/// Plan zum Zeitpunkt des Starts (Phasen und geplante API-Aufrufe).
|
||||
plan Json?
|
||||
/// Kopfzeile wie in der CLI: „Server rz1 · Node pve01 · Debian 13 (Template 9013)“ – von der Engine gesetzt.
|
||||
kopf String?
|
||||
/// IP, SSH-Befehl, Dauer …
|
||||
ergebnis Json?
|
||||
/// Was ProxDeploy angelegt hat – Grundlage fürs Aufräumen.
|
||||
angelegt Json?
|
||||
/// { phase, ursache, hinweis }
|
||||
fehler Json?
|
||||
quelle Quelle
|
||||
erstelltVonId String
|
||||
erstelltVon Benutzer @relation(fields: [erstelltVonId], references: [id], onDelete: Restrict)
|
||||
wiederholtVonId String?
|
||||
/// Bei AUFRAEUMEN: der Auftrag, dessen Reste entfernt werden.
|
||||
bezugId String?
|
||||
abbrechen Boolean @default(false)
|
||||
erstelltAm DateTime @default(now())
|
||||
gestartetAm DateTime?
|
||||
beendetAm DateTime?
|
||||
phasen Phase[]
|
||||
log Logeintrag[]
|
||||
|
||||
@@index([serverId, erstelltAm])
|
||||
@@index([status])
|
||||
@@index([erstelltAm])
|
||||
}
|
||||
|
||||
model Phase {
|
||||
id String @id @default(cuid())
|
||||
auftragId String
|
||||
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
|
||||
reihenfolge Int
|
||||
schluessel String
|
||||
titel String
|
||||
status PhasenStatus @default(OFFEN)
|
||||
/// Der graue Detailtext hinter dem Phasennamen – in Web und CLI derselbe.
|
||||
detail String?
|
||||
ursache String?
|
||||
hinweis String?
|
||||
gestartetAm DateTime?
|
||||
beendetAm DateTime?
|
||||
|
||||
@@unique([auftragId, schluessel])
|
||||
@@index([auftragId, reihenfolge])
|
||||
}
|
||||
|
||||
model Logeintrag {
|
||||
/// Zugleich die Event-ID im SSE-Strom (Last-Event-ID).
|
||||
id BigInt @id @default(autoincrement())
|
||||
auftragId String
|
||||
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
|
||||
zeit DateTime @default(now())
|
||||
stufe String
|
||||
phase String?
|
||||
nachricht String
|
||||
daten Json?
|
||||
|
||||
@@index([auftragId, id])
|
||||
}
|
||||
|
||||
/// Vergebene VMIDs, solange ein Auftrag läuft – verhindert Doppelvergabe zwischen parallelen Aufträgen.
|
||||
model VmidReservierung {
|
||||
serverId String
|
||||
vmid Int
|
||||
auftragId String
|
||||
ablaufAm DateTime
|
||||
|
||||
@@id([serverId, vmid])
|
||||
@@index([auftragId])
|
||||
}
|
||||
|
||||
/// Von ProxDeploy gebaute Templates. Maßgeblich ist Proxmox (Tag proxdeploy-template); hier steht, woraus gebaut wurde.
|
||||
model Template {
|
||||
id String @id @default(cuid())
|
||||
serverId String
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
knoten String
|
||||
vmid Int
|
||||
distributionId String
|
||||
storage String
|
||||
imageDatei String
|
||||
pruefsumme String
|
||||
gebautVonAuftragId String?
|
||||
gebautAm DateTime @default(now())
|
||||
|
||||
@@unique([serverId, vmid])
|
||||
@@index([serverId, distributionId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user