Write down what the audits found and what a real run needs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/betriebsmodus
parent
5d12983ef7
commit
36cb684d8b
|
|
@ -0,0 +1,267 @@
|
|||
# Handoff — vom grünen Test zum ersten echten Durchlauf
|
||||
|
||||
**Stand:** 2026-07-30, `main` auf `v1.3.44`, **1686 Tests grün.**
|
||||
**Zweck:** In dieser Nacht wurden Host-Onboarding, VM-Erstellung und der Stripe-Prozess
|
||||
vollständig geprüft und der größte Teil der Funde behoben. Was bleibt, ist unten
|
||||
beschrieben. Die Kernaussage in einem Satz: **die Testsuite war grün, und trotzdem
|
||||
hätte kein Host durchinstalliert und keine VM ausgeliefert werden können.**
|
||||
|
||||
---
|
||||
|
||||
## 0. Die Lektion, die den ganzen Rest erklärt
|
||||
|
||||
Alle Funde waren gegen eine grüne Suite unsichtbar, und zwar systematisch:
|
||||
|
||||
- `HostOnboardingEndToEndTest` fährt die ganze Pipeline gegen `FakeRemoteShell`,
|
||||
dessen Standardantwort **Erfolg** ist. Der Test beweist, dass die Zustandsmaschine
|
||||
richtig durchzählt — über das Verhalten irgendeines Befehls sagt er nichts.
|
||||
- Rund ein Dutzend Tests schrieben in `config('provisioning.plans.*')`, einen
|
||||
Schlüssel, den es seit dem Katalog-Umbau nicht mehr gibt (und es existiert ein
|
||||
Test, der genau das behauptet: `expect(config('provisioning.plans'))->toBeNull()`).
|
||||
Jede dieser Zeilen war wirkungslos; die Tests waren aus dem falschen Grund grün.
|
||||
- Mehrere Tests prüften eine Spalte, die der Code eine Zeile vorher selbst
|
||||
geschrieben hatte.
|
||||
|
||||
**Konsequenz für die nächste Session:** Ein Testlauf ist keine Abnahme. Jeder
|
||||
Arbeitsblock unten endet mit einem Schritt auf echter Hardware, nicht mit „grün".
|
||||
|
||||
---
|
||||
|
||||
## 1. Entscheidungen aus dem Brainstorming (verbindlich)
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|---|---|
|
||||
| Woher kommt die Software im Gast? | **Vorlage bringt Docker + Compose mit, Nextcloud installiert sich beim ersten Start** aus `NEXTCLOUD_ADMIN_*`, die die Pipeline in die `.env` schreibt. Vorlage bleibt kundenunabhängig, Passwörter werden nie in ein Image gebacken. |
|
||||
| Proxmox-Version | **PVE 9 auf Debian 13 trixie.** Die Codename-Tabelle in `InstallProxmoxVe` kennt beides; PVE 8/bookworm kann bleiben, getestet und unterstützt wird 9. |
|
||||
| Traefik | **Systemdienst (Binary + systemd-Unit), pro Host**, installiert *und geprüft* von einem neuen Pipeline-Schritt. Kein Docker auf dem Hypervisor — nftables bleibt einziger Eigentümer der Host-Firewallregeln. |
|
||||
| `vmbr0` | **Vollautomatisch**, mit Selbstrücknahme-Zeitgeber (siehe Block B). |
|
||||
| Testumgebung | **Ein dedizierter Server mit Debian 13**, neu aufsetzbar. Kein zweiter Rechner nötig. |
|
||||
| Testpaket | Eigene Paketfamilie **„Test"**: 20 GB Platte, 1 GB RAM, 1 Kern, 5 GB Kontingent. Nach dem Beweis wieder aus dem Verkauf nehmen. |
|
||||
| ACME | HTTP-01 pro Host. **Kein** Wildcard/DNS-01 — pro Kunde entsteht genau ein Zertifikat, unabhängig von der Host-Zahl. |
|
||||
|
||||
**Nicht geeignet:** Hetzner Cloud (CPX/CX) — keine verschachtelte Virtualisierung,
|
||||
also `/dev/kvm` nicht vorhanden, also startet kein Gast. Unabhängig von der Größe.
|
||||
Bei netcup-VPS ist es produktabhängig; entscheidend ist
|
||||
`grep -cE 'vmx|svm' /proc/cpuinfo && ls -l /dev/kvm`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Was heute Nacht behoben und ausgeliefert wurde
|
||||
|
||||
Sechs Commits, `8440266` → `9da1358`, Tag `v1.3.44`.
|
||||
|
||||
### Stripe / Geld
|
||||
- **Nichts konnte ein Stripe-Abo beenden.** Kündigung und Widerruf schrieben nur
|
||||
lokale Zustände; Stripe erfuhr nie davon, und die nächste Abbuchung wurde
|
||||
**als echte Rechnung aus der lückenlosen Serie ausgestellt und versendet** —
|
||||
monatlich, unbegrenzt, für eine abgeschaltete Leistung. Jetzt: Kündigung zum
|
||||
Periodenende, Widerruf sofort, und eine Rechnung wird nur noch ausgestellt, wenn
|
||||
ihr Abrechnungszeitraum **vor** dem Vertragsende begann.
|
||||
- Kündigung lief auf **Monatstakt auch bei Jahresverträgen** (bis zu elf bezahlte
|
||||
Monate verloren). Jetzt aus `current_period_end`.
|
||||
- **Einrichtungsgebühr** wurde überall beworben und **nie berechnet** — jetzt eine
|
||||
Einmalposition am Checkout, brutto, auf der ersten Rechnung.
|
||||
- Das Kundenportal zeigte **fünf erfundene Rechnungen**, eine erfundene
|
||||
Ausgabenkurve und eine erfundene nächste Abbuchung, während die echten Belege
|
||||
existierten und nirgends im Portal auftauchten. Jetzt die echten.
|
||||
- **Widerruf erstattet voll** (Ihre Entscheidung), Stornorechnung für **jede**
|
||||
Abbuchung im Zeitfenster, nicht nur die erste.
|
||||
- Ein Absturz nach dem Auftrags-Commit ließ einen bezahlten Kauf **dauerhaft
|
||||
unfakturiert** — `resume()` holt das jetzt nach, idempotent.
|
||||
- Klick auf „Upgrade" löschte stillschweigend eine vorgemerkte Abstufung und tat
|
||||
sonst nichts. Behoben.
|
||||
- **Reverse-Charge-Kunden zahlten 20 % zu viel**: ein Stripe-Preis für alle, also
|
||||
Bruttobetrag auch für den, dem keine Steuer berechnet wird — und der Beleg wies
|
||||
den ganzen Betrag als Netto aus. Jetzt **zwei Preise je Paket** (brutto/netto),
|
||||
Auswahl an der Kasse über `TaxTreatment`.
|
||||
- **UID-Prüfung gegen VIES** gebaut (vorher setzte niemand `vat_id_verified_at`,
|
||||
Reverse Charge konnte also nie auslösen). Drei Ergebnisse: bei „Register nicht
|
||||
erreichbar" wird **nichts** geschrieben.
|
||||
|
||||
### Host-Onboarding
|
||||
- **Debian-Codename war fest auf `bookworm`** — ein heutiger Server hätte PVE-8-Quellen
|
||||
gegen Debian 13 bekommen. Jetzt aus `/etc/os-release`, unbekannte Releases brechen ab.
|
||||
- **WireGuard-Sackgasse:** Die Tunnel-IP wurde gespeichert, bevor der Tunnel bewiesen
|
||||
war; danach wählte *jeder* Verbindungsversuch inklusive Konsolen-Wiederholung diesen
|
||||
kaputten Tunnel, und der reparierende Code war unerreichbar. Rettung nur per
|
||||
Datenbank-Handarbeit. Jetzt gilt der Tunnel erst mit bewiesenem Handshake.
|
||||
- **wg0 hätte den Neustart nicht sicher überlebt** (`||`-Fallback verdeckte die
|
||||
fehlende systemd-Aktivierung); **der Neustart konnte eine nicht mehr bootende
|
||||
Maschine hinterlassen**; **`ConfigureProxmox` tat nichts** (kein `vmbr0`, Datacenter-
|
||||
Firewall aus → die „nur 80/443"-Regeln der Kunden-VMs waren **wirkungslos**);
|
||||
der Rolle fehlte **`Sys.Modify`** (jede Kundenbereitstellung wäre am Backup-Schritt
|
||||
gestorben); nftables verwarf **jedes ICMP** (IPv6 und PMTUD gebrochen);
|
||||
`api_token_ref` hing an `APP_KEY` statt am `SECRETS_KEY`.
|
||||
- Neu: **`VerifyVmTemplate`** — ein Host geht nicht mehr auf „aktiv", wenn die
|
||||
Vorlage fehlt. Vorher starb der erste bezahlte Auftrag *nach* der Zahlung.
|
||||
|
||||
### VM-Erstellung
|
||||
- **Das 14-tägige Kapazitätsparken war unerreichbar:** Abfrageintervall 120 s gegen
|
||||
Schritt-Zeitlimit 60 s, also lief jeder Wiedereinstieg in eine Zeitüberschreitung.
|
||||
Ein bezahlter Auftrag ohne freien Host war nach **sechs Minuten** ein Fehlerfall.
|
||||
Die ganze Warteschlange war toter Code.
|
||||
- **Jeder `occ`-Aufruf lief als root** statt `www-data`; Nextcloud verweigert das.
|
||||
Jetzt ein gemeinsamer Erbauer (`App\Support\NextcloudOcc`) plus ein Test, der
|
||||
jede neue `docker compose exec`-Stelle außerhalb davon verbietet.
|
||||
- Ein **einzelner zuckender HTTPS-Test verurteilte eine fertige Cloud** zum Fehlerfall.
|
||||
- Eine nachgewiesene eigene Domain wurde **nie geroutet**, wenn zufällig ein Neustart
|
||||
lief; gekaufter Speicher wurde **abgerechnet und nie geliefert**. Der „läuft schon
|
||||
etwas?"-Schutz fragt jetzt, ob der laufende Lauf die Arbeit **tatsächlich abdeckt**.
|
||||
- Die `address`-Pipeline lief in der falschen Reihenfolge (Zertifikat vor
|
||||
vertrauenswürdigem Hostnamen → gültiges Zertifikat auf Nextclouds
|
||||
„untrusted domain"-Fehlerseite).
|
||||
- Eine geänderte Gast-IP war **unreparierbar** (502 ohne Weg zurück).
|
||||
- Der Hetzner-DNS-Abgleich las **nur die erste Seite** → ab ~100 Einträgen ein
|
||||
**zweiter A-Record** für denselben Namen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was noch zu tun ist — vier Blöcke, in dieser Reihenfolge
|
||||
|
||||
### Block A — Goldene VM-Vorlage (kritischer Pfad, blockiert alles)
|
||||
Es gibt **keine** Vorlage auf `template_vmid 9000`. Ohne sie kann nichts ausgeliefert
|
||||
werden. Zu tun:
|
||||
- Debian-13-VM auf dem dedizierten Server, ~20 GB Platte, cloud-init-fähig
|
||||
(`qemu-guest-agent` **muss** drin sein — `WaitForGuestAgent` hängt daran).
|
||||
- Docker + Compose, `/opt/nextcloud/` mit `docker-compose.yml`, das
|
||||
`NEXTCLOUD_ADMIN_USER`/`NEXTCLOUD_ADMIN_PASSWORD`/`MYSQL_PASSWORD` aus `.env` liest
|
||||
und beim ersten Start selbst installiert.
|
||||
- **`user: www-data` am `app`-Dienst** — sonst schlagen alle `occ`-Aufrufe fehl.
|
||||
- Partition als **letzte** auf der Platte, **kein LVM** — `GrowGuestFilesystem` kann
|
||||
eine nicht-letzte Partition nicht vergrößern und erkennt LVM bewusst nicht
|
||||
(es würde sonst ein Kontingent über ein nicht gewachsenes Dateisystem schreiben).
|
||||
- Als Proxmox-Vorlage konvertieren, vmid 9000.
|
||||
- **Schriftliches Runbook dazu**, versioniert im Repo. Die Vorlage ist ab dann Ihr
|
||||
wichtigstes Artefakt und gehört nicht in mündliches Wissen.
|
||||
|
||||
### Block B — `vmbr0` vollautomatisch (riskantester Code des Vorhabens)
|
||||
Eigener Schritt, eigener Abnahmetest. Muster:
|
||||
1. Aktuelle Netzkonfiguration sichern.
|
||||
2. Systemd-Timer auf 5 Minuten, der sie zurückspielt und neu startet.
|
||||
3. Brücke aktivieren.
|
||||
4. **Von außen** prüfen, ob der Host antwortet.
|
||||
5. Erst dann den Timer abbestellen.
|
||||
|
||||
Der Schritt muss **erkennen**, was er vorfindet — DHCP auf einer Schnittstelle
|
||||
(Cloud) gegen geroutete Einzel-IP mit Gateway außerhalb des Subnetzes
|
||||
(Hetzner-Dedicated) gegen netcup. Eine Brücke, die für den einen Fall richtig ist,
|
||||
nimmt den anderen vom Netz. `ConfigureProxmox` bricht heute bewusst mit einer
|
||||
Anleitung ab; dieser Block ersetzt das.
|
||||
|
||||
### Block C — Traefik als Systemdienst
|
||||
Neuer Schritt zwischen `SecureHostFirewall` und `CompleteHostOnboarding`:
|
||||
- Binary installieren, systemd-Unit, statische Konfiguration mit `websecure`-Entrypoint
|
||||
und `letsencrypt`-certResolver, **deren Namen zu dem passen müssen, was
|
||||
`SshTraefikWriter::render()` schon ausgibt** — hier zuerst nachlesen, nicht raten.
|
||||
- Dynamisches Verzeichnis anlegen (`TRAEFIK_DYNAMIC_PATH`, Vorgabe
|
||||
`/etc/traefik/dynamic`), Dateiprovider darauf zeigen.
|
||||
- **Prüfen, nicht nur installieren:** Dienst läuft, 80/443 belegt, Ping-Endpunkt
|
||||
antwortet. Ein Host darf nicht „aktiv" werden, wenn das Routing nicht steht.
|
||||
- ACME braucht Port 80 **erreichbar** — die Firewall öffnet ihn, aber Traefik muss
|
||||
lauschen, bevor das erste Zertifikat angefordert wird.
|
||||
|
||||
### Block D — Kleinigkeiten und eine kaufmännische Entscheidung
|
||||
- **Rollen-Konvergenz auf bereits aktiven Hosts.** `Sys.Modify` erreicht sie nicht:
|
||||
ein abgeschlossenes Onboarding kann nicht wiederholt werden, ein Kommando fehlt.
|
||||
- **Traffic-Pakete (Ihre Entscheidung):** Der Auftrag ist einmalig, das Feld
|
||||
`traffic_addons` ist ein *dauerhafter* Zähler. Hochzählen = 1000 GB für immer aus
|
||||
einer Zahlung; nichts tun = der Kunde zahlte für nichts. Der Code protokolliert
|
||||
das derzeit laut, statt still eines von beiden zu tun.
|
||||
- `pveum acl modify … || true` und `pveum user add … || true` verdecken weiter
|
||||
Fehler — dieselbe Klasse wie der behobene `role add`.
|
||||
- `VerifyVmTemplate` prüft nur **Existenz**, nicht `template: 1`.
|
||||
- `applyFirewall()` setzt **nicht** `firewall=1` an `net0` der VM — ohne das greifen
|
||||
die Gastregeln trotz Datacenter-Firewall nicht. Prüfen, ob die Vorlage es mitbringt.
|
||||
- `stripe_addon_prices.stripe_price_id` ist **nicht** eindeutig (Plan-Seite schon):
|
||||
zwei Zeilen könnten einen Preis teilen, Archivieren würde den anderen mitentziehen.
|
||||
- `clupilot:end-due-services` schließt den **Vertrag** nicht — ein geschenkter Vertrag
|
||||
bleibt nach Ende der Instanz für immer `active` und zählt in Umsatz/Dashboard mit.
|
||||
- Eine Abbuchung, die **vor** dem Widerruf entstand und **danach** bezahlt wird,
|
||||
bekommt einen Beleg, wird aber nicht storniert oder erstattet — ohne Signal.
|
||||
- `Livewire\Admin\Provisioning::isStale()` erbt das neue 14-Tage-Budget: ein geparkter
|
||||
Lauf mit totem Worker wird zwei Wochen nicht als hängend gemeldet.
|
||||
|
||||
---
|
||||
|
||||
## 4. Was gesetzt sein muss, bevor ein echter Lauf Sinn hat
|
||||
|
||||
Auf **diesem** Entwicklungsserver ist alles vorhanden; auf dem Live-Server ist es
|
||||
ungeprüft. Vor dem Durchlauf einzeln bestätigen — jedes fehlende Stück bricht die
|
||||
Kette an einer anderen Stelle, und mehrere brechen sie **nach** der Zahlung.
|
||||
|
||||
**Geheimnisse (Vault, Konsole → Integrationen):**
|
||||
- `ssh.private_key` — ohne: `EstablishSshTrust` scheitert mit einem phpseclib-Parserfehler
|
||||
- `dns.token` — Hetzner-DNS-Token. Ohne: der A-Record der VM entsteht nicht, der Kunde
|
||||
hat keine Adresse. **Braucht Schreibrecht auf die Zone.**
|
||||
- `stripe.secret` und `STRIPE_WEBHOOK_SECRET` — ohne: kein Auftrag entsteht überhaupt
|
||||
- `monitoring.token` — Uptime-Kuma-Brücke, `RegisterMonitoring`
|
||||
- `SECRETS_KEY` — **seit heute Nacht auch für `hosts.api_token_ref`.** Fehlt er, ist
|
||||
jeder Host-Token unlesbar und keine Bereitstellung läuft.
|
||||
- `VPN_CONFIG_KEY`
|
||||
|
||||
**Einstellungen:**
|
||||
- `CLUPILOT_DNS_ZONE` = **`clupilot.cloud`** (nicht `.com`). Vorgabe im Code ist
|
||||
richtig; auf diesem Server stand in der `.env` versehentlich `.com`.
|
||||
- `CLUPILOT_WG_HUB_PUBKEY`, `CLUPILOT_WG_ENDPOINT`, `CLUPILOT_WG_SUBNET` — und der
|
||||
**Hub muss laufen und auf 51820/udp erreichbar sein**. Die Hosts sind Initiatoren;
|
||||
der offene UDP-Port ist eine Anforderung an den Hub, nicht an die Hosts.
|
||||
- `CLUPILOT_DNS_HOSTS_DIR` — dnsmasq-Verzeichnis für die internen Hostnamen,
|
||||
geteiltes Volume mit dem `vpn-dns`-Container
|
||||
- `TRAEFIK_DYNAMIC_PATH`
|
||||
- **Firmendaten vollständig** (Name, Adresse, PLZ, Ort, UID) **und Steuersatz** —
|
||||
sonst verweigert `IssueInvoice`, und der Kunde bekommt keinen Beleg
|
||||
- **Mindestens ein Rechenzentrum** angelegt — `ValidateHostInput` verlangt es
|
||||
- **Postfächer** eingerichtet — sonst erreicht die Zugangsdaten-Mail den Kunden nicht
|
||||
|
||||
**Betrieb:**
|
||||
- `queue`, `queue-provisioning`, `scheduler` und `reverb` laufen — die Pipeline ist
|
||||
warteschlangengetrieben, ohne Worker passiert schlicht nichts
|
||||
- Stripe-Katalog nach dem Update: `stripe:sync-catalogue` **und**
|
||||
`stripe:reprice-subscriptions` (je zuerst `--dry-run`). Der Abgleich legt jetzt
|
||||
**zwei** Preise je Paket an; bis er lief, kann ein geprüfter EU-Firmenkunde nicht
|
||||
bestellen — das ist Absicht.
|
||||
|
||||
---
|
||||
|
||||
## 5. Reihenfolge des ersten echten Durchlaufs
|
||||
|
||||
1. Testpaket „Test" in der Konsole anlegen (20 GB / 1 GB / 1 Kern / 5 GB) und veröffentlichen.
|
||||
2. Abschnitt 4 durchgehen und **jedes** Stück bestätigen.
|
||||
3. Debian 13 auf dem dedizierten Server, Host in der Konsole anlegen, onboarden.
|
||||
Erwartung nach Block B/C: läuft bis `active` durch. Ohne Block B bricht es bei
|
||||
`vmbr0` ab — das ist derzeit gewollt.
|
||||
4. Vorlage bauen (Block A), `VerifyVmTemplate` muss sie finden.
|
||||
5. Testpaket kaufen. Erwartung: 16 Schritte bis `CompleteProvisioning`, Nextcloud
|
||||
erreichbar unter `<subdomain>.clupilot.cloud` mit gültigem Zertifikat,
|
||||
Speicherkontingent greift, Zugangsdaten-Mail kommt an.
|
||||
6. Eigene Domain hinterlegen, TXT-Nachweis, prüfen, dass sie **wirklich ausgeliefert**
|
||||
wird (nicht nur „nachgewiesen").
|
||||
7. Kündigen. Prüfen, dass Stripe es erfährt, die Leistung zum Periodenende endet,
|
||||
Route und DNS-Eintrag verschwinden.
|
||||
8. Zweiten Kauf als Privatperson, innerhalb 14 Tagen widerrufen. Prüfen: volle
|
||||
Erstattung, Stornorechnung, Leistung endet.
|
||||
|
||||
Jeder Schritt, der scheitert, ist ein Fund — und der Server darf dabei kaputtgehen,
|
||||
Debian ist neu eingespielt in Minuten.
|
||||
|
||||
---
|
||||
|
||||
## 6. Arbeitsweise in dieser Codebasis (für die neue Session)
|
||||
|
||||
- **Eine zweite Claude-Session arbeitet im selben Arbeitsverzeichnis und im selben
|
||||
Git-Index.** In dieser Nacht hat ein einziges `git commit` ohne Pfadangabe deren
|
||||
vorgemerkte Dateien mitgenommen und dabei `VERSION` sowie zwei ihrer Dateien
|
||||
zurückgedreht. **Immer** `git add -- <pfade>` und `git commit -F - -- <pfade>`,
|
||||
**nie** `git add -A`, `git add .`, `git commit -a` oder ein nacktes `git commit`.
|
||||
- `pint` nur auf eigene Pfade, **nie** `--dirty` — das Repo ist unter der
|
||||
Standardvorgabe nicht durchgängig formatiert und würde fremde Dateien umschreiben.
|
||||
- Regeln R11–R24 in `CLAUDE.md` sind nicht verhandelbar und werden per Test erzwungen.
|
||||
R22 besonders: eine Prüfrunde, eine Fix-Runde, dann parken.
|
||||
- Tests laufen auf **SQLite im Speicher** (`phpunit.xml` erzwingt es), gleichzeitige
|
||||
Läufe kollidieren also nicht, und die Entwicklungsdatenbank wird nie angefasst.
|
||||
- Bricht die ganze Suite auf einmal zusammen, ist das fast immer eine Datei der
|
||||
anderen Session mitten im Schreiben — einmal neu laufen lassen, bevor man sucht.
|
||||
- Push: `git push "https://x-access-token:$TOKEN@git.bave.dev/boban/CluPilotCloud.git" main`
|
||||
mit `GIT_ACCESS_TOKEN` aus der `.env`; ein einfaches `git push` scheitert.
|
||||
- Ein Update auf dem Live-Server greift nur bei einem **`v*`-Tag** — `VERSION` anheben,
|
||||
taggen, Tag mitpushen.
|
||||
Loading…
Reference in New Issue