docs: provisioning engine v1.0 build handoff (standalone fleet + cluster-per-DC target)

Focused TDD build plan for the engine: topology decision baked in (Option 1
standalone fleet as the software model, cluster-per-datacenter as the growth
target — hosts get datacenter + nullable cluster fields, placement filters by
datacenter). Covers data model, DB-state-machine orchestrator, 15 customer steps,
new ProxmoxClient, Stripe idempotent intake, and wiring the existing admin/customer
progress views to real data via Reverb. References the state handoff for workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 09:31:51 +02:00
parent 49b5b7a523
commit bf5813057b
1 changed files with 210 additions and 0 deletions

View File

@ -0,0 +1,210 @@
# Handoff — CluPilot Provisionierungs-Engine v1.0
**Erstellt:** 2026-07-25
**Für:** frische Claude-Code-Session
**Art:** Feature-Bau-Handoff (TDD)
**Zuerst lesen:** `docs/handoffs/2026-07-25-clupilot-state-handoff.md` (Projekt-Zustand, Workflow, Regeln, Gotchas). Dann dieses Dokument.
**Design-Spec:** die ausführliche Engine-Design-Spec liegt beim **Nutzer** (nicht im Repo). **Vor Baubeginn anfordern** — sie ist bei Detail-Konflikten der Vertrag; dieses Dokument ist der Bau-Fahrplan.
---
## 1. Mission
Aus einer **bezahlten Bestellung** wird vollautomatisch eine **laufende, gesicherte, überwachte Nextcloud-Instanz** — mit persistentem Status, Retry, Timeout, sofort gespeicherten externen IDs und Acceptance-Gate. Ausführungsmodell: **DB-State-Machine + Tick-Orchestrator** (kein monolithischer Job).
## 2. Arbeitsweise (verbindlich)
- **TDD.** Das Superpowers-`test-driven-development`-Skill ist auf der VM installiert (ab dieser Session verfügbar) — nutzen. Jede Step-Klasse + der Orchestrator-Kern test-first. **`ProxmoxClient` in Step-Tests mocken.**
- **Tests laufen gegen sqlite `:memory:`** (siehe State-Handoff §5/§6 — Test-Isolation ist bereits korrekt eingerichtet; nicht kaputt machen).
- **Regeln R1R17** gelten (State-Handoff §9). R15 (Codex-Review clean) vor „fertig". Bei Spec-Konflikt: Design-Spec → `CLAUDE.md`/Regeln → Nachfrage.
- **Kleine, fokussierte Klassen** — eine Step-Klasse = eine Verantwortung.
---
## 3. Topologie-Entscheidung (verbindlich) ⭐
Zwei **getrennte** Ebenen — nicht verwechseln:
### 3.1 Software-Modell = **Standalone-Flotte** (Option 1)
CluPilot ist die **zentrale Konsole** und spricht **jeden Host einzeln per Proxmox-API-Token** an; die Flotte lebt in **CluPilots eigener DB**. **Kein corosync im Steuerungs-Layer.** „Cluster" ist zunächst nur CluPilots **logische Sicht**.
> Konsequenz für die Engine: Ein Host wird **immer** über seine eigene `wg_ip` + Token adressiert. Ob dieser Host physisch allein steht oder Teil eines Proxmox-Clusters ist, ist der Engine **egal**. Damit läuft v1.0 **heute mit einem einzigen Host** und bleibt zukunftsoffen.
### 3.2 Wachstumsziel Infrastruktur = **Cluster pro Rechenzentrum** (Option 3)
Sobald pro Standort ≥23 Hosts stehen und **Wartung ohne Downtime** (Live-Migration) bzw. **HA** gewünscht ist: die Hosts **innerhalb eines RZ** zu einem echten **corosync-Cluster** verbinden (Falkenstein-Cluster, Helsinki-Cluster). **Niemals** ein Cluster über WAN (fsn↔hel) — corosync braucht niedrige Latenz.
Fußangeln fürs spätere Clustern (jetzt nur im Datenmodell vorsehen, nicht bauen):
- **Quorum:** 2 Nodes sind fragil → 3 Nodes oder QDevice.
- **Storage:** Live-Migration braucht shared/replizierten Storage (Start: ZFS+Replikation; groß: Ceph).
- Für reine Wartungs-Umzüge reicht auch offline `qm remote-migrate` zwischen Standalone-Nodes (Proxmox 8) — Cluster erst für **Live**-Migration/HA nötig.
### 3.3 Was daraus fürs Datenmodell folgt (jetzt umsetzen)
- `hosts` bekommt **`datacenter`** (z. B. `fsn`, `hel`) und ein **nullable `cluster`** (Cluster-Name/-ID; v1.0 überall `null` = standalone). Beides jetzt anlegen, damit späteres Clustern **additiv** ist.
- **Placement v1.0 = einfach:** erster **aktiver** Host **im Ziel-Rechenzentrum** mit genug **freier Zusage** (`total_gb reserve Σ instances.quota_gb`). Cluster/Live-Migration/HA/Rebalancing sind **v1.1+**.
- Migration/HA werden **nicht** in v1.0 gebaut, aber das Modell verbaut sie nicht.
---
## 4. Scope
**IN v1.0 (Happy Path + Robustheit):**
1. Datenmodell (Migrations + Models).
2. Orchestrator-Kern: Run-State-Machine, Step-Kontrakt (Advance/Retry/Fail), Tick-Scheduler, `AdvanceRunJob`, Per-Run-Lock, Timeout, append-only Event-Log.
3. `ProxmoxClient`-Service (**NEU**).
4. Die **15 Kunden-Pipeline-Schritte** als idempotente Step-Klassen, inkl. Backup- + Monitoring-Registrierung + Acceptance.
5. Bestell-Eingang: Stripe-Webhook → `orders` mit **Idempotency-Key** → Run-Anlage.
6. Live-Fortschritt: die bereits existierenden Views an echte Daten binden (s. §9).
**NICHT in v1.0 (= v1.1, bewusst weglassen):**
- **Host-Pipeline** / Panel-Host-Onboarding / Hetzner-Robot-Client.
- **Live-Migration, HA, Cluster-Bildung, Rebalancing, Reconciliation-Loop** (Waisen-Abgleich).
- Admin-Skip/Resume-Cleanup, Abort-Cleanup-Pipeline (simples Retry + Pause reicht).
- Kompletter Kündigungs-/Löschprozess.
- Multi-Host-Placement über simple Zusage-Rechnung hinaus.
---
## 5. Voraussetzungen (vor echtem End-to-End-Lauf)
1. **Versioniertes Nextcloud-Blueprint-Template in Proxmox** (einmal manuell bauen → Template; muss **qemu-guest-agent + docker** enthalten). `CloneVirtualMachine` klont daraus; Template-VMID in Config/DB hinterlegen. Fehlt es → Step failt (`Fail`, nicht Retry).
2. **Dev-Proxmox-Host erreichbar** über WG-Tunnel (`hosts.wg_ip:8006`) mit `automation@pve`-Token (Minimal-Rolle). Ohne echten Host läuft nur die **gemockte** Test-Suite.
3. **MariaDB-Entscheidung** betrifft den **Nextcloud-Stack im Blueprint** (Compose im Gast), **nicht** die Panel-DB — nicht verwechseln. Die Engine schreibt nur Config, baut die DB nicht selbst.
4. Panel läuft (ist gebaut, s. State-Handoff).
5. **Stripe** nur Test-Modus in Dev, keine echten Zahlungen.
---
## 6. Bestandscode: Reuse / Neubau / Kollision
**Reuse:**
- `phpseclib3` (SSH/SFTP) — für host-nahe Kommandos falls nötig. Gast-interne occ/Compose-Kommandos laufen aber über **qemu-guest-agent (`guestExec`)**, nicht SSH-Warten.
- `Illuminate\Support\Facades\Crypt` — Muster für verschlüsselte Secrets. Proxmox-Token/Keys ebenso verschlüsselt ablegen (`hosts.api_token_ref`).
- **Laravel Reverb** (konfiguriert) → Live-Fortschritt pushen.
- **`scheduler`-Service** (`schedule:work`) → **Tick** hier registrieren (`routes/console.php` Schedule, jede Minute).
- **`redis`-Queue** → `AdvanceRunJob` (eigene Queue `provisioning` denkbar).
- Livewire-Konventionen + das bestehende **Design-System/Komponenten** (State-Handoff §4) für Admin-Ansichten.
**Neubau (existiert NICHT):**
- **`App\Services\ProxmoxClient`** — Proxmox VE REST (`/api2/json`), Token-Auth `PVEAPIToken=automation@pve!<id>=<secret>`, Basis-URL aus `hosts.wg_ip`. Methoden v1.0: `cloneVm, setCloudInit, resizeDisk, startVm, guestAgentPing, guestExec, getTaskStatus` (UPID-Polling), `applyFirewall`.
> ⚠️ **`HetznerClient` existiert nicht hier** (frischer Build). Falls du irgendwo Hetzner-Cloud-Code als Vorlage suchst: Proxmox-API ist **komplett anders** — nicht vermischen.
- Orchestrator-Kern (Runner, Step-Interface, Result-Objekte, Tick, Lock).
- Engine-Tabellen + Models.
**Kollisionen bewusst trennen:** Es gibt hier **keine** Alt-Domäne (`Server`/`Cluster`/`ProvisioningLog`/`ProvisionGaleraNode`) — der frühere „Server-Manager" wurde **nicht** mitgebaut. Die Engine ist ein sauberer Neuaufbau. Die neue `provisioning_step_events`-Tabelle ist **run-scoped & reich** (nicht mit irgendeinem simplen Log verwechseln).
---
## 7. Datenmodell (Migrations + Models) — Kern zuerst
Neu anlegen (Reihenfolge: runs/events/resources zuerst, dann Fach-Tabellen):
- `customers`, `orders` (`stripe_event_id` **unique** = Idempotency; `status`, `plan`, `amount`).
- `hosts`**inkl. `datacenter`, `cluster` (nullable)**, `wg_ip`, `api_token_ref` (encrypted), Kapazität `total_gb / total_ram / cpu_weight / reserve_pct`, `status`.
- `instances``vmid`, `quota_gb`, `disk_gb`, `subdomain`, `custom_domain`, `nc_admin_ref`, `route_written`, `cert_ok`, `status`, FK `host_id`, `customer_id`.
- `provisioning_runs`**polymorph** `subject_type`/`subject_id`, `pipeline`, `current_step`, `status`, `attempt`, `next_attempt_at`, `started_at`, `error`, `context` (json).
- `provisioning_step_events`**append-only:** `run_id, step, attempt, outcome, message, external_ref, created_at`.
- `run_resources``run_id, host_id, kind, external_id` (Brotkrumen für Idempotenz + spätere Reconciliation).
- `dns_records`, `backups`, `monitoring_targets`, `onboarding_tasks`.
**Kapazität = berechnet** (SUM `instances.quota_gb` je Host), kein separater Ledger. **Placement-Query** liest `total_gb reserve zugesagt`, **gefiltert auf `datacenter` und `status='active'`** (Cluster ignoriert die Query in v1.0).
**R11:** in URLs adressierbare Records (customers/instances/runs) per **UUID**, nicht Integer-PK.
---
## 8. Orchestrator-Kern (Herzstück, zuerst + test-first)
**Run-Status:** `pending → running ⇄ waiting → completed`, plus `paused` (Admin), `failed` (Retries erschöpft/fatal).
**Step-Interface (Vorschlag):**
```php
interface ProvisioningStep {
public function execute(ProvisioningRun $run): StepResult; // Advance | Retry | Fail
}
// StepResult::advance() | ::retry(int $afterSeconds, string $reason) | ::fail(string $reason)
```
**Runner-Pflichten je Ausführung:**
- **Vor Arbeit: Idempotenz-Check** (externe ID / DB-State „schon getan?").
- **Externe IDs sofort** in `run_resources`, **BEVOR** weitergemacht wird (vmid, UPID, hetzner_record_id, backup_job_id).
- Ergebnis in `provisioning_step_events` (append-only) + `provisioning_runs` fortschreiben.
- **Timeout** je Schritt (`started_at + max_duration`); Polling-Schritte mit Gesamt-Deadline.
- **Retry** mit Backoff, `attempt++`, `max_attempts` → sonst `Fail`.
- **Fatal vs. retrybar** entscheidet der Schritt je Fehler (Proxmox-5xx/Timeout = Retry; „Template not found"/ungültige Order = Fail).
**Advancement:**
- **Tick** (Scheduler, jede Minute): Runs `status IN (running,waiting) AND next_attempt_at <= now``AdvanceRunJob` dispatchen.
- **Sofort-Dispatch** bei Run-Anlage + nach erfolgreichem Schritt (nicht auf Tick warten).
- **Per-Run-Lock** (`Cache::lock("run:{id}")`) → nie zwei Worker auf demselben Run.
---
## 9. Die 15 Kunden-Schritte
Namespace `App\Provisioning\Steps\Customer\`. Reihenfolge:
`ValidateOrder → ReserveResources → CloneVirtualMachine → ConfigureCloudInit → StartVirtualMachine → WaitForGuestAgent → ConfigureNetwork → DeployApplicationStack → ConfigureNextcloud → CreateCustomerAdmin → ConfigureDnsAndTls → RegisterBackup → RegisterMonitoring → RunAcceptanceChecks → CompleteProvisioning`
Per-Schritt-Verhalten (Idempotenz-Check, **[E]** externe ID sofort, **[P]** Polling, **[F]** Fail) steht in der **Design-Spec Sektion 3** — dort nachschlagen, nicht neu erfinden. Harte Regeln:
- **Kundenzugang ERST nach `RunAcceptanceChecks` (14).** `CompleteProvisioning` (15) schaltet frei.
- Backup (12) + Monitoring (13) sind **Teil** der Bereitstellung, nicht danach.
- Zahlung ok trotz Fehler → Run `failed` + **manueller** Retry, **kein** Auto-Refund.
- `CreateCustomerAdmin`: Initial-Credential sicher an Kunde, **nie plaintext** persistieren.
- **`ReserveResources`** nutzt das Placement aus §7 (Host im Ziel-`datacenter`).
- occ/Compose im Gast via **guest-agent** (`guestExec`), nicht SSH-Warten.
- **DNS/TLS:** Hetzner-DNS-API legt `kundeXY.clupilot.com` an **[E]** `hetzner_record_id`; Cert via **HTTP-01** (kein DNS-01); Traefik-YAML in File-Provider-Verzeichnis schreiben, dann Cert-Ausstellung pollen **[P]** (Deadline großzügig — DNS-Propagation + Port 80 erreichbar).
---
## 10. Bestell-Eingang (Stripe)
- Webhook-Endpoint (Test-Modus). **Idempotency-Key = `stripe_event_id` unique** → verhindert Doppel-Provisionierung bei Webhook-Retries.
- Bezahlt → `orders`-Row → `provisioning_runs`-Eintrag (`pipeline=customer`, Subject = Order/Customer) anlegen + **sofort dispatchen**.
## 11. Live-Fortschritt (an bestehende UI binden)
- **Admin `/admin/provisioning`** ist aktuell eine **Fixture-View** (`app/Livewire/Admin/Provisioning.php`) → an echte `provisioning_runs` + `provisioning_step_events` binden, live via **Reverb** bei jedem `step_event`. Der `x-ui.progress-stepper` existiert bereits.
- **Kundendashboard**: der Bereitstellungs-Stepper auf `/dashboard` (aktuell Fixtures) → an den Run des Kunden binden; Zugang zur Cloud erst nach Abschluss.
- Panel = reine **View** auf die Tabellen (kein State im Frontend). Broadcast-Kanäle **privat** (authentifiziert), pro-Kunde scopen.
---
## 12. Build-Reihenfolge (jede Phase grün testen)
1. Migrations + Models (runs, step_events, run_resources, instances, hosts[+datacenter/cluster], customers, orders).
2. Orchestrator-Kern test-first (StepResult, Interface, Runner Advance/Retry/Fail/Timeout/Lock, `AdvanceRunJob`, Tick) mit 23 Fake-Steps.
3. `ProxmoxClient` + Fake/Mock-Doppel.
4. Steps 12 (ValidateOrder, ReserveResources — reine DB/Placement, früh testbar, inkl. datacenter-Filter).
5. Steps 37 (VM-Lebenszyklus) gegen gemockten ProxmoxClient; externe-ID-Persistenz + Idempotenz je Step.
6. Steps 810 (Stack/Nextcloud/Admin) via guestExec-Mock.
7. Step 11 (DNS/TLS) — Hetzner-DNS-Client + Traefik-YAML-Writer + Cert-Polling.
8. Steps 1213 (Backup/Monitoring registrieren).
9. Steps 1415 (Acceptance + Complete).
10. Stripe-Webhook + Idempotenz + Run-Anlage.
11. Live-Fortschritt an Admin+Kundendashboard binden (Reverb).
12. End-to-End gegen Dev-Host + Template (falls verfügbar), sonst voll gemockt grün.
## 13. Definition of Done (v1.0)
- [ ] Bezahlte Test-Bestellung erzeugt einen Run, der ohne manuelles Zutun bis `completed` läuft (gegen Dev-Host+Template) ODER voll gemockt grün.
- [ ] Jeder Schritt idempotent: künstlicher Abbruch + erneuter Tick → **keine** Doppel-Ressourcen (verifiziert über `run_resources`).
- [ ] Retry mit Backoff + Timeout + Per-Run-Lock getestet.
- [ ] Externe IDs (vmid/UPID/dns/backup) werden **vor** Weitergabe persistiert (Test simuliert Crash danach).
- [ ] Kundenzugang erst nach bestandener Acceptance.
- [ ] Stripe-Doppel-Webhook → nur EINE Provisionierung.
- [ ] Placement respektiert `datacenter` + freie Zusage; `cluster`-Feld existiert (nullable, ungenutzt in v1.0).
- [ ] Admin- + Kunden-Fortschritt live (Reverb).
- [ ] Pest grün; R12 (Admin/Kunden-Views 0 Konsolenfehler); R15 Codex clean; keine Secrets im Repo.
## 14. Watch-Items
1. **Kein Proxmox-Code vorhanden**`ProxmoxClient` inkl. Auth/UPID-Polling ist echter Neubau. Früh isoliert testen.
2. **Guest-Agent statt SSH** — Nextcloud-VM hat evtl. keinen SSH-Zugang; occ/Compose via `guestExec`. Blueprint muss guest-agent + docker enthalten.
3. **Traefik File-Provider-Pfad** muss beschreibbar sein (wo läuft Traefik, wie schreibt die Engine die YAML? — konkreten Schreibweg festlegen, ggf. via Host/guest).
4. **HTTP-01 braucht Port 80** auf Traefik für die neue Subdomain, bevor das Cert kommt → DNS-Propagation abwarten (Polling-Deadline großzügig).
5. **Secrets** (Proxmox-Token, Hetzner-DNS-Token, Backup-Key-Refs) verschlüsselt (`Crypt`)/aus `.env`, nie in Git.
6. **Topologie:** `cluster` bleibt v1.0 `null`. Nicht verleiten lassen, Cluster/Migration/HA einzubauen — das ist v1.1 und würde v1.0 aufblähen.
7. Gotchas aus dem State-Handoff §6 beachten (`@js()`/Test-Isolation/CSRF/Locale/Vite-Reload).
## 15. Danach (v1.1, NICHT jetzt)
Host-Pipeline (Panel-Onboarding + Hetzner-Robot-Client) · **Cluster-pro-RZ-Bildung + Live-Migration/HA** (Topologie-Wachstumsziel) · Reconciliation-Loop · Admin-Skip/Resume-Cleanup · Kündigungs-/Löschprozess. Die Architektur (polymorpher Run, `run_resources`, Step-Kontrakt, `datacenter`/`cluster` im Host) ist so gebaut, dass das **rein additiv** ist.