Refs #013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
616 lines
34 KiB
Markdown
616 lines
34 KiB
Markdown
# 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`.
|