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 mitlocal-lvmoderlocal-zfskommt also gar nicht erst hierher — die nehmen Platten längst, und anlocalwird dort nicht gerührt. - Sonst: die vorhandene Inhaltsliste aus
/etc/pve/storage.cfglesen undimagesanhä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/pveist 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.sherwartet (log,die,http_download,http_get). Bewusst eine eigene, kleine Fassung stattclupilot-bootstrap.shanzufassen: 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: 1nachweisen. - 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:
- Kein Katalog veröffentlicht →
advance(). Dieselbe Ausnahme, dieVerifyVmTemplateschon macht: eine Installation, die ihren ersten Host übernimmt, bevor sie Pakete veröffentlicht, verlangt keine Vorlage. - 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". - Gebaut wird nur die Fehlliste, nie alles Verlangte.
create_proxmox_templaterä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. - 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
vmbr0anlegen.qm create --net0 virtio,bridge=vmbr0schreibt nur Konfigurationstext und stört sich nicht an einer fehlenden Brücke. Die Brücke ist ein eigener offener Punkt inConfigureProxmox.- Die Proxmox-Version fixieren. Eigener offener Punkt.
- Den Rettungssystem-Weg wiederbeleben. Er bleibt liegen; er erbt die
Korrekturen an
template.shnur, 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, nichtpoll.failed→failmit dem Grund ausnote;statewird entfernt.ok, abertemplate: 1fehlt →fail.- Frist überschritten →
fail, und die Prozessgruppe wird beendet. - Start hinterließ keinen Status →
failstatt endlos neu zu starten. - Nach einem
failist die Frist weg, damit „Wiederholen" wirklich neu anfängt. - Status aus einem früheren Lauf:
ok→ neu bauen; lebendesrunning→ ü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 ohnetemplate: 1besteht 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.