CluPilotCloud/docs/superpowers/specs/2026-08-01-vorlagenbau-desi...

14 KiB

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-utilsGrowGuestFilesystem 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 übernehmenpoll(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.
  • failedfail 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: 1true; 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.