diff --git a/deploy/bootstrap/lib/bridge-run.sh b/deploy/bootstrap/lib/bridge-run.sh index d99227a..a99a46e 100644 --- a/deploy/bootstrap/lib/bridge-run.sh +++ b/deploy/bootstrap/lib/bridge-run.sh @@ -108,7 +108,10 @@ interface_is_physical "$_iface" \ _style="$(detect_network_style "$_iface")" _cidr="$("$CLUPILOT_IP" -4 -o addr show dev "$_iface" scope global 2>/dev/null | awk '{ print $4; exit }')" -_gw="$("$CLUPILOT_IP" -4 route show default 2>/dev/null | awk '{ print $3; exit }')" +# Über den Helfer, nicht mit `awk '{ print $3 }'`: bei `default dev ens3 scope +# link` steht dort der KARTENNAME, und der landete als `gateway ens3` in der +# Strophe. Beides — Karte und Gateway — kommt aus derselben Route. +_gw="$(default_gateway)" [ "$_style" = 'dhcp' ] || [ -n "$_cidr" ] \ || fail "Keine globale IPv4-Adresse auf ${_iface} — nichts, was auf eine Bruecke ziehen koennte." diff --git a/deploy/bootstrap/lib/bridge.sh b/deploy/bootstrap/lib/bridge.sh index f35db54..8bc006b 100644 --- a/deploy/bootstrap/lib/bridge.sh +++ b/deploy/bootstrap/lib/bridge.sh @@ -82,19 +82,28 @@ fi # Die Schnittstelle, über die die Vorgabe-Route geht. Nicht „die erste, die # nicht lo heißt": eine Maschine mit zwei Karten hat oft eine angeschlossene und # eine nicht. +# Die EINE Standardroute, aus der alles andere abgeleitet wird. +# +# `head -1`, und alle drei Leser hängen daran. Ein Host kann mehrere haben, und +# wer Karte und Gateway aus verschiedenen Zeilen zieht, baut eine Brücke über die +# Karte der einen mit dem Gateway der anderen — ein Netz, das nirgends hinführt. +primary_default_route() { + "$CLUPILOT_IP" -4 route show default 2>/dev/null | head -1 +} + detect_primary_interface() { - "$CLUPILOT_IP" -4 route show default 2>/dev/null \ + primary_default_route \ | awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i+1); exit } }' } -# Das Gateway der Standardroute — hinter dem `via`, nie an fester Feldnummer. +# Das Gateway dieser Route — hinter dem `via`, nie an fester Feldnummer. # # `default dev ens3 scope link` hat keins. Ein `awk '{ print $3 }'` lieferte dort # den KARTENNAMEN, und daraus würde `gateway ens3` in der Strophe: `ifreload` # scheitert, der Zeitgeber räumt ab — und abgeräumt wird ein Host, der zu bauen # gewesen wäre. Leer heißt hier ehrlich leer. default_gateway() { - "$CLUPILOT_IP" -4 route show default 2>/dev/null \ + primary_default_route \ | awk '{ for (i = 1; i < NF; i++) if ($i == "via") { print $(i+1); exit } }' } @@ -102,7 +111,7 @@ default_gateway() { # die muss die Brücke als eigene `up`-Zeile mitnehmen, sonst verliert der Host # seinen Weg nach draußen. has_default_route() { - "$CLUPILOT_IP" -4 route show default 2>/dev/null | grep -q . + primary_default_route | grep -q . } # Ist das eine physische Karte? diff --git a/tests/Feature/Provisioning/BridgeScriptTest.php b/tests/Feature/Provisioning/BridgeScriptTest.php index 81b749b..8b7326d 100644 --- a/tests/Feature/Provisioning/BridgeScriptTest.php +++ b/tests/Feature/Provisioning/BridgeScriptTest.php @@ -939,7 +939,7 @@ it('entmachtet cloud-init im gesicherten Stand', function () { * * @return array{work:string, calls:string} */ -function bridgeSandbox(string $dir, bool $bridgeComesUp = true, int $handshakeAge = 5): array +function bridgeSandbox(string $dir, bool $bridgeComesUp = true, int $handshakeAge = 5, bool $withGateway = true): array { $work = "{$dir}/work"; $bin = "{$dir}/bin"; @@ -952,12 +952,15 @@ function bridgeSandbox(string $dir, bool $bridgeComesUp = true, int $handshakeAg file_put_contents("{$dir}/interfaces", "# die Fassung von vorher\n"); $up = $bridgeComesUp ? "[ -f '{$state}/bridged' ]" : 'false'; + // Eine Standardroute ohne `via` ist eine Link-Route. Sie hat kein Gateway, + // und ein `awk '{ print $3 }'` läse dort den KARTENNAMEN. + $route = $withGateway ? 'default via 10.0.0.1 dev $DEV' : 'default dev $DEV scope link'; file_put_contents("{$bin}/ip", <<and(file_get_contents($box['calls']))->not->toContain('stop clupilot-network-rollback.timer'); }); +it('schreibt kein Gateway in die Strophe, wenn die Standardroute keins hat', function () { + // Der Beweis über den PRODUKTIVPFAD, nicht über eine Einzelfunktion. + // `detect_network_style` auf `default_gateway()` umzustellen half nichts, + // solange bridge-run.sh sein Gateway weiter mit `awk '{ print $3 }'` las: + // der Treiber reichte `ens3` an build_bridge weiter, und in der Strophe + // stand `gateway ens3`. Ein Test an der Bibliothek allein hätte das nie + // gesehen. + $box = bridgeSandbox($this->dir, withGateway: false); + + Process::path($box['work']) + ->env(['CLUPILOT_WORK_DIR' => $box['work']]) + ->run('sh '.base_path('deploy/bootstrap/lib/bridge-run.sh')); + + $stanza = (string) file_get_contents($this->dir.'/interfaces'); + + expect($stanza)->toContain('address 10.0.0.7/24') + ->not->toContain('gateway enp0s31f6') + ->not->toContain('pointopoint') + // Die Link-Route geht sonst beim Umbau verloren. + ->toContain('up ip route add default dev vmbr0'); +}); + +it('nimmt Karte und Gateway aus DERSELBEN Standardroute', function () { + // Bei zwei Standardrouten nahm `detect_primary_interface` das `dev` der + // ersten und `default_gateway` das `via` der zweiten — die Brücke bekäme + // die Karte der einen mit dem Gateway der anderen. + $ip = fakeIp($this->dir, [ + '-4 route show default' => "default dev ens3 scope link\ndefault via 10.0.0.1 dev ens4 metric 200", + ]); + + expect(runBridgeSh('detect_primary_interface', ['CLUPILOT_IP' => $ip]))->toBe('ens3') + ->and(runBridgeSh('default_gateway', ['CLUPILOT_IP' => $ip]))->toBe(''); +}); + it('meldet failed und lässt den Zeitgeber stehen, wenn die Brücke nicht trägt', function () { // Der Fall, für den die ganze Konstruktion gebaut ist. Der Treiber räumt // NICHT selbst auf — er überlässt es dem Zeitgeber und sagt nur, warum.