265 lines
14 KiB
Markdown
265 lines
14 KiB
Markdown
# Vorlagenbau in der Übernahme-Pipeline (`BuildVmTemplate`)
|
|
|
|
*Entwurf, 1. August 2026*
|
|
|
|
## Das Loch
|
|
|
|
`VerifyVmTemplate` **meldet**, dass eine Vorlage fehlt, und baut ausdrücklich
|
|
keine. Das war richtig, solange niemand entschieden hatte, was in die goldene
|
|
Vorlage gehört — inzwischen ist das entschieden und steht in
|
|
`deploy/bootstrap/lib/template.sh`. Damit ist die Übernahme eines Hosts heute an
|
|
genau einer Stelle unfertig: der Betreiber muss die Vorlage von Hand bauen oder
|
|
von einem anderen Knoten kopieren, sonst bleibt der Lauf bei `verify_vm_template`
|
|
stehen.
|
|
|
|
Das Abnahmeziel ist eine frische Debian-13-Maschine, Host anlegen, zusehen,
|
|
`active` — ohne einen Handgriff. Der Vorlagenbau ist der Schritt, der dazwischen
|
|
fehlt.
|
|
|
|
## Die Entscheidung: das vorhandene Skript ausführen, nicht in PHP nachbauen
|
|
|
|
`template.sh` sind 252 geprüfte Zeilen, die drei Fallen abdecken, die je einen
|
|
bezahlten Auftrag gekostet haben. Eine zweite Fassung in PHP wäre genau der
|
|
Fehler, den die Wege-Entscheidung vom 1. August vermeiden sollte: zwei
|
|
Installationen, die bei jeder Proxmox-Version nachgezogen werden müssen, und die
|
|
zweite fällt erst auf, wenn jemand sie benutzt.
|
|
|
|
Also: **die Datei wortgleich per SSH auf den Host laden und dort ausführen.**
|
|
Was heute nur `clupilot-bootstrap.sh` konnte, kann danach die Pipeline — aus
|
|
derselben Datei.
|
|
|
|
## Aufbau
|
|
|
|
### 1. `deploy/bootstrap/lib/template.sh` — erweitert, nicht kopiert
|
|
|
|
Fünf Änderungen an der einen Fassung. Der Rettungssystem-Weg erbt sie
|
|
automatisch, sollte er je wiederbelebt werden.
|
|
|
|
**`ensure_image_storage()` — neu.** Auf einem per Debian aufgesetzten Proxmox
|
|
ist `local` eine Verzeichnis-Ablage, deren Inhaltsliste standardmäßig **keine
|
|
Platten** enthält. `detect_vm_storage()` findet dann nichts und der Bau stirbt,
|
|
bevor er anfängt — und `RegisterCapacity` meldete hinterher Kapazität 0. Das
|
|
wäre ein Host, der in der Liste fertig aussieht und auf den nie ein Kunde gelegt
|
|
werden kann: dieselbe Sorte Fehler wie die fehlende Vorlage, nur leiser.
|
|
|
|
Die Funktion greift **nur ein, wenn keine Ablage Platten annimmt**:
|
|
|
|
- Erst `detect_vm_storage()`. Liefert sie etwas, passiert nichts. Ein Host mit
|
|
`local-lvm` oder `local-zfs` kommt also gar nicht erst hierher — die nehmen
|
|
Platten längst, und an `local` wird dort nicht gerührt.
|
|
- Sonst: die **vorhandene** Inhaltsliste aus `/etc/pve/storage.cfg` lesen und
|
|
`images` **anhängen**. Eine feste Liste zu schreiben würde löschen, was
|
|
jemand auf diesem Host ergänzt hat.
|
|
- Geschrieben wird über **`pvesm set`**, nie in die Datei. `/etc/pve` ist
|
|
pmxcfs; ein von Hand geschriebener Eintrag umgeht Prüfung und Neuladen.
|
|
|
|
**`check_build_space()` — neu.** Abbild, Arbeitskopie und importierte Platte
|
|
sind zusammen ein Vielfaches der Abbildgröße. Ein `df` vorab mit klarer Absage
|
|
ist billiger als ein Abbruch nach zwanzig Minuten mitten im `virt-customize`.
|
|
|
|
**`fetch_cloud_image()` lädt nach `.part`** und benennt erst um, wenn die
|
|
SHA512-Summe stimmt. Bisher schrieb sie direkt ans Ziel; ein abgebrochener Lauf
|
|
hinterließ eine abgeschnittene Datei, die der nächste wegen `[ ! -f "$_image" ]`
|
|
ungeprüft weiterbenutzt hätte.
|
|
|
|
**`customise_cloud_image()` installiert `cloud-guest-utils`** —
|
|
`GrowGuestFilesystem` ruft `growpart` auf, und installiert hat es nie jemand. Es
|
|
funktioniert heute, weil Debians Cloud-Abbild es zufällig mitbringt. Fällt es
|
|
dort einmal heraus, bekommt jeder Kunde stillschweigend ein Kontingent, das nie
|
|
angewendet wird — bemerkt wird das erst, wenn eine Platte voll ist, die laut
|
|
Rechnung dreimal so groß sein sollte. `verify_image_contents()` weist es danach
|
|
als **vierte Falle** nach. Installieren *und* nachweisen: `ping` war auch
|
|
„bringt das System doch mit", und `VM.Monitor` war auch „das gibt es doch".
|
|
|
|
**`create_proxmox_template()` importiert über `qm set --import-from`** statt
|
|
`qm importdisk` mit anschließend geratenem Datenträgernamen. Der bisherige Code
|
|
hängte die Platte als `${storage}:vm-${vmid}-disk-0` ein — dieser Name gilt nur
|
|
bei **Block**-Speichern. Auf einer Verzeichnis-Ablage heißt der Datenträger
|
|
`local:9000/vm-9000-disk-0.qcow2`, und genau diese Ablage ist der Fall, den
|
|
`ensure_image_storage()` gerade erst möglich macht. Das Skript hätte die Platte
|
|
angelegt und dann unter einem Namen eingehängt, den es dort nicht gibt.
|
|
`--import-from` überlässt die Benennung Proxmox, das als einziges weiß, wie sie
|
|
auf dieser Ablage lautet.
|
|
|
|
### 2. `deploy/bootstrap/lib/template-run.sh` — neu, der Treiber
|
|
|
|
Der einzige Shell-Neubau. Er trägt:
|
|
|
|
- einen knappen Vorspann mit den vier Helfern, die `template.sh` erwartet
|
|
(`log`, `die`, `http_download`, `http_get`). Bewusst eine eigene, kleine
|
|
Fassung statt `clupilot-bootstrap.sh` anzufassen: das ist der stillgelegte
|
|
Weg, und die Helfer sind curl-Hüllen ohne Entwurfsentscheidung — die 252
|
|
Zeilen mit den Fallen bleiben die eine Fassung, und nur darum ging es.
|
|
- die Phasen in Reihe: Platz prüfen → `libguestfs-tools` → Ablage sichern →
|
|
Abbild laden → Aufbau prüfen (Falle 2) → **Arbeitskopie ziehen** → umbauen →
|
|
Inhalt prüfen (Fallen 1, 3, 4) → je VMID Vorlage anlegen → `template: 1`
|
|
nachweisen.
|
|
- die Statusdateien.
|
|
|
|
**Die Arbeitskopie** ist der Grund, warum ein Wiederanlauf nichts erbt: das
|
|
geprüfte Abbild bleibt unangetastet liegen, umgebaut wird eine Kopie. Ein
|
|
Abbruch mitten im `virt-customize` hinterlässt sonst ein halb umgebautes Abbild,
|
|
das der nächste Lauf für fertig heruntergeladen hält.
|
|
|
|
**Statusprotokoll** im Arbeitsverzeichnis `/var/lib/clupilot/template-build`:
|
|
|
|
| Datei | Inhalt |
|
|
|---|---|
|
|
| `state` | `running` \| `ok` \| `failed` |
|
|
| `pid` | PID des Treibers |
|
|
| `phase` | laufende Phase, für die Fortschrittszeile im Protokoll |
|
|
| `note` | Grund im Fehlerfall |
|
|
| `build.log` | alles |
|
|
|
|
### 3. `App\Provisioning\Steps\Host\BuildVmTemplate`
|
|
|
|
Zwischen `VerifyProxmoxApi` und `VerifyVmTemplate`: bauen, dann prüfen. Vor
|
|
`RegisterCapacity`, damit die Ablagenerweiterung schon gewirkt hat, wenn die
|
|
Kapazität gezählt wird.
|
|
|
|
Ablauf je Aufruf:
|
|
|
|
1. Kein Katalog veröffentlicht → `advance()`. Dieselbe Ausnahme, die
|
|
`VerifyVmTemplate` schon macht: eine Installation, die ihren ersten Host
|
|
übernimmt, bevor sie Pakete veröffentlicht, verlangt keine Vorlage.
|
|
2. Welche der verlangten VMIDs melden **nicht** `template: 1`? Keine → `advance()`,
|
|
ohne etwas anzufassen. Die Abkürzung fragt bewusst nach dem Merkmal, nicht
|
|
nach der Existenz: ein abgebrochener Bau hinterlässt eine VM mit der Nummer
|
|
9000, und eine Existenzprüfung sagte dazu „passt".
|
|
3. Gebaut wird **nur die Fehlliste**, nie alles Verlangte. `create_proxmox_template`
|
|
räumt eine VMID weg, bevor es sie anlegt — zwei Pakete mit zwei Vorlagen, von
|
|
denen eine steht, hieße die stehende auf dem Weg zu zerstören und bei einem
|
|
Fehlschlag ohne sie dazustehen.
|
|
4. Statusdateien lesen. Sie überleben den Lauf, der sie geschrieben hat, also
|
|
entscheidet zuerst: **hat dieser Lauf überhaupt etwas gestartet?** Das sagt
|
|
die Frist im Run-Kontext.
|
|
|
|
**Status aus einem früheren Lauf** (keine Frist im Kontext):
|
|
|
|
| `state` | Prozess | Ergebnis |
|
|
|---|---|---|
|
|
| `running` | lebt | **übernehmen** — `poll(30)`, eigene Frist setzen |
|
|
| alles andere | — | Dateien hochladen, `nohup setsid` starten, `poll(30)` |
|
|
|
|
Ein `ok` von gestern beantwortet keine Frage, die dieser Lauf gestellt hat. Als
|
|
eigenes Ergebnis gelesen hieße es „der Bau, den ich angestoßen habe, ist fertig
|
|
geworden, ohne zu liefern, worum ich gebeten habe" — und das ist der gewöhnliche
|
|
Fall an dem Tag, an dem der Katalog eine zweite Vorlage bekommt. Ein *lebender*
|
|
Bau ist die Ausnahme: er baut dasselbe, also wird er übernommen statt daneben
|
|
ein zweiter gestartet.
|
|
|
|
**Status aus diesem Lauf** (Frist gesetzt):
|
|
|
|
| `state` | Prozess | Ergebnis |
|
|
|---|---|---|
|
|
| — | — | **`fail`** — der Start hat nicht gegriffen |
|
|
| `running` | lebt (`kill -0`) | `poll(30, phase)` |
|
|
| `running` | tot | **sofort `fail`** |
|
|
| `ok` | — | `fail` — das Skript meldet fertig, die API widerspricht |
|
|
| `failed` | — | `fail(note)` mit den letzten Protokollzeilen |
|
|
|
|
Zeile 3 ist der Punkt, an dem beides schiefgeht, wenn man ihn weglässt: startet
|
|
der Schritt neu, obwohl noch ein Treiber läuft, laufen zwei `virt-customize` auf
|
|
derselben Datei und beide legen VMID 9000 an. Und pollt er weiter, weil in der
|
|
Datei `running` steht, obwohl der Prozess nach einem Neustart weg ist, wartet er
|
|
fünfundvierzig Minuten gegen eine Leiche. **„Läuft noch" heißt: der Prozess
|
|
lebt** — nicht: in der Datei steht `running`.
|
|
|
|
Eigene Frist von 45 Minuten im Run-Kontext, `maxDuration()` 3600 darüber, damit
|
|
die eigene Frist zuerst greift und nicht der allgemeine Schritt-Zeitablauf —
|
|
dieselbe Form wie `RebootIntoPveKernel`.
|
|
|
|
**Aufgeben beendet erst, dann räumt es auf.** Der Fristablauf trifft einen
|
|
Prozess, der noch läuft; nur die Statusdateien zu löschen machte den Schritt
|
|
wiederholbar und ließe den alten Bau weiterlaufen — dieselbe Kollision, von der
|
|
anderen Seite. Beendet wird die **Prozessgruppe** (`kill -TERM -$P`, dann
|
|
`-KILL`), weil der Treiber die meiste Zeit auf ein `virt-customize` wartet, das
|
|
sonst allein weitermahlt. Dass die PID zugleich die Prozessgruppe ist, ist der
|
|
Grund für `setsid` beim Start — und der Grund, warum das Skript seine PID selbst
|
|
per `$$` hinterlegt statt `$!` auf der Startseite: mit `setsid` dazwischen kann
|
|
`$!` auf einen Prozess zeigen, den es schon nicht mehr gibt. Der Startbefehl
|
|
wartet deshalb bis zu 15 Sekunden auf die Datei, sonst hielte der erste Poll
|
|
einen gesunden Bau für tot. Gegen eine wiederverwendete PID prüft der
|
|
Abbruch `/proc/$P/cmdline`.
|
|
|
|
Beim `fail` verschwinden Statusdateien **und** die Frist im Kontext, damit ein
|
|
Wiederholen aus der Konsole wieder ein erster Besuch ist; das geprüfte Abbild
|
|
bleibt liegen und muss nicht neu geladen werden.
|
|
|
|
### 4. `ProxmoxClient::isTemplate()` — neu
|
|
|
|
Zwei Anfragen, weil die falsche Antwort hier **etwas zerstört**: `false` heißt
|
|
für `BuildVmTemplate` „Vorlage fehlt", und der Bau fängt mit `qm destroy
|
|
--purge` an. „Nicht da" und „konnte nicht fragen" dürfen sich deshalb nicht
|
|
ähnlich sehen — und am Rückgabecode sind sie nicht zu unterscheiden: Proxmox
|
|
beantwortet die Konfiguration einer nicht vorhandenen VM mit **500**, demselben
|
|
Code wie einen Knoten in Not.
|
|
|
|
Also klärt die **VM-Liste** (`GET /nodes/{node}/qemu`) die Abwesenheit und sonst
|
|
nichts: fehlt die VMID in einer erfolgreichen Liste, ist sie wirklich nicht da.
|
|
Alles unterhalb einer erfolgreichen Liste **wirft** und landet im
|
|
Wiederholungs-Zweig des Aufrufers — die vorhandene Vorlage überlebt. Erst danach
|
|
klärt das **Konfigurationsdokument** (`GET …/{vmid}/config`) das Merkmal
|
|
`template`.
|
|
|
|
Benutzt von `VerifyVmTemplate` **und** von `VmTemplateCheck`. Beide prüften nur
|
|
`vmExists`, beide stellen dieselbe Frage — die eine bei der Übernahme, die
|
|
andere vor dem Verkauf —, und beide hätten eine VM durchgelassen, die zufällig
|
|
9000 heißt.
|
|
|
|
### 5. `PlanVersion::requiredTemplateVmids()` — neu
|
|
|
|
Dieselbe Fensterlogik stand in `VerifyVmTemplate` und `VmTemplateCheck` und wäre
|
|
mit `BuildVmTemplate` zum dritten Mal abgeschrieben worden. Drei Kopien driften,
|
|
und die abgedriftete fände ein Kunde.
|
|
|
|
## Was dieser Entwurf nicht tut
|
|
|
|
- **`vmbr0` anlegen.** `qm create --net0 virtio,bridge=vmbr0` schreibt nur
|
|
Konfigurationstext und stört sich nicht an einer fehlenden Brücke. Die Brücke
|
|
ist ein eigener offener Punkt in `ConfigureProxmox`.
|
|
- **Die Proxmox-Version fixieren.** Eigener offener Punkt.
|
|
- **Den Rettungssystem-Weg wiederbeleben.** Er bleibt liegen; er erbt die
|
|
Korrekturen an `template.sh` nur, falls er je zurückkommt.
|
|
|
|
## Prüfung
|
|
|
|
Pest, vollständig mit Attrappen, wie der Rest der Pipeline:
|
|
|
|
- Kein veröffentlichter Katalog → `advance`, nichts hochgeladen.
|
|
- Vorhandene echte Vorlage → `advance`, kein Start.
|
|
- VM 9000 existiert, ist aber **keine** Vorlage → es wird gebaut.
|
|
- Erster Aufruf lädt alle drei Dateien **wortgleich** hoch (Byte für Byte gegen
|
|
die Repo-Dateien geprüft) und startet abgekoppelt → `poll`.
|
|
- Zwei Vorlagen verlangt, eine steht → nur die fehlende landet in `env`.
|
|
- `running` + lebender Prozess → `poll`, **kein** zweiter Start.
|
|
- `running` + toter Prozess → `fail`, nicht `poll`.
|
|
- `failed` → `fail` mit dem Grund aus `note`; `state` wird entfernt.
|
|
- `ok`, aber `template: 1` fehlt → `fail`.
|
|
- Frist überschritten → `fail`, **und** die Prozessgruppe wird beendet.
|
|
- Start hinterließ keinen Status → `fail` statt endlos neu zu starten.
|
|
- Nach einem `fail` ist die Frist weg, damit „Wiederholen" wirklich neu anfängt.
|
|
- Status aus einem früheren Lauf: `ok` → neu bauen; lebendes `running` →
|
|
übernehmen statt danebenstarten.
|
|
- `isTemplate`: 502 → **wirft** (nicht „fehlt"); VMID nicht in der Liste →
|
|
`false`; `template: 1` → `true`; gewöhnliche VM mit der Nummer → `false`.
|
|
- `VerifyVmTemplate`/`VmTemplateCheck`: eine VM ohne `template: 1` besteht nicht
|
|
mehr.
|
|
- **Jeder Befehl, den der Schritt absetzt, geht durch `sh -n`.** Keine andere
|
|
Prüfung führt diese Shell je aus — ein verrutschtes Anführungszeichen käme
|
|
sonst zuerst auf einem echten Host zur Ausführung, und zwar in dem Schritt,
|
|
der VMs zerstört und neu anlegt.
|
|
|
|
Dazu `sh -n` über alle drei Shell-Dateien, und die awk-Auswertung von
|
|
`storage.cfg` gegen eine echte Beispieldatei durchgespielt (`CLUPILOT_STORAGE_CFG`
|
|
ist genau dafür überschreibbar).
|
|
|
|
## Was hier NICHT geprüft ist
|
|
|
|
Nichts davon lief je gegen einen echten Proxmox-Host. Alle Tests sind Attrappen,
|
|
und die Stellen mit dem größten Risiko sind genau die, die eine Attrappe nicht
|
|
abbilden kann: `qm set --import-from` auf einer Verzeichnis-Ablage, `virt-customize`
|
|
unter PVE 9, `pvesm set` gegen pmxcfs, und ob `nohup setsid` den Bau wirklich
|
|
überleben lässt, wenn phpseclib den Kanal schließt. Die erste Übernahme auf
|
|
echter Hardware ist die Abnahme, nicht dieser Testlauf.
|