Codex-Runde 2: der Gateway-Fix erreichte den Produktivpfad nicht
tests / pest (push) Failing after 9m29s Details
tests / assets (push) Successful in 22s Details
tests / release (push) Has been skipped Details

Zwei Befunde, beide echt, und der erste ist ein Patzer:
detect_network_style wurde auf default_gateway() umgestellt,
bridge-run.sh blieb auf awk '{ print  }'. Der Treiber reichte also
weiter den KARTENNAMEN als Gateway an build_bridge, und in der Strophe
stand 'gateway ens3' — der Fix half genau der Stelle nicht, fuer die er
gedacht war.

Meine Sandkiste konnte das nicht sehen: sie hatte immer ein via. Jetzt
ist sie parametrisiert, und ein Test faehrt bridge-run.sh mit einer
via-losen Standardroute durch und liest die geschriebene Strophe. Der
Beweis laeuft ueber den Produktivpfad, nicht ueber eine Einzelfunktion.

Zweitens: default_gateway suchte das erste via IRGENDWO in der Ausgabe,
detect_primary_interface das dev der ERSTEN Zeile. Bei zwei
Standardrouten baute das eine Bruecke ueber die Karte der einen mit dem
Gateway der anderen. Beide haengen jetzt an primary_default_route mit
head -1 — eine Route, eine Quelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
nexxo 2026-08-01 13:34:12 +02:00
parent 0bb61d39bd
commit 133b2cf552
3 changed files with 56 additions and 7 deletions

View File

@ -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."

View File

@ -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?

View File

@ -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", <<<SH
#!/bin/sh
if {$up}; then DEV=vmbr0; else DEV=enp0s31f6; fi
case "\$*" in
'-4 route show default') echo "default via 10.0.0.1 dev \$DEV" ;;
'-4 route show default') echo "{$route}" ;;
'-4 -o addr show dev enp0s31f6 scope global')
echo '2: enp0s31f6 inet 10.0.0.7/24 scope global enp0s31f6' ;;
'-4 -o addr show dev vmbr0 scope global')
@ -1028,6 +1031,40 @@ it('fährt den ganzen Ablauf durch und meldet ok', function () {
->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.