host($run); $this->keyLogin($this->shell, $host); $state = $this->readBridgeState(); // Already there means touch nothing. The same shortcut as "the template // exists and reports template: 1" — and here it matters more, because // rebuilding a working network is the one thing that can end this badly. if ($state['up']) { return StepResult::advance(); } if ($state['iface'] === '') { return StepResult::fail( 'No interface on this host carries the default route, so there is nothing to derive a bridge '. 'from. Check the machine on the provider console.' ); } // bridge_ports on a bond or an existing bridge is wrong: the bridge // would take its own substrate as a port, and what comes out of that // cannot be repaired from a remote shell. if (! $state['physical']) { return StepResult::fail( 'The interface carrying the default route ('.$state['iface'].') is not a physical NIC — it is a '. 'bridge, a bond or a VLAN. CluPilot will not put a bridge on top of it. Build vmbr0 by hand in '. '/etc/network/interfaces (bridge_ports = the NIC carrying the default route, host address and '. 'gateway moved onto vmbr0), apply it with `ifreload -a`, confirm you still have SSH, then retry. '. 'Current default route: '.($state['route'] ?: '(none reported)').'.' ); } // Start, poll and cancel arrive in the next task. Harmless until then: // the step is not in the pipeline yet, so no run can reach this line. return StepResult::fail('not implemented yet'); } /** * What the host says about its bridge and its primary NIC, in one round trip. * * Derived from the RUNNING state — `ip`, `/sys/class/net` — never from the * provider's file. What runs is structured the same everywhere; how it was * written down is not, and that is the part that carries Hetzner and netcup * at once. * * "up" is deliberately all three facts and not `ip link show`: a vmbr0 with * no address and no default route is a bridge in the sense of `ip link` and * nothing else. The step this replaces checked exactly that and threw the * answer away. * * @return array{up:bool, iface:string, physical:bool, route:string} */ private function readBridgeState(): array { $out = $this->shell->run(implode("\n", [ ': clupilot-bridge-state', 'B=vmbr0', 'IF=$(ip -4 route show default 2>/dev/null | awk \'{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i+1); exit } }\')', 'UP=no', 'if ip link show "$B" >/dev/null 2>&1 && [ "$IF" = "$B" ] && [ -n "$(ip -4 -o addr show dev "$B" scope global 2>/dev/null)" ]; then UP=yes; fi', 'PHY=no', 'if [ -n "$IF" ] && [ -e "/sys/class/net/$IF/device" ] && [ ! -d "/sys/class/net/$IF/bridge" ] && [ ! -d "/sys/class/net/$IF/bonding" ]; then PHY=yes; fi', "printf 'up=%s\\n' \"\$UP\"", "printf 'iface=%s\\n' \"\$IF\"", "printf 'physical=%s\\n' \"\$PHY\"", "printf 'route=%s\\n' \"\$(ip -4 route show default 2>/dev/null | head -1)\"", ]))->stdout; $parsed = ['up' => 'no', 'iface' => '', 'physical' => 'no', 'route' => '']; foreach (preg_split('/\R/', $out) ?: [] as $line) { [$key, $value] = array_pad(explode('=', $line, 2), 2, ''); if (array_key_exists($key, $parsed)) { $parsed[$key] = trim($value); } } return [ 'up' => $parsed['up'] === 'yes', 'iface' => $parsed['iface'], 'physical' => $parsed['physical'] === 'yes', 'route' => $parsed['route'], ]; } }