Entwurf: vmbr0 automatisch bauen
Der letzte Handgriff der Host-Uebernahme. Derselbe Mechanismus wie BuildVmTemplate — bridge.sh + bridge-run.sh hochladen und abgekoppelt fahren, nicht in PHP nachbauen. Die tragenden Punkte: - Abgekoppelt ist Bedingung, nicht Optimierung: ifreload -a nimmt die Leitung, ueber die der Befehl laeuft. - Der Zeitgeber steht vor jeder Aenderung, und nur er stellt zurueck. Kein zweiter Ruecknahmeweg. - Abbestellt wird erst, wenn BEIDE Richtungen stimmen: Internet erreichbar UND frischer WireGuard-Handshake. Nur die erste zu pruefen laesst den Fall zu, in dem der Host oeffentlich lebt, CluPilot ausgesperrt ist und die Rueckfahrkarte gerade weggeworfen wurde. - Kein ping — Hetzners Debian-Basis hat keins, und network.sh:190 haette deshalb immer 'nicht erreichbar' gesagt. - Bruecke schon da heisst nichts anfassen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>main
parent
dc35e5310f
commit
5cae870dd9
|
|
@ -0,0 +1,397 @@
|
|||
# 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`.
|
||||
Loading…
Reference in New Issue