398 lines
18 KiB
Markdown
398 lines
18 KiB
Markdown
# vmbr0 automatisch bauen
|
||
|
||
**Datum:** 1. August 2026
|
||
**Zustand:** Entwurf abgenommen, Umsetzung folgt
|
||
**Betrifft:** Subsystem A (Host-Übernahme), letzter Punkt vor dem Abnahmeziel
|
||
|
||
---
|
||
|
||
## Das Problem
|
||
|
||
Proxmox auf Debian installiert legt **keine `vmbr0` an** — nur der ISO-Installer
|
||
schreibt sie in `/etc/network/interfaces`. `BuildVmTemplate` legt die Vorlage mit
|
||
`--net0 virtio,bridge=vmbr0` an, und jeder Klon erbt das. Ein Host ohne Brücke
|
||
bedient also keinen einzigen Kunden.
|
||
|
||
Heute lehnt `ConfigureProxmox::refuseWithoutBridge()` genau hier ab und schickt
|
||
den Betreiber an die Tastatur. Das war richtig, solange niemand den Handgriff
|
||
sicher automatisieren konnte. Er ist inzwischen einmal von Hand gemacht worden
|
||
(`pve-fns-1`, 1. August 2026) — mit derselben Rückfahrkarte, die hier
|
||
beschrieben wird.
|
||
|
||
Danach ist das Abnahmeziel erreichbar: frische Debian-13-Maschine, Host anlegen,
|
||
zusehen. Ohne einen einzigen Handgriff.
|
||
|
||
---
|
||
|
||
## Die drei festen Entscheidungen
|
||
|
||
### 1. Derselbe Mechanismus wie `BuildVmTemplate` — Datei fahren, nicht nachbauen
|
||
|
||
Die Shell-Bibliothek wird auf den Host geladen und dort gefahren. Kein PHP-Nachbau
|
||
der Netzlogik. Zwei Fassungen hießen zwei Installationen, die bei jeder
|
||
Proxmox-Version nachgezogen werden müssten — und die zweite fiele erst auf, wenn
|
||
jemand sie benutzt.
|
||
|
||
Der Weg ist gebahnt: `template.sh` + `template-run.sh` + `env`, hochgeladen,
|
||
abgekoppelt gestartet, per Statusdatei und PID abgefragt.
|
||
|
||
### 2. Aus dem laufenden Zustand ableiten, nicht aus der Datei des Anbieters
|
||
|
||
`ip -4 addr`, `ip -4 route`, `/sys/class/net/…` — nicht das, was Hetzner oder
|
||
netcup in `/etc/network/interfaces` geschrieben haben. Was läuft, ist überall
|
||
gleich strukturiert; wie es aufgeschrieben wurde, nicht. Das ist der Teil, der
|
||
mehrere Anbieter zugleich trägt.
|
||
|
||
### 3. Die Rückfahrkarte ist Bedingung, nicht Beiwerk
|
||
|
||
Der Schritt sägt an dem Ast, auf dem er sitzt: SSH läuft zu diesem Zeitpunkt über
|
||
den Tunnel, und WireGuard hängt an derselben Netzwerkkarte, die in die Brücke
|
||
wandert. Eine falsche Brücke nimmt beides mit.
|
||
|
||
Also: **erst den Zeitgeber für die Rücknahme scharf stellen, dann umstellen, dann
|
||
neu verbinden, nachsehen, abbestellen.** Kommt die Verbindung nicht zurück, holt
|
||
der Zeitgeber den alten Zustand, und der Schritt landet in einer Wiederholung
|
||
statt auf einem toten Server.
|
||
|
||
---
|
||
|
||
## Was gebaut wird
|
||
|
||
### `deploy/bootstrap/lib/bridge.sh` (neu)
|
||
|
||
Die Brückenhälfte aus `network.sh` (heute Zeilen 47–254), herausgelöst und
|
||
**allein lauffähig**. Sie borgt sich heute vier Dinge von woanders, und die
|
||
wandern mit hinein:
|
||
|
||
| geborgt | steht heute in |
|
||
|---|---|
|
||
| `detect_primary_interface` | `lib/proxmox.sh:132` |
|
||
| `log` | `clupilot-bootstrap.sh` |
|
||
| `http_get` | `clupilot-bootstrap.sh` |
|
||
| `CLUPILOT_PROBE_URL` | `clupilot-bootstrap.sh:273` |
|
||
|
||
`network.sh` sourced `bridge.sh` künftig und behält seine WireGuard-/nftables-
|
||
Hälfte. **Eine Fassung, zwei Benutzer** — der stillgelegte Rettungssystem-Weg
|
||
und der Debian-Weg.
|
||
|
||
Warum nicht `network.sh` im Ganzen fahren: die zweite Hälfte derselben Datei
|
||
enthält WireGuard-, nftables- und Datacenter-Firewall-Funktionen, die als
|
||
PHP-Schritte längst existieren (`ConfigureWireguard`, `SecureHostFirewall`,
|
||
`ConfigureProxmox`). Sie auf den Host zu legen heißt, einem künftigen Leser zwei
|
||
Fassungen derselben Sache anzubieten — genau der Fehler, den die
|
||
Wege-Entscheidung vermeidet.
|
||
|
||
### `deploy/bootstrap/lib/bridge-run.sh` (neu)
|
||
|
||
Der Treiber, gebaut nach dem Muster von `template-run.sh`:
|
||
|
||
```
|
||
/var/lib/clupilot/bridge/
|
||
state running | ok | failed
|
||
pid PID = Prozessgruppe (setsid)
|
||
phase laufende Phase, für die Konsolenzeile
|
||
note Grund im Fehlerfall
|
||
bridge.log alles
|
||
env von CluPilot geschrieben
|
||
```
|
||
|
||
PID als **allererstes**, `trap on_exit`, und die Regel: „läuft noch" heißt, der
|
||
Prozess lebt (`kill -0`), nie: in der Statusdatei steht `running`.
|
||
|
||
### `App\Provisioning\Steps\Host\EnsureNetworkBridge` (neu)
|
||
|
||
Eingehängt zwischen `RebootIntoPveKernel` und `ConfigureProxmox`.
|
||
|
||
**Nach dem Neustart**, weil dort `ifupdown2` steht (`PrepareBaseSystem`
|
||
installiert es) und der Tunnel gerade bewiesen hat, dass er einen Neustart
|
||
überlebt.
|
||
|
||
**Vor `ConfigureProxmox`**, dessen `refuseWithoutBridge()` **unverändert stehen
|
||
bleibt** und damit zur Nachprüfung wird — dasselbe Paar wie `BuildVmTemplate` →
|
||
`VerifyVmTemplate`. Bauen und prüfen, und beides, nicht eines von beiden.
|
||
|
||
Ein eigener Schritt statt einer Erweiterung von `ConfigureProxmox`, aus drei
|
||
Gründen: er braucht `poll()` und eine eigene Frist, er läuft abgekoppelt, und
|
||
`ConfigureProxmox` würde seine Firewall-Arbeit sonst bei jedem Poll wiederholen.
|
||
Außerdem bekommt der Betreiber eine eigene Zeile in der Konsole, an der er
|
||
zusehen kann — und das ist das Abnahmeziel.
|
||
|
||
### Abgekoppelt ist hier Bedingung, nicht Optimierung
|
||
|
||
`BuildVmTemplate` läuft abgekoppelt, weil zwanzig Minuten länger sind als der
|
||
Befehlszeitablauf. Dieser Schritt läuft abgekoppelt, weil die SSH-Verbindung
|
||
**mitten im Befehl stirbt**: `ifreload -a` nimmt die Leitung, über die der Befehl
|
||
läuft. Ein synchroner Aufruf hätte keinen Rückgabewert, sondern eine Leiche.
|
||
|
||
---
|
||
|
||
## Der Ablauf
|
||
|
||
### Auf dem Host — `bridge-run.sh`
|
||
|
||
1. **sichern** — `tar czf` über `/etc/network/interfaces` und
|
||
`/etc/network/interfaces.d/`, dazu der Zustand der Fremdverwalter, die gleich
|
||
entmachtet werden.
|
||
2. **Zeitgeber stellen** — systemd-Timer, 5 Minuten, `OnActiveSec`.
|
||
**Vor jeder Änderung, ausnahmslos.** Ein `sleep &` täte es nicht: ein
|
||
Hintergrundlauf stirbt mit seiner Sitzung, und die Sitzung ist genau das, was
|
||
abreißt.
|
||
Das Rücknahme-Skript schreibt zusätzlich `failed` **und den Grund** in die
|
||
Statusdatei. Sonst liest der Schritt nach dem Wiederverbinden `running` und
|
||
pollt gegen einen Lauf, den es nicht mehr gibt — der Zeitgeber beendet den
|
||
Treiber ja gerade nicht.
|
||
3. **übernehmen** — cloud-inits Netzteil abschalten
|
||
(`/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg`),
|
||
`systemd-networkd`/NetworkManager maskieren, kollidierende
|
||
`interfaces.d`-Strophen wegräumen, die dieselbe Karte beanspruchen.
|
||
4. **umstellen** — Strophe nach `/etc/network/interfaces`, `ifreload -a`
|
||
(Rückfall: `systemctl restart networking`). **Hier stirbt SSH.**
|
||
5. **nachsehen — beide Richtungen.** Siehe eigenen Abschnitt unten.
|
||
6. `state=ok`, Ende. **Der Treiber bestellt nichts ab.**
|
||
|
||
### In CluPilot — `EnsureNetworkBridge`
|
||
|
||
**Erster Blick, vor allem anderen:** trägt `vmbr0` bereits die Standardroute und
|
||
eine Adresse, dann `advance()`, ohne irgendetwas anzufassen. Dieselbe Abkürzung
|
||
wie „Vorlage existiert und meldet `template: 1`". Sonst baut ein Wiederanlauf ein
|
||
funktionierendes Netz um — genau `pve-fns-1`.
|
||
|
||
**Guard: keine Brücke auf einer Brücke.** Ist die primäre Schnittstelle keine
|
||
physische Karte (`/sys/class/net/$if/device` fehlt, oder `…/bridge`, `…/bonding`
|
||
ist vorhanden), dann `fail()` mit Klartext statt bauen. `bridge_ports` auf einem
|
||
Bond oder einer bestehenden Bridge ist falsch.
|
||
|
||
**Erster Besuch** (kein `bridge_deadline` im Run-Kontext):
|
||
|
||
- Zustand lesen, Plan ableiten, Fremdverwalter erkennen.
|
||
- Unbekannter Verwalter → `fail()` mit der heutigen Handarbeit-Meldung. Kein
|
||
Versuch ins Blaue.
|
||
- `bridge.sh`, `bridge-run.sh` und `env` hochladen.
|
||
- `bridge_deadline` (15 min) und `bridge_attempts` in den Run-Kontext.
|
||
- `nohup setsid sh bridge-run.sh`, auf die PID-Datei warten.
|
||
- `poll(20)`.
|
||
|
||
**Jeder weitere Besuch:**
|
||
|
||
- `keyLogin()` **in einem try/catch**. Fliegt es und die Frist läuft noch →
|
||
`poll(20, 'warte darauf, dass der Host über die neue Brücke wieder antwortet')`.
|
||
Das ist der erwartete Fall, kein Fehler.
|
||
**Warum das die tragende Zeile ist:** `RunRunner.php:102` verwandelt jede
|
||
geworfene Ausnahme in ein `retry()`, und `retry` verbraucht das Versuchskonto.
|
||
Ungefangen brennt der Verbindungsabriss die fünf Versuche in wenigen Minuten
|
||
durch und lässt den Lauf scheitern, **bevor** der Host wieder da ist.
|
||
- Status lesen (`state`/`alive`/`phase`/`note`), gleiche Form wie
|
||
`BuildVmTemplate::readStatus()`.
|
||
|
||
| gelesen | Reaktion |
|
||
|---|---|
|
||
| `ok`, Brücke steht | abbestellen, dann `advance()` — **in dieser Reihenfolge** |
|
||
| `ok`, Brücke fehlt | der Zeitgeber war schneller → `giveUp()` |
|
||
| `running`, Prozess lebt | `poll(20)` |
|
||
| `running`, Prozess tot | Lauf ist tot, nicht langsam → auf die Rücknahme warten |
|
||
| `failed` | auf die Rücknahme warten, dann `giveUp()` mit `note` |
|
||
| leer | nie angelaufen → `giveUp()` |
|
||
| Frist überschritten | `giveUp()` |
|
||
|
||
**„Auf die Rücknahme warten" heißt konkret:** `poll(20)` weiter, bis entweder das
|
||
Rücknahme-Skript `failed` samt Grund geschrieben hat **oder** der Zeitgeber nicht
|
||
mehr existiert und der alte Zustand wieder auf der Karte liegt. Erst dann
|
||
`giveUp()`. Nicht sofort scheitern: solange die Rücknahme aussteht, ist die
|
||
Maschine mitten in einer Umstellung, und ein `fail()` hinterließe sie dort.
|
||
Gedeckelt ist das Warten durch die Schritt-Frist — und die ist das Dreifache des
|
||
Zeitgebers, das Warten kann also nicht unbegrenzt laufen.
|
||
|
||
`giveUp()` löscht `bridge_deadline` (damit ein Wiederanlauf wieder als erster
|
||
Besuch gilt), räumt die Statusdateien weg und beendet einen etwa noch laufenden
|
||
Treiber über seine Prozessgruppe — dieselbe Reihenfolge und dieselbe
|
||
`cmdline`-Absicherung gegen eine wiederverwendete PID wie in
|
||
`BuildVmTemplate::giveUp()`. `bridge_attempts` bleibt stehen.
|
||
|
||
**Fristen:** Zeitgeber 5 Minuten, Schritt-Frist 15 Minuten, `maxDuration()` 3600
|
||
(damit die eigene Frist die Fehlermeldung schreibt und nicht der generische
|
||
Zeitablauf, der zusätzlich einen Versuch kostet — dieselbe Form wie
|
||
`RebootIntoPveKernel` und `BuildVmTemplate`).
|
||
|
||
Die Frist ist **absichtlich das Dreifache** des Zeitgebers: wenn der Schritt
|
||
aufgibt, ist der alte Zustand garantiert längst zurückgespielt. Das ist die
|
||
Zeile, die „Wiederholung statt toter Server" wahr macht.
|
||
|
||
**Höchstens zwei automatische Versuche je Lauf** (`bridge_attempts`, von
|
||
`giveUp()` **nicht** gelöscht). Der zweite ist für den Fall, dass der Betreiber
|
||
zwischen den Versuchen etwas repariert hat; danach die Handarbeit-Meldung samt
|
||
der Strophe, die der Schritt geschrieben hätte.
|
||
|
||
---
|
||
|
||
## Die Erfolgsbedingung — beide Richtungen
|
||
|
||
Der Zeitgeber wird **erst** abbestellt, wenn beides stimmt:
|
||
|
||
```
|
||
# 1. komme ich raus? http_get "$CLUPILOT_PROBE_URL"
|
||
# 2. steht der Tunnel? date +%s gegen wg show wg0 latest-handshakes
|
||
```
|
||
|
||
Nur `1.` zu prüfen ist notwendig und **nicht hinreichend**. Der Fehlerfall:
|
||
`ifreload -a` bringt `vmbr0` sauber hoch, die Maschine erreicht das Internet, der
|
||
Treiber ist zufrieden und bestellt ab — aber `wg0` kommt nicht zurück. Ergebnis:
|
||
Der Host ist öffentlich erreichbar, CluPilot ist ausgesperrt, und die
|
||
Rückfahrkarte wurde gerade weggeworfen.
|
||
|
||
`wg0.conf` (`ConfigureWireguard::renderConfig()`) enthält **keine
|
||
Gerätebindung** — kein `PostUp` nennt die Karte. Der Tunnel hängt also nicht
|
||
wörtlich an `enp0s31f6`, sondern an der Quelladresse, die die Routing-Tabelle
|
||
hergibt — und genau das ist die Größe, die der Umbau anfasst. Das macht den
|
||
Fehlerfall nicht unwahrscheinlicher, nur unauffälliger: kein Fehler im
|
||
Protokoll, nur ein Handshake, der ausbleibt.
|
||
|
||
Prüfung `2.` ist **wortgleich** die aus v1.3.85 (`ConfigureWireguard`): frischer
|
||
Handshake (180 s) vom konfigurierten Hub, gestellt mit `date +%s` und
|
||
`wg show wg0 latest-handshakes`. Dieselbe Frage, dieselbe Antwort, lokal
|
||
stellbar — niemand muss von außen hereinkommen.
|
||
|
||
**Ist der Handshake schal: einmal `systemctl restart wg-quick@wg0`, dann noch
|
||
einmal nachsehen.** Das ist zu diesem Zeitpunkt gefahrlos, weil die SSH-Sitzung
|
||
ohnehin schon weg ist, und verwandelt einen hängenden Tunnel in einen laufenden
|
||
statt in eine Rücknahme. Bleibt er schal, `failed` — der Zeitgeber holt den alten
|
||
Zustand.
|
||
|
||
### Kein `ping`
|
||
|
||
`network.sh:190` und `:365` prüfen mit `ping -c2`. Das ist wortgleich die Falle,
|
||
die schon `ConfigureWireguard` erwischt hat: Hetzners `Debian-trixie-…-base` hat
|
||
kein `iputils-ping`, und `PrepareBaseSystem` installiert es nicht (es holt
|
||
`curl gnupg ifupdown2 chrony`). Die Nachsehen-Stufe sagte damit **immer** „nicht
|
||
erreichbar", der Zeitgeber spielte **immer** zurück — der Schritt käme nie durch.
|
||
|
||
`bridge.sh` prüft deshalb ohne `ping`:
|
||
|
||
1. Standardroute liegt auf `vmbr0`, mit dem erwarteten Gateway.
|
||
2. Die erwartete Adresse liegt auf `vmbr0`.
|
||
3. `http_get "$CLUPILOT_PROBE_URL"` (`curl`, Rückfall `wget` — beide vorhanden).
|
||
4. Frischer WireGuard-Handshake.
|
||
|
||
`iputils-ping` wird **nicht** nachinstalliert. Ein Paket nachzuziehen, um eine
|
||
schlechtere Prüfung benutzen zu können, ist rückwärts.
|
||
|
||
---
|
||
|
||
## Was aus dem laufenden Zustand abgeleitet wird
|
||
|
||
Alles auf dem Host, alles aus `ip`/`sys`:
|
||
|
||
| Größe | Quelle |
|
||
|---|---|
|
||
| primäre Karte | `ip -4 route show default` → `dev` |
|
||
| Adresse/Maske | `ip -4 -o addr show dev $if scope global`, erste |
|
||
| Gateway | `ip -4 route show default` → `via` |
|
||
| MAC | `/sys/class/net/$if/address` → `hwaddress ether`, festgenagelt |
|
||
| physisch? | `/sys/class/net/$if/device` da, `…/bridge`+`…/bonding` weg |
|
||
| DHCP? | Flag `dynamic` in `ip -4 -o addr show` (Datei-Grep nur als Zweitsignal) |
|
||
| Zusatzrouten | `ip -4 route show dev $if` minus Standard-, Subnetz- und Gateway-Link-Route |
|
||
|
||
**Anbieterform** (`detect_network_style`, bleibt wie sie ist):
|
||
|
||
- `dhcp` — Adresse kommt per DHCP.
|
||
- `routed` — Gateway außerhalb des eigenen Subnetzes (Hetzner dediziert, /32).
|
||
Braucht `pointopoint`, sonst findet der Kernel das Gateway nicht und die
|
||
Maschine ist still weg.
|
||
- `subnet` — gewöhnliches Subnetz, Gateway darin.
|
||
|
||
Ob das Gateway im eigenen Subnetz liegt, beantwortet `ip -4 route get` — mit
|
||
derselben Logik, die der Kernel später anwendet, statt Netzmasken nachzurechnen.
|
||
|
||
### IPv6
|
||
|
||
IPv6 trägt in dieser Installation keinen Verkehr; IPv4 genügt. Mitgenommen wird
|
||
es trotzdem — aber **nur in der einen Form, die nichts kostet**: liegt auf der
|
||
Karte eine **statische, globale** v6-Adresse und gibt es ein v6-Standardgateway,
|
||
wandern beide als `iface vmbr0 inet6 static` mit. Das deckt Hetzners
|
||
`2a01:…::2/64` samt `fe80::1` ab und ist derselbe Dreisatz wie bei v4.
|
||
|
||
**Nicht nachgebaut werden SLAAC und DHCPv6.** Proxmox setzt `forwarding=1`, und
|
||
der Kernel verwirft Router-Advertisements dann ohne `accept_ra=2` — das sauber
|
||
hinzubekommen wäre ein zweiter Satz Fehlerfälle innerhalb des Fensters, das die
|
||
ganze Konstruktion gerade zu überleben versucht, und er kaufte nichts. In dem
|
||
Fall wird v6 fallen gelassen **und eine Zeile ins Protokoll geschrieben**. Still
|
||
verlieren gilt nicht.
|
||
|
||
### Fremdverwalter
|
||
|
||
Erkannt und entmachtet werden drei: cloud-init (Netzteil), `systemd-networkd`,
|
||
NetworkManager. Alles, was der Schritt **nicht** erkennt, führt zur heutigen
|
||
Handarbeit-Meldung statt zu einem Versuch ins Blaue.
|
||
|
||
Der Grund, warum das überhaupt hineingehört: aus dem laufenden Zustand
|
||
abzuleiten löst das *Lesen*, nicht das *Schreiben*. Führt cloud-init das Netz,
|
||
ist `/etc/network/interfaces` nicht die Stelle, an der die Wahrheit steht — die
|
||
Brücke griffe entweder sofort nicht oder, schlimmer, sie griffe jetzt und würde
|
||
beim nächsten Neustart wieder eingesammelt. Dann läuft die Übernahme grün durch
|
||
und der Host verliert seine Brücke Monate später, mit Kunden darauf. Der
|
||
Zeitgeber fängt diesen Fall **nicht** ab.
|
||
|
||
Dazu der kleinere Verwandte: `build_bridge` behält heute die Zeile
|
||
`source /etc/network/interfaces.d/*`. Bleibt dort eine Strophe liegen, die
|
||
dieselbe Karte beansprucht, hat der Host zwei Stellen, die seine Adresse
|
||
vergeben.
|
||
|
||
---
|
||
|
||
## Was ausdrücklich nicht gebaut wird
|
||
|
||
- **Kein PHP-Nachbau** der Netzlogik. Der Schritt lädt und fährt.
|
||
- **Kein zweiter Rücknahmeweg.** Nur der Zeitgeber stellt zurück, nie der
|
||
Treiber selbst und nie CluPilot. Ein Treiber, der zurückspielt und das
|
||
Ergebnis über eine Verbindung meldet, die es vielleicht nicht gibt, rät über
|
||
seine eigene Erreichbarkeit. Ein Besitzer, an einer Stelle.
|
||
- **Kein `iputils-ping`.**
|
||
- **Keine Änderung an `ConfigureProxmox`.** `refuseWithoutBridge()` bleibt
|
||
wortgleich stehen und wird zur Nachprüfung.
|
||
- **Kein SLAAC/DHCPv6-Nachbau.**
|
||
- **Keine Reparatur des stillgelegten Rettungssystem-Wegs.** `network.sh:365`
|
||
benutzt weiterhin `ping` in `wireguard_handshake_proven`. Bekannt, außerhalb
|
||
dieses Schnitts, hier notiert.
|
||
|
||
---
|
||
|
||
## Tests
|
||
|
||
Nach der Testtechnik, die sich bei `BuildVmTemplate` gelohnt hat:
|
||
|
||
1. **Byte für Byte.** Ein Test vergleicht das hochgeladene `bridge.sh` gegen die
|
||
Repo-Datei. Dieselbe Zusicherung wie bei `template.sh`.
|
||
2. **`sh -n` über jeden Befehl, den der Schritt absetzt** (`FakeRemoteShell`
|
||
führt nichts aus), plus `sh -n` über `bridge.sh` und `bridge-run.sh` selbst.
|
||
Gegengeprobt mit absichtlich kaputtem Quoting.
|
||
3. **Der Strophengenerator, gegen echte `sh` gefahren.** `build_bridge` schreibt
|
||
nach `${CLUPILOT_INTERFACES_FILE:-/etc/network/interfaces}` und ruft
|
||
`${CLUPILOT_IFRELOAD:-ifreload}` — dieselbe Überschreibbarkeit wie
|
||
`CLUPILOT_STORAGE_CFG`. Damit prüft ein Pest-Test die erzeugte Strophe gegen
|
||
Aufzeichnungen von `ip addr`/`ip route` aus vier Fällen: Hetzner dediziert
|
||
(`routed`, /32, `pointopoint`), Hetzner Cloud (`dhcp`), netcup (`subnet`),
|
||
und statisches IPv6 mit `fe80::1`.
|
||
**Das ist der wertvollste Test des Vorhabens** — die Strophe ist das, was die
|
||
Maschine umbringt.
|
||
4. **Die Reihenfolge-Zusicherung.** Ein Test über die abgesetzte Befehlsfolge:
|
||
der Zeitgeber steht **vor** dem ersten verändernden Befehl.
|
||
5. **Beide Richtungen.** Ein Test, dass ein schaler Handshake trotz erreichbarem
|
||
Internet **nicht** abbestellt.
|
||
6. **Das Verhalten des Schritts:** Verbindungsabriss → `poll`, nicht `retry`
|
||
(die Zeile aus `RunRunner.php:102`); `ok` ohne Brücke → `giveUp`; `running`
|
||
ohne lebenden Prozess → `giveUp`; Frist > Zeitgeber; Abbestellen erst nach
|
||
der Nachprüfung; Brücke schon da → `advance()` ohne einen einzigen
|
||
verändernden Befehl.
|
||
|
||
---
|
||
|
||
## Abnahme
|
||
|
||
Frische Debian-13-Maschine, Host in `/admin/hosts` anlegen, zusehen, `active` —
|
||
ohne einen Handgriff. Und auf `pve-fns-1`, das die Brücke schon hat: der Schritt
|
||
meldet `advance()` und fasst nichts an.
|
||
|
||
---
|
||
|
||
Siehe `docs/superpowers/specs/2026-07-25-host-onboarding-design.md`,
|
||
`deploy/bootstrap/lib/template.sh`, `app/Provisioning/Steps/Host/BuildVmTemplate.php`.
|