Files
proxdeploy/docs/architektur.md
T
2026-10-12 15:08:42 +02:00

616 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 mit `LISTEN` daran 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 ab `Last-Event-ID` fort.
- **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:**
1. **Fähigkeitserkennung pro Server** (`src/engine/proxmox/faehigkeiten.ts`), beim Verbindungstest und
vor jedem Auftrag aufgefrischt: PVE-Version je Node, Storages mit Inhaltstyp `import`, `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“).
2. **VM-Provisionierung läuft immer über den Guest-Agent** (Skript per `file-write` in 60-KiB-Stücken,
dann `exec` + `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 Profil `base` (sichtbar als eigene Phase statt stummem Boot-Upgrade).
3. **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>` (installiert
`qemu-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.
4. **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 per
`ssh-public-keys`, IP aus Konfiguration bzw. `lxc/{id}/interfaces`). Das `debian`-Standard-Template
bringt `openssh-server` mit; der Bereitstellungsschlüssel wird am Ende wieder entfernt.
5. **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 statt `Sys.AccessNetwork` → `Sys.Modify`
- **8.2+ und 9.x:** `Sys.AccessNetwork` auf `/nodes/<node>` (für `download-url`)
- **nur 9.x:** `VM.GuestAgent.Audit VM.GuestAgent.Unrestricted` (ersetzt `VM.Monitor`)
- **optional:** `Datastore.Allocate` (heruntergeladene Images/Templates wieder löschen),
`Sys.Modify` (privilegierte Container ab 9.0)
- ACL: `/vms` oder ein Pool, `/storage/<id>`, `/sdn/zones/localnetwork` (bzw. konkrete Zonen), `/`
nur für `Sys.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>`), Benutzer `debian`
- Ubuntu: `cloud-images.ubuntu.com/<codename>/current/<codename>-server-cloudimg-amd64.img` (qcow2),
`SHA256SUMS` (Zeilenformat `<hash>␣*<datei>`), Benutzer `ubuntu`
- LXC: alle `*-standard_*_amd64.tar.zst`; bei mehreren Treffern (Debian 13: `13.1-2` und `13.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):
```ts
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).
```yaml
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) |
```prisma
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 `server` als Standard und
`[instances.<name>]` für mehrere Instanzen – nicht `profiles`, das hieße wie `--profile`), Rechte
0600; `PROXDEPLOY_URL`/`PROXDEPLOY_TOKEN`/`PROXDEPLOY_INSTANCE` überschreiben.
- **Kurze Anfragen:** Die CLI schickt nur gesetzte Flags. Der Server füllt nach der Vorlage Storage,
Bridge, Node (Server-Standards bzw. Verbindungstest), Benutzer (eigener Name) und SSH-Schlüssel
(hinterlegte) – `anfrageErgaenzen` in `src/server/services/ausrollen.ts`.
- **Abschlussbox** (`abschluss` im Auftrag) berechnet der Server; Web und CLI zeigen dieselben Zeilen.
- **Generierter Code:** Antwortschemas als zod in `src/lib/schemas/api.ts`, daraus `openapi.json`
(`pnpm openapi`) und daraus die Go-Typen (oapi-codegen, nur Modelle). Anfragen gehen als Map hinaus
(sonst schickte Go leere Pflichtfelder statt sie wegzulassen). `pnpm cli:pruefen` schlägt fehl,
wenn etwas veraltet ist; `tests/api-schemas.test.ts` prüft echte Antworten gegen die Schemas.
- `login <url>` fragt Benutzername/Passwort ab, tauscht sie über `POST /api/v1/auth/token` gegen ein
Token und speichert es (oder `--token`). Tokens: `pd_` + 256 Bit, gespeichert nur als SHA-256.
- 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/plan` liefert `cliBefehl`), 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=true` wertet `X-Forwarded-For/Proto` aus; Cookies `Secure`
abhängig vom tatsächlichen Schema (Lehre aus ProxLedger 0.11.1); SSE mit `X-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` (einmalig `prisma migrate deploy`).
Ein gemeinsames Image, Next `output: "standalone"`, Non-root.
- `deploy/install-lxc.sh`: auf dem Proxmox-Host ausführen → legt Debian-13-LXC an (per `pct`),
installiert Node, PostgreSQL, systemd-Units für app und worker, erzeugt `.env` mit Zufallsschlüssel.
- `.env.example` mit allen Variablen (`DATABASE_URL`, `PROXDEPLOY_SCHLUESSEL`, `SITZUNG_GEHEIMNIS`,
`APP_URL`, `TRUST_PROXY`, `OIDC_*`, `WORKER_PARALLEL`, `LOG_LEVEL`, `ADMIN_BENUTZER/PASSWORT` fü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)
1. **Ubuntu 26.04 LTS** wird mitgeliefert (Debian 12/13, Ubuntu 22.04/24.04/26.04).
2. **Templates booten UEFI** (OVMF, q35, EFI-Disk mit `pre-enrolled-keys=1`); `vm.bios` pro
Distribution überschreibbar.
3. **Template-VMID:** keine feste Zuordnung. ProxDeploy nimmt die **nächste freie ID im
Template-Bereich** des Servers (Standard 9000–9999), ermittelt aus `/cluster/resources` plus
eigenen Reservierungen – dieselbe Sperre wie bei Gast-IDs. Welche VMID welches Template ist,
steht in der Tabelle `Template` und als Tag (`proxdeploy-template`, `distro-debian-13`) in Proxmox.
4. **Namensabbildung** aus Abschnitt 7 und deutsche API-Pfade bestätigt; Katalog-YAML bleibt englisch.
5. **Domain** für `APP_URL` und OIDC-Redirect folgt später über `.env`.