Ubuntu 26.04 kommt mit, Templates booten UEFI, Template-VMIDs werden als nächste freie ID im Template-Bereich des Servers vergeben. Refs #001 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
33 KiB
ProxDeploy – Architektur- und Umsetzungsplan
Stand: 2026-10-12 · Version 0.1.0-dev · freigegeben am 2026-10-12
1. Überblick
Browser (Web-GUI) Terminal (proxdeploy, Go)
│ Cookie │ Bearer-Token
└──────────┬──────────────┘
▼ REST /api/v1 + SSE
┌────────────────────────────────────────────────────────────────────────┐
│ ProxDeploy (Docker Compose oder Debian-LXC), hinter Reverse-Proxy │
│ ┌──────────────────────┐ pg-boss ┌────────────────────────────┐ │
│ │ app (Next.js) │──Auftrag────▶│ worker (Node) │ │
│ │ UI · REST-API · SSE │◀─LISTEN/─────│ Engine: Planer, Phasen, │ │
│ │ Dry-Run (Planer) │ NOTIFY │ Proxmox-Client, Adapter │ │
│ └──────────┬───────────┘ └──────┬───────────┬─────────┘ │
│ └────────── PostgreSQL ───────────┘ │ │
└───────────────────────────────────────────────────────────┼─────────────┘
HTTPS :8006 (Token, Fingerprint-Pin) │ optional SSH
▼
Proxmox VE 8.x / 9.x (Einzelhost oder Cluster)
│ Guest-Agent / SSH in den Gast
▼
VM oder LXC (Debian, Ubuntu, …)
- Ein Backend, zwei Oberflächen. Die Engine (
src/engine/) ist frameworkfrei und enthält die gesamte Deploy-Logik. Die Web-GUI und die CLI sprechen ausschließlich die REST-API; die GUI hat keinen Sonderweg an der API vorbei (Server Actions rufen dieselben Services wie die Route Handler). - app liefert UI, REST-API, OpenAPI-Spezifikation und SSE-Ströme. Sie führt nie ein Deployment aus, nur den Planer für den Dry-Run (reine Funktion, keine schreibenden Proxmox-Aufrufe).
- worker ist derselbe Codestand als eigener Prozess und arbeitet Aufträge aus der pg-boss-Queue ab.
Jede Phasenänderung wird in die DB geschrieben und per
NOTIFY auftrag_<id>gemeldet; die app hängt sich mitLISTENdaran und streamt an alle offenen SSE-Verbindungen. Ein Neuladen der Seite oder ein Wiederverbinden der CLI liest zuerst den Stand aus der DB und setzt dann abLast-Event-IDfort. - PostgreSQL ist die einzige Datenhaltung (inkl. Queue). Keine Dateien außer dem Katalog im Image.
2. Tech-Stack
| Bereich | Wahl | Begründung |
|---|---|---|
| Web/API | Next.js 16 (App Router), React 19, TypeScript strict | Vorgabe; wie Laufzettel/ProxLedger |
| Styling | Tailwind CSS 4, Tokens als CSS-Variablen unter @theme, eigene Komponenten |
Vorgabe, kein Admin-Template |
| Schriften | Space Grotesk, JetBrains Mono über @fontsource-variable, selbst gehostet |
keine Google-Anfragen aus dem Intranet |
| Icons | Lucide (Stroke) + die Statussymbole ✓ ◐ ✗ ○ als Text | Status nie nur Farbe |
| DB | PostgreSQL 17, Prisma 7 mit @prisma/adapter-pg |
Vorgabe |
| Jobs | pg-boss auf derselben Datenbank | Vorgabe; kein Redis nötig |
| Validierung | zod (Request, Katalog-YAML, Profil-Editor) | eine Schemaquelle für API, OpenAPI und Katalog |
| OpenAPI | aus den zod-Schemas erzeugt (/api/v1/openapi.json) |
Go-Client wird daraus generiert |
| Auth | jose (Sitzungs-Cookie), openid-client (OIDC, PKCE), argon2id |
wie ProxLedger; kein Auth.js-Beta |
| SSH | ssh2 (Node) |
Snippets schreiben, pct exec, SSH in den Gast |
| YAML | yaml (eemeli) |
erhält Kommentare im Profil-Editor |
| Logs | pino (JSON auf stdout), zusätzlich pro Auftrag in der DB |
Betrieb + GUI |
| Tests | Vitest (Engine, API mit echter Test-DB), Playwright (Assistent, Live-Ansicht, 390 px) | |
| CLI | Go, statisches Binary | siehe Abschnitt 9 |
Exakte Versionen werden in #001 gegen npm/Go-Proxy geprüft und gepinnt (abgelagerte Fassungen,
keine RCs) und in CLAUDE.md festgehalten.
3. Was geht per Proxmox-API, was braucht SSH
Geprüft am 2026-10-12 gegen das API-Schema von PVE 9.2 (apidoc.js), die Quellen der Zweige
stable-8/stable-bookworm (pve-storage, qemu-server, pve-container) und die Release Notes
8.0–9.2. Belege stehen in den Issues #005 und #007.
| Fähigkeit | PVE 8.0–8.3 | PVE 8.4+ / 9.x | Weg |
|---|---|---|---|
Cloud-Image herunterladen + Checksumme prüfen (download-url, content=import, qcow2/raw/vmdk) |
nein (nur iso/vztmpl, OVA ab 8.2) | ja | API |
Disk aus heruntergeladenem Image anlegen (scsi0: <stor>:0,import-from=<stor>:import/<datei>) |
nur per absolutem Pfad → nur root@pam |
ja, per Volume-ID | API |
Klonen, Konfiguration, cloud-init (ciuser, sshkeys, ipconfigN, nameserver, searchdomain, ciupgrade), Start/Stopp, Template |
ja | ja | API |
Guest-Agent exec, exec-status, file-write (max. 60 KiB je Aufruf), network-get-interfaces |
ja (VM.Monitor) |
ja (VM.GuestAgent.* ab 9.0) |
API |
LXC anlegen (ssh-public-keys, net0 mit tag, unprivileged, features: nesting) |
ja | ja | API |
LXC-Template-Liste und -Download (aplinfo) |
ja | ja | API |
LXC-IP auslesen (lxc/{id}/interfaces) |
ja | ja | API |
LXC-Features außer nesting (keyctl, fuse, …) |
nur root@pam per Ticket, kein Token |
dito | SSH (pct set) |
| Befehle im LXC ausführen | keine API | keine API | SSH (pct exec auf dem Node oder SSH in den Container) |
cloud-init-Snippets schreiben (cicustom) |
keine API (Upload nur iso/vztmpl/import) | dito | SSH |
| Zertifikats-Fingerprint | nodes/{n}/certificates/info |
dito | API (nach erstem Pin) |
Folgerungen für die Engine:
- Fähigkeitserkennung pro Server (
src/engine/proxmox/faehigkeiten.ts), beim Verbindungstest und vor jedem Auftrag aufgefrischt: PVE-Version je Node, Storages mit Inhaltstypimport,snippets,vztmpl,images/rootdir, Rechte des Tokens (GET /access/permissions), SSH konfiguriert und erreichbar. Daraus ergibt sich pro Server und Vorgang die Strategie, die in der GUI sichtbar ist („Template-Bau: per API“ / „braucht SSH, weil PVE 8.3“). - VM-Provisionierung läuft immer über den Guest-Agent (Skript per
file-writein 60-KiB-Stücken, dannexec+exec-status-Polling). Damit funktioniert sie ohne SSH und liefert Live-Fortschritt. Standard-cloud-init von Proxmox liefert nur Benutzer, Keys und Netz;ciupgrade=0, Updates macht das Profilbase(sichtbar als eigene Phase statt stummem Boot-Upgrade). - Der Guest-Agent ist in keinem der vier Cloud-Images vorinstalliert (geprüft in den Paketlisten).
Deshalb wird er beim Template-Bau eingebaut, damit jeder Klon sofort einen Agent hat:
- mit SSH zum Node: einmaliger Vorbereitungsstart mit
cicustom: vendor=<snippet>(installiertqemu-guest-agent,cloud-init clean,poweroff) – funktioniert auch mit DHCP; - ohne SSH: Vorbereitungsstart mit einer Bau-Adresse (statische IP aus den Server-Einstellungen)
und dem ProxDeploy-Bereitstellungsschlüssel per
sshkeys, dann SSH in die VM, dieselben Schritte. - Danach
cloud-init clean, Maschinen-ID leeren, als Template markieren.
- mit SSH zum Node: einmaliger Vorbereitungsstart mit
- LXC-Provisionierung braucht immer SSH – bevorzugt
pct execüber SSH zum Node (kein Netzweg zum Container nötig), sonst SSH direkt in den Container (Bereitstellungsschlüssel perssh-public-keys, IP aus Konfiguration bzw.lxc/{id}/interfaces). Dasdebian-Standard-Template bringtopenssh-servermit; der Bereitstellungsschlüssel wird am Ende wieder entfernt. - Ohne jedes SSH sind möglich: VM aus vorhandenem oder per API gebautem Template (PVE ≥ 8.4) mit allen Profilen; LXC ohne Profile (nur Anlegen + Start). Die GUI graut nicht Mögliches mit Begründung aus.
SSH-Zugang pro Server: ProxDeploy erzeugt ein ed25519-Schlüsselpaar, zeigt den öffentlichen
Schlüssel samt Einzeiler zum Hinterlegen, pinnt den Host-Key beim ersten Test (Bestätigung durch den
Admin). Benutzer root auf dem Node (für pct und /var/lib/vz/snippets nötig).
4. Proxmox-Rechte (Rolle ProxDeploy)
Wird im README ausführlich dokumentiert, inkl. pveum-Befehlen.
- Gemeinsam:
VM.Allocate VM.Clone VM.Audit VM.Config.Disk VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Network VM.Config.HWType VM.Config.Options VM.Config.Cloudinit VM.PowerMgmt VM.Console Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit SDN.Use SDN.Audit Sys.Audit Pool.Audit - nur 8.x:
VM.Monitor(Guest-Agent); vor 8.2 stattSys.AccessNetwork→Sys.Modify - 8.2+ und 9.x:
Sys.AccessNetworkauf/nodes/<node>(fürdownload-url) - nur 9.x:
VM.GuestAgent.Audit VM.GuestAgent.Unrestricted(ersetztVM.Monitor) - optional:
Datastore.Allocate(heruntergeladene Images/Templates wieder löschen),Sys.Modify(privilegierte Container ab 9.0) - ACL:
/vmsoder ein Pool,/storage/<id>,/sdn/zones/localnetwork(bzw. konkrete Zonen),/nur fürSys.Audit. Der Verbindungstest gleicht die effektiven Rechte gegen diese Liste ab und nennt fehlende Rechte einzeln.
5. Distributionen, Adapter, Profile
Katalog als Daten
catalog/distros/*.yaml und catalog/profiles/*.yaml liegen im Repo und im Image. Der worker
synchronisiert sie beim Start in die DB (Upsert über id, Hash der Datei; Aktiv-Schalter und eigene
Profile bleiben unangetastet). Die YAML-Schlüssel bleiben englisch wie in der Vorgabe – sie sind
ein Austauschformat, kein Code-Bezeichner. Validiert mit zod; ein fehlerhafter Eintrag wird mit
Dateiname und Pfad gemeldet und übersprungen, nicht der Start abgebrochen.
Mitgeliefert (URLs am 2026-10-12 per Abruf geprüft): Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04.
Dazu Ubuntu 26.04 LTS (resolute, Cloud-Image und LXC-Template verfügbar).
- Debian:
cloud.debian.org/images/cloud/<codename>/latest/debian-<n>-genericcloud-amd64.qcow2,SHA512SUMS(Zeilenformat<hash>␣␣<datei>), Benutzerdebian - Ubuntu:
cloud-images.ubuntu.com/<codename>/current/<codename>-server-cloudimg-amd64.img(qcow2),SHA256SUMS(Zeilenformat<hash>␣*<datei>), Benutzerubuntu - LXC: alle
*-standard_*_amd64.tar.zst; bei mehreren Treffern (Debian 13:13.1-2und13.6-1) gewinnt die höchste Version (Versionssortierung).
Ergänzende Felder gegenüber der Vorgabe: codename, arch (vorerst amd64), vm.bios
(ovmf Standard, seabios möglich), vm.min_disk_gb.
Provisionierungs-Adapter
src/engine/adapter/schnittstelle.ts definiert FamilienAdapter – jede Methode liefert ein
idempotentes Shell-Fragment (kein Ausführen im Adapter, damit testbar und im Dry-Run anzeigbar):
interface FamilienAdapter {
familie: string; // "debian"
aktualisieren(): Skript;
paketeInstallieren(pakete: string[]): Skript;
dienstAktivieren(name: string, starten?: boolean): Skript;
repoHinzufuegen(repo: PaketQuelle): Skript; // Schlüssel-URL, URIs, Suites, Komponenten
zeitzoneSetzen(tz: string): Skript;
localeSetzen(locale: string): Skript;
tastaturSetzen(layout: string): Skript;
benutzerAnlegen(b: Benutzerangaben): Skript; // inkl. sudo, authorized_keys
sshHaerten(o: SshHaertung): Skript; // PasswordAuthentication no, PermitRootLogin no
gastAgentInstallieren(): Skript;
automatischeUpdates(an: boolean): Skript;
fakten(): Skript; // gibt OS-Fakten (codename, ID) als JSON aus
}
Registriert in src/engine/adapter/index.ts. Ein neuer Adapter (rhel, alpine, suse) ist eine
neue Datei + ein Eintrag in der Registry + Tests; Katalog und Profile verweisen per family darauf.
Profile
Ein Profil ist eine Liste deklarativer Schritte je Familie, die der Adapter in Shell übersetzt.
Für Sonderfälle gibt es run (rohes Shell, ausdrücklich nicht portabel). Variablen werden als
Formularfelder angezeigt und mit {{ var.name }} eingesetzt (eigene kleine Template-Funktion,
nur Ersetzung, keine Logik; Werte werden für Shell gequotet).
id: zabbix-agent
name: Zabbix Agent 2
description: Zabbix Agent 2 (7.0 LTS) aus dem offiziellen Repo
requires: [base]
variables:
- { name: server, label: Zabbix-Server, type: string, required: true }
- { name: server_active, label: ServerActive, type: string, default: "{{ var.server }}" }
lxc: {} # z. B. docker: { features: { nesting: true } }
families:
debian:
- repo_deb: "https://repo.zabbix.com/zabbix/7.0/{{ os.id }}/pool/main/z/zabbix-release/zabbix-release_latest_7.0+{{ os.id }}{{ os.version_id }}_all.deb"
- packages: [zabbix-agent2]
- file: { path: /etc/zabbix/zabbix_agent2.d/proxdeploy.conf, content: "Server={{ var.server }}\nServerActive={{ var.server_active }}\nHostname={{ guest.name }}\n" }
- service: { name: zabbix-agent2, enable: true, restart: true }
Fehlt families.<familie> für die gewählte Distribution, ist das Profil im Assistenten ausgegraut
(„keine Umsetzung für Familie rhel“). Eigene Profile werden im YAML-Editor angelegt, gegen dasselbe
zod-Schema geprüft und mit origin: custom gespeichert; Katalog-Profile sind schreibgeschützt,
lassen sich aber als Kopie bearbeiten.
Mitgeliefert: base, docker (Docker CE, deb822 docker.sources, Key /etc/apt/keyrings/docker.asc,
Pakete docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; LXC:
nesting=1), zabbix-agent (7.0 LTS; 8.0 hat noch keine Agent-Pakete).
6. Engine: Aufträge und Phasen
src/engine/
proxmox/ client.ts (fetch + undici, Fingerprint-Pinning), endpunkte/*.ts, faehigkeiten.ts, aufgaben.ts (UPID warten)
ssh/ verbindung.ts, node.ts (pct exec, Snippet schreiben), gast.ts
katalog/ schema.ts (zod), laden.ts, pruefsumme.ts (beide Zeilenformate)
adapter/ schnittstelle.ts, debian.ts, index.ts
profile/ aufloesen.ts (Reihenfolge, requires, Variablen), rendern.ts (→ Skript)
planer/ vm.ts, lxc.ts, template.ts → Plan { phasen[], aufrufe[] } (rein, keine I/O außer Lesen)
ablauf/ ausfuehren.ts (Phasen-Runner), ereignisse.ts (Senke-Interface), fehler.ts (Ursache + Hinweis)
transport/ gastagent.ts, ssh-node.ts, ssh-gast.ts → gemeinsames Interface `Ausfuehrer`
sperren.ts VMID-Reservierung
Planer und Ausführung trennen. Der Planer bekommt eine validierte Anfrage + einen lesenden
Proxmox-Zugriff + Fähigkeiten und liefert den Plan: die Phasen und je Phase die geplanten
API-Aufrufe (POST /nodes/pve01/qemu/9013/clone newid=120 full=1 …). Der Dry-Run zeigt genau diesen
Plan; die Ausführung arbeitet ihn ab. So können Dry-Run und Wirklichkeit nicht auseinanderlaufen.
Phasen (VM): Prüfungen → Template (übersprungen, wenn vorhanden) → Klonen → Hardware & Disk →
cloud-init → Start → Guest-Agent → Provisionierung (je Profil eine Unterzeile im Log) → Abschluss.
LXC: Prüfungen → LXC-Template → Anlegen → Start → Netzwerk → Provisionierung → Abschluss.
Jede Phase schreibt status, detail (der Muted-Text der CLI), dauer, bei Fehler ursache und
hinweis. Web und CLI rendern nur diese Felder – kein Text wird in einem Client erfunden.
VMID-Vergabe ohne Doppelungen: pg_advisory_xact_lock(hash(serverId)), dann nächste freie ID
aus /cluster/resources ∪ Tabelle VmidReservierung, Reservierung schreiben, Sperre fällt mit der
Transaktion. Die Reservierung gilt bis Auftragsende (bzw. Ablauf nach 2 h als Sicherung). Beim
create/clone wird ein Konflikt (already exists) einmal mit neuer ID wiederholt.
Fehler und Aufräumen: Ein Fehler beendet den Auftrag mit Status FEHLGESCHLAGEN, die Phase trägt
Ursache und Lösungshinweis (z. B. „Token fehlt VM.GuestAgent.Unrestricted auf /vms/120 → Rolle
ergänzen“). Was angelegt wurde, steht in Auftrag.angelegt (VMID, heruntergeladene Volumes).
„Aufräumen“ ist ein eigener Auftrag (AUFRAEUMEN), nur nach Bestätigung, und löscht nur, was
ProxDeploy selbst angelegt hat (geprüft über das Tag proxdeploy + Auftrags-ID in der Beschreibung).
Parallelität: pg-boss mit konfigurierbarer Parallelität im worker (Standard 4); Template-Bau pro Server+Distribution singleton (zweiter Deploy wartet auf denselben Bau statt doppelt zu bauen).
7. Datenmodell (Prisma, Entwurf)
Namensabbildung (verbindlich, kommt nach CLAUDE.md):
| Vorgabe | Modell / Begriff | Vorgabe | Modell / Begriff |
|---|---|---|---|
| User | Benutzer |
Deployment / Job | Auftrag (Art AUSROLLEN, TEMPLATE_BAUEN, AUFRAEUMEN) |
| API token (CLI) | ApiToken |
Phase | Phase |
| SSH key | SshSchluessel |
Job log line | Logeintrag |
| Server | Server |
VMID lock | VmidReservierung |
| Node | Knoten (nur Feldname, kein Modell) |
Template (Proxmox) | Template |
| Distribution | Distribution |
Preset | Vorlage |
| Profile | Profil |
Audit log | Protokoll |
| Setting | Einstellung |
Key provider | SchluesselQuelle (Interface) |
enum Rolle { ADMIN OPERATOR }
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 }
enum Herkunft { KATALOG EIGEN }
enum Quelle { WEB CLI API }
model Benutzer {
id String @id @default(cuid())
benutzername String @unique
anzeigename String
email String?
passwortHash String? // null bei reinen OIDC-Benutzern
oidcSubjekt String? @unique
rolle Rolle @default(OPERATOR)
aktiv Boolean @default(true)
standards Json? // persönliche Standardwerte im Assistenten
erstelltAm DateTime @default(now())
apiTokens ApiToken[] sshSchluessel SshSchluessel[] auftraege Auftrag[] protokoll Protokoll[]
}
model ApiToken {
id String @id @default(cuid())
benutzerId String
name String
praefix String @unique // "pd_ab12cd34" – erkennbar, Rest nur gehasht
hash String // SHA-256 des Geheimnisses
zuletztGenutztAm DateTime?
ablaufAm DateTime?
widerrufenAm DateTime?
erstelltAm DateTime @default(now())
}
model SshSchluessel {
id String @id @default(cuid())
benutzerId String? // null = für alle sichtbar (Team-Schlüssel)
name String
oeffentlich String
fingerabdruck String // SHA256:…
quelle String @default("lokal") // Kennung der SchluesselQuelle (später z. B. sshkeyvault)
@@unique([benutzerId, fingerabdruck])
}
model Server {
id String @id @default(cuid())
name String @unique // "rz1" – auch CLI-Name
url String // https://host:8006
tokenId String // user@realm!tokenid
tokenGeheimnis String // AES-256-GCM, v1:<iv>:<tag>:<daten>
tlsPruefen Boolean @default(true)
fingerabdruck String? // SHA-256, gepinnt bei selbst signiertem Zertifikat
sshAktiv Boolean @default(false)
sshBenutzer String @default("root")
sshPort Int @default(22)
sshPrivat String? // verschlüsselt
sshOeffentlich String?
sshHostSchluessel Json? // je Node gepinnt
templateVmidVon Int @default(9000)
templateVmidBis Int @default(9999)
gastVmidVon Int @default(100)
bauNetz Json? // Bridge, VLAN, IP/CIDR, Gateway für Template-Vorbereitung ohne SSH
standards Json? // Standard-Node, -Storage, -Bridge
faehigkeiten Json? // letzte Erkennung (Version, Storages, Rechte, Strategien)
erreichbar Boolean?
geprueftAm DateTime?
erstelltAm DateTime @default(now())
}
model Distribution {
id String @id // "debian-13"
name String
familie String
definition Json // validiertes YAML
dateiHash String
aktiv Boolean @default(true)
synchronisiertAm DateTime
}
model Profil {
id String @id // "docker"
name String
beschreibung String?
herkunft Herkunft
quelltext String // YAML wie eingegeben (Kommentare bleiben)
definition Json
aktiv Boolean @default(true)
geaendertVonId String?
geaendertAm DateTime @updatedAt
}
model Template {
id String @id @default(cuid())
serverId String
knoten String
vmid Int
distributionId String
storage String
imageQuelle String // URL + Prüfsumme des Images
gebautVonAuftragId String?
gebautAm DateTime
@@unique([serverId, vmid])
}
model Vorlage { // Preset
id String @id @default(cuid())
name String @unique // "docker-host-klein"
titel String
beschreibung String?
gastTyp GastTyp
parameter Json // Teilmenge der Auftrags-Anfrage
erstelltVonId String?
geaendertAm DateTime @updatedAt
}
model Auftrag {
id String @id @default(cuid())
kurzId String @unique // "d-7f3k2" für CLI und URLs
art AuftragsArt
status AuftragsStatus @default(WARTEND)
serverId String
knoten String?
gastTyp GastTyp?
vmid Int?
name String?
distributionId String?
profile String[]
anfrage Json // validierte Anfrage (Grundlage für „Wiederholen“)
plan Json // Plan zum Zeitpunkt des Starts
ergebnis Json? // IP, SSH-Befehl, Dauer
angelegt Json? // was zum Aufräumen gehört
fehler Json? // { phase, ursache, hinweis }
quelle Quelle
erstelltVonId String
wiederholtVonId String?
vorlageId String?
erstelltAm DateTime @default(now())
gestartetAm DateTime?
beendetAm DateTime?
phasen Phase[] log Logeintrag[]
@@index([serverId, erstelltAm]) @@index([status])
}
model Phase {
id String @id @default(cuid())
auftragId String
reihenfolge Int
schluessel String // "klonen"
titel String // "Klonen"
status PhasenStatus @default(OFFEN)
detail String?
ursache String?
hinweis String?
gestartetAm DateTime?
beendetAm DateTime?
@@unique([auftragId, schluessel])
}
model Logeintrag {
id BigInt @id @default(autoincrement()) // auch SSE-Event-ID
auftragId String
zeit DateTime @default(now())
stufe String // debug|info|warn|error
phase String?
nachricht String
daten Json?
@@index([auftragId, id])
}
model VmidReservierung {
serverId String
vmid Int
auftragId String
ablaufAm DateTime
@@id([serverId, vmid])
}
model Protokoll { // Audit-Log, nur anhängen
id BigInt @id @default(autoincrement())
zeit DateTime @default(now())
benutzerId String?
quelle Quelle
aktion String // "Server angelegt", "Ausrollung gestartet"
ziel String? // "server:rz1", "auftrag:d-7f3k2"
details Json?
ip String?
}
model Einstellung { schluessel String @id wert Json }
Datenbankregeln (CHECK/Trigger, wie in Laufzettel): Protokoll nur INSERT, VMID-Bereiche
von < bis, Auftrag.vmid innerhalb 100–999999999, ein Template je Server/VMID.
8. REST-API (/api/v1)
Authentifizierung: Sitzungs-Cookie (Web) oder Authorization: Bearer pd_… (CLI). Rolle und
Aktiv-Status werden pro Anfrage aus der DB gelesen. Jede schreibende Anfrage → Protokoll.
Fehler einheitlich { fehler: { code, nachricht, hinweis?, felder? } }.
| Methode + Pfad | Rolle | Zweck |
|---|---|---|
POST /auth/anmelden, POST /auth/abmelden, GET /auth/oidc/start, GET /auth/oidc/rueckruf |
– | Anmeldung |
GET /ich |
alle | aktueller Benutzer, Rolle, Version |
GET /server · POST /server · GET/PATCH/DELETE /server/:name |
Admin schreibt | Server |
POST /server/pruefen |
Admin | Verbindungstest ohne Speichern (liefert Fingerprint zum Bestätigen) |
POST /server/:name/test |
alle | Version, Cluster/Nodes, Storages, Bridges, Rechte, Fähigkeiten |
GET /server/:name/ressourcen?knoten= |
alle | Nodes, freie Ressourcen, Storages, Bridges/VLANs/VNets, nächste freie ID |
POST /server/:name/ssh |
Admin | Schlüsselpaar erzeugen/erneuern, Host-Key pinnen |
GET /distributionen?typ=vm|lxc · PATCH /distributionen/:id |
Admin schreibt | Katalog |
GET /profile · POST /profile · PUT/DELETE /profile/:id · POST /profile/pruefen |
Admin schreibt | Profile + Validierung |
GET /server/:name/templates |
alle | Templates je Node |
POST /server/:name/templates |
Admin, Operator | Template bauen → Auftrag |
POST /auftraege/plan |
alle | Dry-Run: Plan + CLI-Befehl, nichts wird angelegt |
POST /auftraege |
Operator+ | Ausrollen (VM/LXC) → 202 { id, kurzId } |
POST /auftraege/stapel |
Operator+ | Batch (YAML/JSON-Liste) |
GET /auftraege?server=&typ=&distribution=&status=&seite= |
alle | Verlauf |
GET /auftraege/:id |
alle | Stand inkl. Phasen + Ergebnis |
GET /auftraege/:id/ereignisse |
alle | SSE: phase, log, ende; Last-Event-ID |
GET /auftraege/:id/log?ab= |
alle | Detail-Log |
POST /auftraege/:id/wiederholen · /abbrechen · /aufraeumen |
Operator+ | |
GET/POST/PATCH/DELETE /vorlagen… |
Operator+ | Presets |
GET/POST/DELETE /schluessel… |
eigene | SSH-Public-Keys (über SchluesselQuelle) |
GET/POST/DELETE /tokens… |
eigene | CLI-Tokens (Geheimnis nur einmal angezeigt) |
GET/POST/PATCH /benutzer… |
Admin | Benutzer, Rollen |
GET /protokoll?… |
Admin | Audit-Log |
GET/PUT /einstellungen |
Admin | Standardwerte, OIDC, Bereitstellungsschlüssel |
GET /openapi.json, GET /gesundheit |
– | Spezifikation, Healthcheck |
9. CLI (cli/, Go)
Begründung Go: ein statisches Binary pro Plattform (linux/darwin/windows × amd64/arm64, ~8 MB,
keine Laufzeit nötig – passt auf jeden Admin-Rechner und Jump-Host), sehr gute Unterstützung für
TTY-Erkennung, 24-bit-Farben und SSE. Der Nachteil (keine geteilten TS-Typen) wird durch den aus
openapi.json generierten Client (oapi-codegen) aufgefangen; ein CI-Schritt prüft, dass der
generierte Code aktuell ist. Bibliotheken: cobra (Befehle), BurntSushi/toml, golang.org/x/term.
- Konfig
~/.config/proxdeploy/config.toml(url,token, optional[profiles.<name>]für mehrere Instanzen), Rechte 0600;PROXDEPLOY_URL/PROXDEPLOY_TOKENüberschreiben. login <url>fragt Benutzername/Passwort ab, legt ein Token an und speichert es (oder--token).- Ausgabe exakt nach Vorgabe; Renderer in
cli/internal/ausgabe/mit Golden-File-Tests für Farbe,--no-color,--ascii,--quiet,--json,--dry-run. Breite der Phasenspalte aus dem längsten Phasennamen, Box-Breite aus dem Inhalt (Unicode-Breite beachtet). - Exit-Codes: 0 Erfolg, 1 Auftrag fehlgeschlagen, 2 Bedienfehler, 3 Verbindung/Anmeldung.
- Die Web-GUI zeigt im letzten Assistentenschritt den passenden CLI-Befehl; erzeugt wird er vom
Server (
POST /auftraege/planliefertcliBefehl), damit beide Seiten dieselben Flags kennen.
10. Web-GUI
Seitenleiste mit Wortmarke, Server-Umschalter, Navigation (Übersicht, Ausrollen, Verlauf, Templates,
Vorlagen, Katalog, Server*, Einstellungen; * nur Admin schreibend). Panel-Karten mit 1-px-Linie,
Radius 14 px, Tokens aus der Farbtabelle der Vorgabe, dunkel zuerst, helles Theme umschaltbar
(data-theme), Kontrast ≥ 4,5:1 in beiden (wird in Tests geprüft). Statusanzeige als Komponente
<Status> mit Symbol + Farbe + Text.
- Assistent
/ausrollen: Schritte als Route-Segmente mit dem Zustand in der URL (?s=…), damit Zurückspringen, Neuladen und Teilen funktionieren; Vorbelegen per?vorlage=oder?wiederholen=. Am Handy eine Schrittleiste oben, Formular einspaltig. - Live-Ansicht
/auftraege/[kurzId]: Terminal-Karte (JetBrains Mono) mit genau dem Aufbau der CLI, darunter ausklappbares Detail-Log, am Ende die Ergebnis-Karte mit Kopieren-Knopf. Fallback auf Polling, falls SSE durch den Proxy geblockt wird.
11. Sicherheit
- Secrets (Proxmox-Token, SSH-Private-Keys, OIDC-Client-Secret) mit AES-256-GCM, Schlüssel aus
PROXDEPLOY_SCHLUESSEL(32 Byte, Base64); Versionspräfix im Chiffrat für spätere Rotation. Geheimnisse verlassen die API nie (nur „gesetzt“/Fingerprint). - CLI-Tokens nur gehasht gespeichert; Passwörter argon2id; Anmeldung mit Rate-Limit.
- TLS zu Proxmox: Standard Systemvertrauen; bei selbst signiertem Zertifikat Pinning des
SHA-256-Fingerprints (TOFU mit Bestätigung im Formular). Kein globales
NODE_TLS_REJECT_UNAUTHORIZED. - Hinter Reverse-Proxy:
TRUST_PROXY=truewertetX-Forwarded-For/Protoaus; CookiesSecureabhängig vom tatsächlichen Schema (Lehre aus ProxLedger 0.11.1); SSE mitX-Accel-Buffering: no. - Profil-Variablen werden für Shell gequotet;
run-Schritte nur in Profilen, die ein Admin anlegt. - Bereitstellungsschlüssel (für SSH in Gäste) wird nach der Provisionierung aus dem Gast entfernt.
12. Betrieb
docker-compose.yml:app,worker,postgres,migrate(einmaligprisma migrate deploy). Ein gemeinsames Image, Nextoutput: "standalone", Non-root.deploy/install-lxc.sh: auf dem Proxmox-Host ausführen → legt Debian-13-LXC an (perpct), installiert Node, PostgreSQL, systemd-Units für app und worker, erzeugt.envmit Zufallsschlüssel..env.examplemit allen Variablen (DATABASE_URL,PROXDEPLOY_SCHLUESSEL,SITZUNG_GEHEIMNIS,APP_URL,TRUST_PROXY,OIDC_*,WORKER_PARALLEL,LOG_LEVEL,ADMIN_BENUTZER/PASSWORTfür den ersten Start).
13. Dateistruktur
proxdeploy/
├─ catalog/{distros,profiles}/*.yaml
├─ cli/ Go-Modul: cmd/proxdeploy, internal/{api,ausgabe,konfig,befehle}
├─ src/
│ ├─ app/(app)/… Seiten (Übersicht, ausrollen, auftraege, server, templates, vorlagen, katalog, einstellungen)
│ ├─ app/(auth)/anmelden
│ ├─ app/api/v1/… Route Handler (dünn: Recht prüfen, zod, Service)
│ ├─ components/ UI-Bausteine (Karte, Status, Terminal, Assistent, Formulare)
│ ├─ engine/ siehe Abschnitt 6 – frameworkfrei, ohne Prisma-Import
│ ├─ server/services/ Regeln + Schreiboperationen, rufen Engine und Prisma
│ ├─ server/queries/ Lesemodelle
│ ├─ server/auth/ Sitzung, Token, OIDC, Rechte
│ ├─ worker/ main.ts, Senke (Ereignisse → DB + NOTIFY), Katalog-Sync
│ └─ lib/ kryptografie.ts, formate.ts, logger.ts
├─ prisma/schema.prisma, migrations/
├─ public/brand/ Logo-Assets, Favicons, Manifest-Icons
├─ assets/ Banner, Avatar (Repo)
├─ deploy/install-lxc.sh
├─ tests/ engine/*, api/*, e2e/*
├─ docs/architektur.md, docs/issues/
├─ CHANGELOG.md, DEVLOG.md, CLAUDE.md, README.md, .env.example
└─ docker-compose.yml, Dockerfile
14. Etappen und Meilensteine
| Version | Etappe | Issues | Test gegen echten Proxmox |
|---|---|---|---|
| v0.1.0 | 1 – Server-Verwaltung + Verbindungstest | #001–#003 | Verbindungstest, Fähigkeiten, Rechte |
| v0.2.0 | 2 – Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt | #004–#008 | Template bauen, VM ausrollen, Fehler + Aufräumen |
| v0.3.0 | 3 – LXC, Ubuntu, Profile, Presets | #009–#012 | LXC mit/ohne SSH, docker/zabbix |
| v0.4.0 | 4 – CLI | #013 | alle Befehle gegen die laufende Instanz |
| v0.5.0 | 5 – Verlauf, Audit, Rollen, OIDC | #014–#016 | – |
| v0.6.0 | 6 – Branding, README, Installer | #017–#019 | Installer in echtem LXC |
| v1.0.0 | Feldtest 8.x + 9.x, Feinschliff | #020 | vollständig |
Bis die Testumgebung steht, entwickle ich gegen einen Proxmox-Mock (tests/proxmox-mock/, ein
kleiner HTTP-Server mit Zustand, der die benutzten Endpunkte inkl. UPID-Tasks nachbildet). Die
Abnahme jeder Etappe gegen echten Proxmox wird nachgeholt und im DEVLOG vermerkt; ohne sie wird die
Version nicht als abgeschlossen markiert.
15. Entscheidungen zur Freigabe (2026-10-12)
- Ubuntu 26.04 LTS wird mitgeliefert (Debian 12/13, Ubuntu 22.04/24.04/26.04).
- Templates booten UEFI (OVMF, q35, EFI-Disk mit
pre-enrolled-keys=1);vm.biospro Distribution überschreibbar. - Template-VMID: keine feste Zuordnung. ProxDeploy nimmt die nächste freie ID im
Template-Bereich des Servers (Standard 9000–9999), ermittelt aus
/cluster/resourcesplus eigenen Reservierungen – dieselbe Sperre wie bei Gast-IDs. Welche VMID welches Template ist, steht in der TabelleTemplateund als Tag (proxdeploy-template,distro-debian-13) in Proxmox. - Namensabbildung aus Abschnitt 7 und deutsche API-Pfade bestätigt; Katalog-YAML bleibt englisch.
- Domain für
APP_URLund OIDC-Redirect folgt später über.env.