diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index 85a3153..783ab57 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -30,6 +30,7 @@ on: - "tests/uv-required-version.sh" - "tests/docker-lxcfs.sh" - "tests/java-memory.sh" + - "tests/pve-no-ip-facts.sh" - "tests/sudo-env.sh" - "tests/deb822-local-keyring.sh" - "tests/gpu-pci-class.sh" @@ -91,6 +92,7 @@ on: - "tests/uv-required-version.sh" - "tests/docker-lxcfs.sh" - "tests/java-memory.sh" + - "tests/pve-no-ip-facts.sh" - "tests/sudo-env.sh" - "tests/deb822-local-keyring.sh" - "tests/gpu-pci-class.sh" @@ -176,6 +178,9 @@ jobs: - name: Java memory drop-in tests run: bash tests/java-memory.sh + - name: No-IP diagnosis tests + run: bash tests/pve-no-ip-facts.sh + - name: sudo environment tests run: bash tests/sudo-env.sh diff --git a/pve/backend.func b/pve/backend.func index da4bd73..45473aa 100644 --- a/pve/backend.func +++ b/pve/backend.func @@ -144,6 +144,38 @@ _pct_create() { return $rc } +# ------------------------------------------------------------------------------ +# _pve_no_ip_facts +# +# What the host can see about a container that got no address. It goes to the +# log, where "No IP assigned" used to be all a quarter of the network failures +# said. +# ------------------------------------------------------------------------------ +_pve_no_ip_facts() { + local ctid="$1" brg="$2" net0 ip4 tag link port uplinks=0 + net0="$(pct config "$ctid" 2>/dev/null | sed -n 's/^net0: //p')" + ip4="$(grep -oE '(^|,)ip=[^,]+' <<<"$net0" | cut -d= -f2 || true)" + tag="$(grep -oE '(^|,)tag=[^,]+' <<<"$net0" | cut -d= -f2 || true)" + link="$(pct exec "$ctid" -- ip -br link show dev eth0 2>/dev/null | awk '{print $2}' || true)" + msg_warn "Container network: ip=${ip4:-none}${tag:+, VLAN ${tag}}, eth0 ${link:-missing}" + + if [[ ! -d "/sys/class/net/${brg}" ]]; then + msg_warn "Bridge ${brg} does not exist on this host" + elif [[ -d "/sys/class/net/${brg}/brif" ]]; then + for port in "/sys/class/net/${brg}/brif"/*; do + [[ -e "$port" ]] || continue + case "${port##*/}" in + veth* | tap* | fwpr* | fwln*) ;; + *) uplinks=$((uplinks + 1)) ;; + esac + done + ((uplinks)) || msg_warn "Bridge ${brg} has no uplink port, so nothing outside this host can answer on it" + fi + if [[ "$ip4" == "dhcp" ]]; then + msg_warn "No DHCP server answered on ${brg}${tag:+ (VLAN ${tag})}" + fi +} + # ------------------------------------------------------------------------------ # _pct_start_failed [pct start output] # @@ -1128,6 +1160,7 @@ EOF if [ -z "$ip_in_lxc" ]; then msg_error "No IP assigned to CT $CTID after 60 attempts" + _pve_no_ip_facts "$CTID" "$BRG" msg_custom "🔧" "${YW}" "Troubleshooting:" echo " • Verify bridge ${BRG} exists and has connectivity" echo " • Check if DHCP server is reachable (if using DHCP)" diff --git a/tests/pve-no-ip-facts.sh b/tests/pve-no-ip-facts.sh new file mode 100644 index 0000000..0bdf5f8 --- /dev/null +++ b/tests/pve-no-ip-facts.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +passed=0 failed=0 + +eval "$(awk '/^_pve_no_ip_facts[(][)] [{]/,/^}/' "$CORE_DIR/pve/backend.func" | + sed "s#/sys/class/net#$TEST_DIR/net#g")" + +check() { + if [[ "$3" == "$2" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +msg_warn() { echo "$1"; } +NET0="" LINK="UP" +pct() { + case "$1" in + config) echo "arch: amd64" && echo "net0: $NET0" ;; + exec) [[ -n "$LINK" ]] && echo "eth0@if12 $LINK aa:bb:cc:dd:ee:ff " ;; + esac +} +bridge() { + rm -rf "$TEST_DIR/net" + [[ "$1" == "none" ]] && return 0 + mkdir -p "$TEST_DIR/net/vmbr0" + [[ "$1" == "ovs" ]] && return 0 + mkdir -p "$TEST_DIR/net/vmbr0/brif" + local p + for p in "${@:2}"; do : >"$TEST_DIR/net/vmbr0/brif/$p"; done +} + +NET0="name=eth0,bridge=vmbr0,hwaddr=BC:24:11:00:00:01,ip=dhcp,tag=20,type=veth" +bridge linux veth101i0 fwpr101p0 +out="$(_pve_no_ip_facts 101 vmbr0)" +check "DHCP on a bridge without an uplink" \ + "Container network: ip=dhcp, VLAN 20, eth0 UP|Bridge vmbr0 has no uplink port, so nothing outside this host can answer on it|No DHCP server answered on vmbr0 (VLAN 20)" \ + "$(paste -sd'|' <<<"$out")" +check "the MAC address stays out of the log" 0 "$(grep -ci 'BC:24' <<<"$out" || true)" + +bridge linux veth101i0 enp3s0 +check "DHCP with an uplink blames the DHCP server only" \ + "Container network: ip=dhcp, VLAN 20, eth0 UP|No DHCP server answered on vmbr0 (VLAN 20)" \ + "$(_pve_no_ip_facts 101 vmbr0 | paste -sd'|')" + +NET0="name=eth0,bridge=vmbr0,ip=192.168.1.50/24,gw=192.168.1.1,type=veth" +LINK="" +check "a static address with eth0 missing" "Container network: ip=192.168.1.50/24, eth0 missing" \ + "$(_pve_no_ip_facts 101 vmbr0 | paste -sd'|')" + +LINK="DOWN" +bridge none +check "a bridge that is not there" "Container network: ip=192.168.1.50/24, eth0 DOWN|Bridge vmbr0 does not exist on this host" \ + "$(_pve_no_ip_facts 101 vmbr0 | paste -sd'|')" + +bridge ovs +check "an Open vSwitch bridge has no brif and is not judged" "Container network: ip=192.168.1.50/24, eth0 DOWN" \ + "$(_pve_no_ip_facts 101 vmbr0 | paste -sd'|')" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0))