Last active
March 19, 2026 10:14
-
-
Save sarkrui/63a1ba3dfb41009d5eb6d7e2b96a5875 to your computer and use it in GitHub Desktop.
PVE network config auto-bind auto-net
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| AUTONET_URL="https://gist.githubusercontent.com/sarkrui/63a1ba3dfb41009d5eb6d7e2b96a5875/raw/pve-autonet" | |
| SERVICE_URL="https://gist.githubusercontent.com/sarkrui/63a1ba3dfb41009d5eb6d7e2b96a5875/raw/pve-autonet.service" | |
| AUTONET_DST="/usr/local/sbin/pve-autonet" | |
| SERVICE_DST="/etc/systemd/system/pve-autonet.service" | |
| DEFAULTS_DST="/etc/default/pve-autonet" | |
| INTERFACES_FILE="/etc/network/interfaces" | |
| BACKUP_SUFFIX="$(date +%Y%m%d-%H%M%S)" | |
| if [ "${EUID:-$(id -u)}" -ne 0 ]; then | |
| echo "Run installer as root." >&2 | |
| exit 1 | |
| fi | |
| need_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || { | |
| echo "Missing required command: $1" >&2 | |
| exit 1 | |
| } | |
| } | |
| fetch() { | |
| local url="$1" | |
| local dst="$2" | |
| if command -v curl >/dev/null 2>&1; then | |
| curl -fsSL "$url" -o "$dst" | |
| elif command -v wget >/dev/null 2>&1; then | |
| wget -qO "$dst" "$url" | |
| else | |
| echo "Need curl or wget to download files." >&2 | |
| exit 1 | |
| fi | |
| } | |
| echo "[1/7] Checking prerequisites..." | |
| need_cmd systemctl | |
| need_cmd install | |
| need_cmd ifreload | |
| TMPDIR="$(mktemp -d)" | |
| trap 'rm -rf "$TMPDIR"' EXIT | |
| echo "[2/7] Downloading pve-autonet files from gist..." | |
| fetch "$AUTONET_URL" "$TMPDIR/pve-autonet" | |
| fetch "$SERVICE_URL" "$TMPDIR/pve-autonet.service" | |
| echo "[3/7] Installing script and service..." | |
| install -m 0755 "$TMPDIR/pve-autonet" "$AUTONET_DST" | |
| install -m 0644 "$TMPDIR/pve-autonet.service" "$SERVICE_DST" | |
| echo "[4/7] Installing defaults file (if absent)..." | |
| if [ ! -f "$DEFAULTS_DST" ]; then | |
| cat > "$DEFAULTS_DST" <<'EOF' | |
| # Optional fixed NIC (for example: enp3s0). Leave empty for auto-selection. | |
| PVE_AUTONET_NIC="" | |
| # Bridge name created/managed by pve-autonet. | |
| PVE_AUTONET_BRIDGE_NAME="vmbr0" | |
| # Allowed values: dhcp or static | |
| PVE_AUTONET_BRIDGE_METHOD="dhcp" | |
| # Used only when BRIDGE_METHOD=static | |
| PVE_AUTONET_BRIDGE_ADDRESS="" | |
| PVE_AUTONET_BRIDGE_GATEWAY="" | |
| # Seconds to wait for bridge IPv4 before skipping /etc/hosts update. | |
| PVE_AUTONET_WAIT_SECONDS="20" | |
| # Bark notifications for important auto-net stages and online status. | |
| PVE_AUTONET_BARK_ENABLED="1" | |
| PVE_AUTONET_BARK_URL="https://bark.ws/WNA2EuVCumFkyG7BJuua8L" | |
| PVE_AUTONET_BARK_TITLE="PVE9 Info" | |
| PVE_AUTONET_BARK_CURL_TIMEOUT="5" | |
| EOF | |
| chmod 0644 "$DEFAULTS_DST" | |
| fi | |
| echo "[5/7] Backing up existing network config..." | |
| if [ -f "$INTERFACES_FILE" ]; then | |
| cp -a "$INTERFACES_FILE" "${INTERFACES_FILE}.bak.${BACKUP_SUFFIX}" | |
| fi | |
| echo "[6/7] Reloading systemd, enabling and starting service..." | |
| systemctl daemon-reload | |
| systemctl enable pve-autonet.service | |
| systemctl restart pve-autonet.service | |
| echo "[7/7] Done." | |
| echo | |
| echo "Installed:" | |
| echo " - $AUTONET_DST" | |
| echo " - $SERVICE_DST" | |
| echo " - $DEFAULTS_DST" | |
| echo | |
| echo "Backup:" | |
| echo " - ${INTERFACES_FILE}.bak.${BACKUP_SUFFIX}" | |
| echo | |
| echo "The script will auto-generate /etc/network/interfaces with the" | |
| echo "detected NIC and bridge on the next run (or reboot). Once PVE" | |
| echo "has a bridge, the script skips generation and only refreshes" | |
| echo "/etc/hosts and sends the online notification." | |
| echo | |
| echo "Next steps:" | |
| echo " 1) Review $DEFAULTS_DST if you need static IP or pinned NIC." | |
| echo " 2) Reboot once to validate early-boot behavior on this host." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -uo pipefail | |
| readonly INTERFACES_FILE="/etc/network/interfaces" | |
| readonly STATE_DIR="/var/lib/pve-autonet" | |
| readonly STATE_UPLINK_FILE="${STATE_DIR}/last-uplink" | |
| readonly LOCK_FILE="/run/lock/pve-autonet.lock" | |
| readonly DEFAULTS_FILE="/etc/default/pve-autonet" | |
| PVE_AUTONET_NIC="${PVE_AUTONET_NIC:-}" | |
| PVE_AUTONET_BRIDGE_NAME="${PVE_AUTONET_BRIDGE_NAME:-vmbr0}" | |
| PVE_AUTONET_BRIDGE_METHOD="${PVE_AUTONET_BRIDGE_METHOD:-dhcp}" | |
| PVE_AUTONET_BRIDGE_ADDRESS="${PVE_AUTONET_BRIDGE_ADDRESS:-}" | |
| PVE_AUTONET_BRIDGE_GATEWAY="${PVE_AUTONET_BRIDGE_GATEWAY:-}" | |
| PVE_AUTONET_WAIT_SECONDS="${PVE_AUTONET_WAIT_SECONDS:-20}" | |
| PVE_AUTONET_BARK_ENABLED="${PVE_AUTONET_BARK_ENABLED:-1}" | |
| PVE_AUTONET_BARK_URL="${PVE_AUTONET_BARK_URL:-https://bark.ws/WNA2EuVCumFkyG7BJuua8L}" | |
| PVE_AUTONET_BARK_TITLE="${PVE_AUTONET_BARK_TITLE:-PVE9 Info}" | |
| PVE_AUTONET_BARK_CURL_TIMEOUT="${PVE_AUTONET_BARK_CURL_TIMEOUT:-5}" | |
| PVE_AUTONET_FAILURE_NOTIFIED=0 | |
| # ── Colors (disabled when stderr is not a terminal or via journal) ─ | |
| if [ -t 2 ]; then | |
| C_RESET='\033[0m' | |
| C_BOLD='\033[1m' | |
| C_DIM='\033[2m' | |
| C_GREEN='\033[1;32m' | |
| C_YELLOW='\033[1;33m' | |
| C_RED='\033[1;31m' | |
| C_CYAN='\033[1;36m' | |
| else | |
| C_RESET='' C_BOLD='' C_DIM='' C_GREEN='' C_YELLOW='' C_RED='' C_CYAN='' | |
| fi | |
| # ── Logging ────────────────────────────────────────────────────── | |
| log() { echo -e "${C_DIM}[pve-autonet]${C_RESET} $*" >&2; } | |
| log_ok() { echo -e "${C_GREEN}[pve-autonet] ✓${C_RESET} ${C_BOLD}$*${C_RESET}" >&2; } | |
| log_key() { echo -e "${C_CYAN}[pve-autonet]${C_RESET} ${C_BOLD}$*${C_RESET}" >&2; } | |
| warn() { echo -e "${C_YELLOW}[pve-autonet] WARN:${C_RESET} $*" >&2; } | |
| fail() { | |
| notify_failure "$*" | |
| echo -e "${C_RED}[pve-autonet] ERROR:${C_RESET} ${C_BOLD}$*${C_RESET}" >&2 | |
| exit 1 | |
| } | |
| host_short() { | |
| hostname -s 2>/dev/null || hostname 2>/dev/null || echo "pve-host" | |
| } | |
| # ── Bark notifications ─────────────────────────────────────────── | |
| urlencode() { | |
| local input="$1" output="" char i | |
| local LC_ALL=C | |
| for ((i = 0; i < ${#input}; i++)); do | |
| char="${input:i:1}" | |
| case "$char" in | |
| [a-zA-Z0-9.~_-]) output+="$char" ;; | |
| ' ') output+="%20" ;; | |
| *) printf -v output '%s%%%02X' "$output" "'$char" ;; | |
| esac | |
| done | |
| printf '%s' "$output" | |
| } | |
| send_bark() { | |
| local message="$1" | |
| [ "$PVE_AUTONET_BARK_ENABLED" = "1" ] || return 0 | |
| [ -n "$PVE_AUTONET_BARK_URL" ] || return 0 | |
| command -v curl >/dev/null 2>&1 || return 0 | |
| curl -fsS --max-time "$PVE_AUTONET_BARK_CURL_TIMEOUT" \ | |
| "${PVE_AUTONET_BARK_URL%/}/$(urlencode "$PVE_AUTONET_BARK_TITLE")/$(urlencode "$message")" \ | |
| >/dev/null 2>&1 || true | |
| } | |
| notify_stage() { | |
| log_key "$1" | |
| send_bark "$1" | |
| } | |
| notify_ok() { | |
| log_ok "$1" | |
| send_bark "$1" | |
| } | |
| notify_failure() { | |
| local message="$1" | |
| [ "$PVE_AUTONET_FAILURE_NOTIFIED" = "1" ] && return 0 | |
| PVE_AUTONET_FAILURE_NOTIFIED=1 | |
| send_bark "FAILED on $(host_short): ${message}" | |
| } | |
| notify_online() { | |
| local ip="$1" | |
| log_ok "Host IP Address is: https://${ip}:8006" | |
| send_bark "Host IP Address is: https://${ip}:8006" | |
| } | |
| # ── Hardware helpers ───────────────────────────────────────────── | |
| read_int_file() { | |
| local path="$1" value | |
| [ -r "$path" ] || { echo "0"; return; } | |
| value="$(<"$path")" | |
| case "$value" in ''|*[!0-9]*) echo "0" ;; *) echo "$value" ;; esac | |
| } | |
| is_physical_nic() { | |
| local dev="$1" | |
| [ -d "/sys/class/net/${dev}/device" ] || return 1 | |
| case "$dev" in | |
| lo|vmbr*|bond*|tap*|fw*|veth*|dummy*|ifb*|sit*|gre*|gretap*|erspan*|ip6*|tun*|wg*) return 1 ;; | |
| esac | |
| return 0 | |
| } | |
| phys_nics() { | |
| local p d | |
| for p in /sys/class/net/*; do | |
| d="${p##*/}" | |
| is_physical_nic "$d" || continue | |
| echo "$d" | |
| done | |
| } | |
| route_uplink_nic() { | |
| local dev | |
| dev="$(ip -4 route show default 2>/dev/null | awk 'NR==1 {for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1);exit}}')" | |
| [ -n "$dev" ] && is_physical_nic "$dev" && echo "$dev" | |
| } | |
| pick_best_physical_nic() { | |
| local best="" best_score=-1 best_carrier=0 best_speed=0 | |
| local dev carrier speed score | |
| while IFS= read -r dev; do | |
| [ -n "$dev" ] || continue | |
| carrier="$(read_int_file "/sys/class/net/${dev}/carrier")" | |
| speed="$(read_int_file "/sys/class/net/${dev}/speed")" | |
| score=$((carrier * 100000 + speed)) | |
| if [ "$score" -gt "$best_score" ] || { [ "$score" -eq "$best_score" ] && [ "$dev" \< "$best" ]; }; then | |
| best="$dev"; best_score="$score"; best_carrier="$carrier"; best_speed="$speed" | |
| fi | |
| done < <(phys_nics) | |
| [ -n "$best" ] || return 1 | |
| log_key "Selected NIC ${best} (carrier=${best_carrier}, speed=${best_speed}Mb/s)" | |
| echo "$best" | |
| } | |
| select_uplink_nic() { | |
| if [ -n "$PVE_AUTONET_NIC" ]; then | |
| is_physical_nic "$PVE_AUTONET_NIC" || fail "Configured PVE_AUTONET_NIC=${PVE_AUTONET_NIC} is not a physical NIC" | |
| echo "$PVE_AUTONET_NIC"; return 0 | |
| fi | |
| local route_dev | |
| route_dev="$(route_uplink_nic || true)" | |
| if [ -n "$route_dev" ]; then | |
| log_key "Using default-route NIC ${route_dev}" | |
| echo "$route_dev"; return 0 | |
| fi | |
| pick_best_physical_nic | |
| } | |
| # ── Network diagnostics ───────────────────────────────────────── | |
| bridge_exists_in_config() { | |
| grep -qE "^iface[[:space:]]+${PVE_AUTONET_BRIDGE_NAME}[[:space:]]" "$INTERFACES_FILE" 2>/dev/null | |
| } | |
| bridge_port_from_config() { | |
| awk "/^iface[[:space:]]+${PVE_AUTONET_BRIDGE_NAME}[[:space:]]/,/^(auto|iface|source|$)/" \ | |
| "$INTERFACES_FILE" 2>/dev/null \ | |
| | awk '/bridge-ports/ {print $2; exit}' | |
| } | |
| bridge_is_up() { | |
| [ -d "/sys/class/net/${PVE_AUTONET_BRIDGE_NAME}" ] | |
| } | |
| bridge_has_ip() { | |
| ip -4 -o addr show dev "${PVE_AUTONET_BRIDGE_NAME}" scope global 2>/dev/null | grep -q . | |
| } | |
| bridge_port_nic_exists() { | |
| local port="$1" | |
| [ -n "$port" ] && [ -d "/sys/class/net/${port}" ] | |
| } | |
| gateway_reachable() { | |
| local gw | |
| gw="$(ip -4 route show default dev "${PVE_AUTONET_BRIDGE_NAME}" 2>/dev/null \ | |
| | awk 'NR==1 {for(i=1;i<=NF;i++) if($i=="via"){print $(i+1);exit}}')" | |
| [ -n "$gw" ] && ping -c1 -W2 -I "${PVE_AUTONET_BRIDGE_NAME}" "$gw" >/dev/null 2>&1 | |
| } | |
| get_bridge_ip() { | |
| ip -4 -o addr show dev "${PVE_AUTONET_BRIDGE_NAME}" scope global 2>/dev/null \ | |
| | awk 'NR==1 {print $4}' | cut -d/ -f1 || true | |
| } | |
| diagnose_network() { | |
| local port reason="" | |
| if ! bridge_exists_in_config; then | |
| reason="no bridge in config" | |
| else | |
| port="$(bridge_port_from_config)" | |
| if [ -z "$port" ]; then | |
| reason="bridge has no port defined" | |
| elif ! bridge_port_nic_exists "$port"; then | |
| reason="bridge port ${port} does not exist (hardware changed?)" | |
| elif ! bridge_is_up; then | |
| reason="bridge device not up" | |
| fi | |
| fi | |
| if [ -n "$reason" ]; then | |
| echo "$reason" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| wait_for_ip() { | |
| local wait="$1" i ip="" | |
| for ((i = 0; i < wait; i++)); do | |
| ip="$(get_bridge_ip)" | |
| [ -n "$ip" ] && { echo "$ip"; return 0; } | |
| sleep 1 | |
| done | |
| return 1 | |
| } | |
| # ── Config generation ──────────────────────────────────────────── | |
| render_interfaces() { | |
| local uplink="$1" | |
| local bridge="${PVE_AUTONET_BRIDGE_NAME}" | |
| local method="${PVE_AUTONET_BRIDGE_METHOD}" | |
| local all_nics nic | |
| all_nics="$(phys_nics)" | |
| cat <<EOF | |
| # network interface settings; autogenerated | |
| # Please do NOT modify this file directly, unless you know what | |
| # you're doing. | |
| # | |
| # If you want to manage parts of the network configuration manually, | |
| # please utilize the 'source' or 'source-directory' directives to do | |
| # so. | |
| # PVE will preserve these directives, but will NOT read its network | |
| # configuration from sourced files, so do not attempt to move any of | |
| # the PVE managed interfaces into external files! | |
| auto lo | |
| iface lo inet loopback | |
| EOF | |
| while IFS= read -r nic; do | |
| [ -n "$nic" ] || continue | |
| echo "iface ${nic} inet manual" | |
| echo "" | |
| done <<< "$all_nics" | |
| cat <<EOF | |
| auto ${bridge} | |
| iface ${bridge} inet ${method} | |
| bridge-ports ${uplink} | |
| bridge-stp off | |
| bridge-fd 0 | |
| bridge-vlan-aware yes | |
| bridge-vids 2-4094 | |
| EOF | |
| if [ "$method" = "static" ]; then | |
| [ -n "$PVE_AUTONET_BRIDGE_ADDRESS" ] || fail "PVE_AUTONET_BRIDGE_ADDRESS required for static" | |
| echo " address ${PVE_AUTONET_BRIDGE_ADDRESS}" | |
| [ -n "$PVE_AUTONET_BRIDGE_GATEWAY" ] && echo " gateway ${PVE_AUTONET_BRIDGE_GATEWAY}" | |
| fi | |
| echo "" | |
| echo "source /etc/network/interfaces.d/*" | |
| } | |
| apply_interfaces() { | |
| command -v ifreload >/dev/null 2>&1 || fail "ifreload is required (install ifupdown2)" | |
| ifreload -a | |
| } | |
| purge_stale_files() { | |
| rm -f /run/network/interfaces.d/10-uplink* \ | |
| /run/network/interfaces.d/pve-autonet* \ | |
| /etc/network/interfaces.d/10-uplink* \ | |
| /etc/network/interfaces.d/pve-autonet 2>/dev/null || true | |
| } | |
| # ── /etc/hosts + online notification ───────────────────────────── | |
| update_hosts_and_notify_online() { | |
| local ip fqdn short hosts_tmp | |
| ip="$(wait_for_ip "$PVE_AUTONET_WAIT_SECONDS" || true)" | |
| if [ -z "$ip" ]; then | |
| warn "No IPv4 on ${PVE_AUTONET_BRIDGE_NAME} after ${PVE_AUTONET_WAIT_SECONDS}s" | |
| send_bark "No IP on ${PVE_AUTONET_BRIDGE_NAME} after ${PVE_AUTONET_WAIT_SECONDS}s" | |
| return 0 | |
| fi | |
| fqdn="$(hostname -f 2>/dev/null || hostname || true)" | |
| short="$(hostname -s 2>/dev/null || hostname || true)" | |
| if [ -n "$fqdn" ] && [ -n "$short" ]; then | |
| hosts_tmp="$(mktemp /run/pve-autonet.hosts.XXXXXX)" | |
| awk -v short="$short" -v fqdn="$fqdn" ' | |
| { | |
| drop=0 | |
| if ($1 ~ /^[0-9]/) { | |
| for (i=2; i<=NF; i++) { | |
| if ($i == short || $i == fqdn) { drop=1; break } | |
| } | |
| } | |
| if (!drop) print $0 | |
| } | |
| ' /etc/hosts > "$hosts_tmp" || true | |
| { cat "$hosts_tmp"; echo "${ip} ${fqdn} ${short}"; } > /etc/hosts | |
| rm -f "$hosts_tmp" | |
| fi | |
| [ -x /usr/bin/pvebanner ] && { /usr/bin/pvebanner || true; } | |
| notify_online "$ip" | |
| } | |
| # ── Generate or regenerate bridge config ───────────────────────── | |
| generate_bridge_config() { | |
| local uplink="$1" | |
| local cfg_tmp cfg_backup | |
| cfg_tmp="$(mktemp /tmp/pve-autonet.cfg.XXXXXX)" | |
| cfg_backup="$(mktemp /tmp/pve-autonet.bak.XXXXXX)" | |
| trap 'rm -f "${cfg_tmp:-}" "${cfg_backup:-}"' EXIT | |
| purge_stale_files | |
| render_interfaces "$uplink" > "$cfg_tmp" | |
| if ! grep -q "bridge-ports ${uplink}" "$cfg_tmp"; then | |
| fail "Generated config failed validation (missing bridge-ports ${uplink})" | |
| fi | |
| if cmp -s "$cfg_tmp" "$INTERFACES_FILE"; then | |
| log "Config unchanged; no reload needed" | |
| return 0 | |
| fi | |
| cp -a "$INTERFACES_FILE" "$cfg_backup" | |
| cp "$cfg_tmp" "$INTERFACES_FILE" | |
| if ! apply_interfaces; then | |
| warn "ifreload failed; rolling back" | |
| cp -a "$cfg_backup" "$INTERFACES_FILE" | |
| apply_interfaces || true | |
| fail "Failed to apply generated network config" | |
| fi | |
| return 0 | |
| } | |
| # ── Main ───────────────────────────────────────────────────────── | |
| main() { | |
| local uplink previous_uplink diag_reason ip | |
| if [ -r "$DEFAULTS_FILE" ]; then | |
| # shellcheck disable=SC1090 | |
| . "$DEFAULTS_FILE" | |
| fi | |
| : "${PVE_AUTONET_NIC:=}" | |
| : "${PVE_AUTONET_BRIDGE_NAME:=vmbr0}" | |
| : "${PVE_AUTONET_BRIDGE_METHOD:=dhcp}" | |
| : "${PVE_AUTONET_BRIDGE_ADDRESS:=}" | |
| : "${PVE_AUTONET_BRIDGE_GATEWAY:=}" | |
| : "${PVE_AUTONET_WAIT_SECONDS:=20}" | |
| : "${PVE_AUTONET_BARK_ENABLED:=1}" | |
| : "${PVE_AUTONET_BARK_URL:=https://bark.ws/WNA2EuVCumFkyG7BJuua8L}" | |
| : "${PVE_AUTONET_BARK_TITLE:=PVE9 Info}" | |
| : "${PVE_AUTONET_BARK_CURL_TIMEOUT:=5}" | |
| case "$PVE_AUTONET_BRIDGE_METHOD" in | |
| dhcp|static) ;; | |
| *) fail "PVE_AUTONET_BRIDGE_METHOD must be 'dhcp' or 'static'" ;; | |
| esac | |
| mkdir -p "$STATE_DIR" "$(dirname "$LOCK_FILE")" | |
| if command -v flock >/dev/null 2>&1; then | |
| exec 9>"$LOCK_FILE" | |
| if ! flock -n 9; then | |
| log "Another instance running — skipping" | |
| exit 0 | |
| fi | |
| fi | |
| notify_stage "Auto-net started on $(host_short)" | |
| log "" | |
| # ── Phase 1: Diagnose existing config ──────────────────────── | |
| diag_reason="$(diagnose_network 2>/dev/null || true)" | |
| if [ -z "$diag_reason" ]; then | |
| log "Bridge config present; checking connectivity..." | |
| ip="$(wait_for_ip "$PVE_AUTONET_WAIT_SECONDS" || true)" | |
| if [ -n "$ip" ]; then | |
| if gateway_reachable; then | |
| log_ok "Network healthy (${ip}, gateway reachable)" | |
| echo "$(bridge_port_from_config)" > "$STATE_UPLINK_FILE" 2>/dev/null || true | |
| update_hosts_and_notify_online | |
| return 0 | |
| else | |
| diag_reason="bridge has IP ${ip} but gateway unreachable" | |
| fi | |
| else | |
| diag_reason="bridge has no IP after ${PVE_AUTONET_WAIT_SECONDS}s" | |
| fi | |
| fi | |
| # ── Phase 2: Self-heal ─────────────────────────────────────── | |
| notify_stage "Network issue detected: ${diag_reason}" | |
| notify_stage "Attempting auto-repair..." | |
| uplink="$(select_uplink_nic || true)" | |
| [ -n "$uplink" ] || fail "No usable physical NIC found" | |
| previous_uplink="" | |
| if [ -r "$STATE_UPLINK_FILE" ]; then | |
| previous_uplink="$(<"$STATE_UPLINK_FILE")" | |
| previous_uplink="$(echo "$previous_uplink" | head -n1 | tr -cd 'a-zA-Z0-9._-')" | |
| fi | |
| if [ -n "$previous_uplink" ] && [ "$previous_uplink" != "$uplink" ]; then | |
| notify_stage "Uplink changed: ${previous_uplink} -> ${uplink}" | |
| else | |
| notify_stage "Using uplink ${uplink} for ${PVE_AUTONET_BRIDGE_NAME}" | |
| fi | |
| generate_bridge_config "$uplink" | |
| echo "$uplink" > "$STATE_UPLINK_FILE" | |
| notify_stage "Applied ${PVE_AUTONET_BRIDGE_NAME} on ${uplink}" | |
| # ── Phase 3: Verify the fix worked ─────────────────────────── | |
| ip="$(wait_for_ip "$PVE_AUTONET_WAIT_SECONDS" || true)" | |
| if [ -n "$ip" ]; then | |
| if gateway_reachable; then | |
| notify_ok "Auto-repair successful (${ip}, gateway reachable)" | |
| else | |
| notify_stage "Bridge has IP ${ip} but gateway still unreachable" | |
| fi | |
| update_hosts_and_notify_online | |
| else | |
| notify_stage "Bridge still has no IP after repair — manual intervention needed" | |
| fi | |
| } | |
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Unit] | |
| Description=Generate resilient Proxmox bridge config from active NIC | |
| DefaultDependencies=no | |
| After=local-fs.target | |
| Wants=network-pre.target | |
| Before=network-pre.target | |
| Before=networking.service | |
| Before=pve-guests.service | |
| ConditionPathExists=/usr/local/sbin/pve-autonet | |
| [Service] | |
| Type=oneshot | |
| ExecStart=/usr/local/sbin/pve-autonet | |
| TimeoutStartSec=90 | |
| [Install] | |
| WantedBy=multi-user.target |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment