Files
pvedhcp/pve-netconfig.sh
2026-09-10 13:08:10 +00:00

531 lines
18 KiB
Bash

#!/bin/bash
#===============================================================================
# pve-netconfig.sh
#
# Gestion réseau pour hôte Proxmox VE 9 (Debian 13 Trixie) autonome :
# - vmbr0 : DHCP avec fallback IP statique
# - vmbr1 : DHCP sans fallback
# - Génère /etc/network/interfaces
# - Synchronise /etc/hosts avec les IP réellement obtenues (ip -j a)
#
# Usage :
# pve-netconfig.sh apply-interfaces # (Ré)écrit /etc/network/interfaces
# pve-netconfig.sh sync-hosts # Met à jour /etc/hosts seulement
# pve-netconfig.sh check # Vérifie DHCP vmbr0 -> fallback si KO
# pve-netconfig.sh status # Affiche l'état courant
#
# Auteur : <service informatique>
# Licence : usage interne
#===============================================================================
set -uo pipefail
#===============================================================================
# C O N F I G U R A T I O N
#===============================================================================
# --- Interfaces bridges gérées ---------------------------------------------
# vmbr0 = LAN principal (management PVE)
# vmbr1 = second réseau (DHCP seul, pas de fallback)
BRIDGE_MAIN="vmbr0"
BRIDGE_SECONDARY="vmbr1"
# --- Ports physiques rattachés aux bridges ---------------------------------
# Laisser vide ("") pour conserver la configuration existante détectée
# dans /etc/network/interfaces (recommandé après une install PVE standard).
# Sinon, forcer explicitement : PORT_MAIN="eno1"
PORT_MAIN=""
PORT_SECONDARY=""
# --- Fallback statique pour vmbr0 (si le DHCP ne répond pas) ----------------
FALLBACK_IP="192.168.1.250"
FALLBACK_CIDR="24"
FALLBACK_GATEWAY="192.168.1.1"
FALLBACK_DNS="192.168.1.1 9.9.9.9"
# --- Comportement DHCP ------------------------------------------------------
DHCP_TIMEOUT=25 # Secondes d'attente d'un bail DHCP avant fallback
DHCP_RETRY_INTERVAL=300 # Secondes entre 2 tentatives de retour au DHCP
# (0 = ne jamais retenter, reste en statique)
# --- /etc/hosts -------------------------------------------------------------
# Nom court + FQDN de l'hôte. Par défaut déduits du système.
HOSTNAME_SHORT="$(hostname -s)"
HOSTNAME_FQDN="$(hostname -f 2>/dev/null || echo "${HOSTNAME_SHORT}.local")"
# Alias supplémentaires ajoutés à la ligne de l'IP principale
HOSTS_EXTRA_ALIASES="pve"
# Marqueurs délimitant le bloc géré dans /etc/hosts
HOSTS_BEGIN="# >>> pve-netconfig BEGIN (ne pas editer manuellement) >>>"
HOSTS_END="# <<< pve-netconfig END <<<"
# --- Divers -----------------------------------------------------------------
LOG_TAG="pve-netconfig"
STATE_DIR="/var/lib/pve-netconfig"
LOCK_FILE="/run/pve-netconfig.lock"
INTERFACES_FILE="/etc/network/interfaces"
HOSTS_FILE="/etc/hosts"
BACKUP_DIR="/var/backups/pve-netconfig"
# Mettre à 1 pour appliquer les changements réseau à chaud (ifreload -a)
# après réécriture de /etc/network/interfaces. Sinon, reboot requis.
AUTO_RELOAD=1
#===============================================================================
# F I N C O N F I G U R A T I O N
#===============================================================================
umask 022
mkdir -p "$STATE_DIR" "$BACKUP_DIR"
log() { logger -t "$LOG_TAG" -p daemon.info -- "$*"; echo "[*] $*"; }
warn() { logger -t "$LOG_TAG" -p daemon.warning -- "$*"; echo "[!] $*" >&2; }
err() { logger -t "$LOG_TAG" -p daemon.err -- "$*"; echo "[X] $*" >&2; }
require_root() {
if [[ $EUID -ne 0 ]]; then
err "Ce script doit être exécuté en root."
exit 1
fi
}
# Verrou pour éviter les exécutions concurrentes (hook DHCP + timer + boot)
acquire_lock() {
exec 9>"$LOCK_FILE"
if ! flock -w 30 9; then
warn "Impossible d'obtenir le verrou ($LOCK_FILE), abandon."
exit 0
fi
}
backup_file() {
local f="$1"
[[ -f "$f" ]] || return 0
local ts base
ts="$(date +%Y%m%d-%H%M%S)"
base="$(basename "$f")"
cp -a "$f" "${BACKUP_DIR}/${base}.${ts}"
# Rotation : conserver les 20 dernières sauvegardes par fichier
ls -1t "${BACKUP_DIR}/${base}."* 2>/dev/null | tail -n +21 | xargs -r rm -f
}
#-------------------------------------------------------------------------------
# Détection des ports physiques déjà déclarés dans /etc/network/interfaces
#-------------------------------------------------------------------------------
detect_bridge_ports() {
local br="$1"
awk -v br="$br" '
$1=="iface" && $2==br {inblk=1; next}
inblk && $1=="iface" {inblk=0}
inblk && ($1=="bridge-ports" || $1=="bridge_ports") {
$1=""; sub(/^[ \t]+/,""); print; exit
}
' "$INTERFACES_FILE" 2>/dev/null
}
resolve_ports() {
if [[ -z "$PORT_MAIN" ]]; then
PORT_MAIN="$(detect_bridge_ports "$BRIDGE_MAIN")"
fi
if [[ -z "$PORT_SECONDARY" ]]; then
PORT_SECONDARY="$(detect_bridge_ports "$BRIDGE_SECONDARY")"
fi
[[ -z "$PORT_MAIN" ]] && PORT_MAIN="none"
[[ -z "$PORT_SECONDARY" ]] && PORT_SECONDARY="none"
}
#-------------------------------------------------------------------------------
# Génération de /etc/network/interfaces
#-------------------------------------------------------------------------------
apply_interfaces() {
require_root
resolve_ports
log "Génération de $INTERFACES_FILE (${BRIDGE_MAIN}=dhcp+fallback, ${BRIDGE_SECONDARY}=dhcp)"
backup_file "$INTERFACES_FILE"
local tmp
tmp="$(mktemp /etc/network/.interfaces.XXXXXX)"
cat > "$tmp" <<EOF
# Généré par pve-netconfig.sh le $(date -Is)
# Ne pas éditer à la main : modifier /usr/local/sbin/pve-netconfig.sh
# puis exécuter : pve-netconfig.sh apply-interfaces
auto lo
iface lo inet loopback
EOF
# --- Ports physiques en manual -----------------------------------------
local p
for p in $PORT_MAIN $PORT_SECONDARY; do
[[ "$p" == "none" ]] && continue
cat >> "$tmp" <<EOF
auto ${p}
iface ${p} inet manual
EOF
done
# --- vmbr0 : DHCP (le fallback est géré par le service systemd) ---------
cat >> "$tmp" <<EOF
auto ${BRIDGE_MAIN}
iface ${BRIDGE_MAIN} inet dhcp
bridge-ports ${PORT_MAIN}
bridge-stp off
bridge-fd 0
# Fallback statique si aucun bail DHCP :
# ${FALLBACK_IP}/${FALLBACK_CIDR} gw ${FALLBACK_GATEWAY}
# appliqué dynamiquement par pve-netconfig-watch.service
EOF
# --- vmbr1 : DHCP simple ------------------------------------------------
cat >> "$tmp" <<EOF
auto ${BRIDGE_SECONDARY}
iface ${BRIDGE_SECONDARY} inet dhcp
bridge-ports ${PORT_SECONDARY}
bridge-stp off
bridge-fd 0
EOF
# --- Conservation d'un éventuel source-directory ------------------------
if grep -qE '^\s*source\s+/etc/network/interfaces\.d' "$INTERFACES_FILE" 2>/dev/null; then
echo "source /etc/network/interfaces.d/*" >> "$tmp"
fi
chmod 0644 "$tmp"
mv "$tmp" "$INTERFACES_FILE"
log "$INTERFACES_FILE écrit."
if [[ "$AUTO_RELOAD" == "1" ]]; then
if command -v ifreload >/dev/null 2>&1; then
log "Rechargement à chaud (ifreload -a)..."
ifreload -a || warn "ifreload a retourné une erreur, vérifier 'journalctl -u networking'."
else
warn "ifreload absent (ifupdown2 non installé ?) — un redémarrage sera nécessaire."
fi
fi
}
#-------------------------------------------------------------------------------
# Récupération des IPv4 via 'ip -j a' (JSON, parsé avec jq)
#-------------------------------------------------------------------------------
get_ipv4() {
local ifc="$1"
ip -4 -j addr show dev "$ifc" 2>/dev/null \
| jq -r '.[0].addr_info[]? | select(.family=="inet" and .scope=="global") | .local' \
| head -n1
}
get_ipv4_cidr() {
local ifc="$1"
ip -4 -j addr show dev "$ifc" 2>/dev/null \
| jq -r '.[0].addr_info[]? | select(.family=="inet" and .scope=="global")
| "\(.local)/\(.prefixlen)"' \
| head -n1
}
# Détecte si l'adresse courante de vmbr0 provient d'un bail DHCP
has_dhcp_lease() {
local ifc="$1"
# dhclient (isc-dhcp-client)
if [[ -f "/var/lib/dhcp/dhclient.${ifc}.leases" ]] \
&& grep -q "fixed-address" "/var/lib/dhcp/dhclient.${ifc}.leases" 2>/dev/null; then
pgrep -f "dhclient.*${ifc}" >/dev/null 2>&1 && return 0
fi
# dhcpcd
if [[ -f "/var/lib/dhcpcd/${ifc}.lease" ]] || [[ -f "/var/lib/dhcpcd5/dhcpcd-${ifc}.lease" ]]; then
pgrep -f "dhcpcd.*${ifc}" >/dev/null 2>&1 && return 0
fi
return 1
}
is_static_fallback_active() {
[[ -f "${STATE_DIR}/fallback_active" ]]
}
#-------------------------------------------------------------------------------
# Application / retrait du fallback statique sur vmbr0
#-------------------------------------------------------------------------------
apply_fallback() {
log "Application du fallback statique sur ${BRIDGE_MAIN} : ${FALLBACK_IP}/${FALLBACK_CIDR}"
# Arrêt du client DHCP sur cette interface
pkill -f "dhclient.*${BRIDGE_MAIN}" 2>/dev/null
if command -v dhcpcd >/dev/null 2>&1; then
dhcpcd -k "$BRIDGE_MAIN" 2>/dev/null
fi
# Purge des adresses IPv4 globales existantes
ip -4 addr flush dev "$BRIDGE_MAIN" scope global 2>/dev/null
ip addr add "${FALLBACK_IP}/${FALLBACK_CIDR}" dev "$BRIDGE_MAIN"
ip link set "$BRIDGE_MAIN" up
if [[ -n "$FALLBACK_GATEWAY" ]]; then
ip route replace default via "$FALLBACK_GATEWAY" dev "$BRIDGE_MAIN" 2>/dev/null \
|| warn "Impossible d'installer la route par défaut via ${FALLBACK_GATEWAY}"
fi
write_resolv_conf $FALLBACK_DNS
touch "${STATE_DIR}/fallback_active"
log "Fallback actif. Interface de management : https://${FALLBACK_IP}:8006"
}
clear_fallback() {
if is_static_fallback_active; then
log "Bail DHCP retrouvé sur ${BRIDGE_MAIN} — retrait du fallback statique."
ip addr del "${FALLBACK_IP}/${FALLBACK_CIDR}" dev "$BRIDGE_MAIN" 2>/dev/null
rm -f "${STATE_DIR}/fallback_active"
fi
}
write_resolv_conf() {
# N'écrase resolv.conf que si ce n'est pas un lien géré (resolvconf/systemd)
if [[ -L /etc/resolv.conf ]]; then
warn "/etc/resolv.conf est un lien symbolique — DNS non modifié."
return 0
fi
backup_file /etc/resolv.conf
{
echo "# Généré par pve-netconfig.sh (fallback statique) $(date -Is)"
local domain
domain="${HOSTNAME_FQDN#*.}"
[[ "$domain" != "$HOSTNAME_FQDN" ]] && echo "search ${domain}"
local ns
for ns in "$@"; do
echo "nameserver ${ns}"
done
} > /etc/resolv.conf
}
#-------------------------------------------------------------------------------
# Tentative de récupération d'un bail DHCP sur vmbr0
#-------------------------------------------------------------------------------
try_dhcp() {
local ifc="$1"
log "Tentative d'obtention d'un bail DHCP sur ${ifc} (timeout ${DHCP_TIMEOUT}s)..."
if command -v dhclient >/dev/null 2>&1; then
timeout "$((DHCP_TIMEOUT + 5))" \
dhclient -1 -timeout "$DHCP_TIMEOUT" -v "$ifc" >/dev/null 2>&1
elif command -v dhcpcd >/dev/null 2>&1; then
timeout "$((DHCP_TIMEOUT + 5))" \
dhcpcd -t "$DHCP_TIMEOUT" "$ifc" >/dev/null 2>&1
else
err "Aucun client DHCP trouvé (dhclient / dhcpcd). Installer isc-dhcp-client."
return 1
fi
[[ -n "$(get_ipv4 "$ifc")" ]]
}
#-------------------------------------------------------------------------------
# Contrôle principal : DHCP ou fallback
#-------------------------------------------------------------------------------
do_check() {
require_root
acquire_lock
# --- Attente du lien physique (link carrier) ----------------------------
local waited=0
while [[ $waited -lt 30 ]]; do
if [[ "$(cat "/sys/class/net/${BRIDGE_MAIN}/operstate" 2>/dev/null)" == "up" ]]; then
break
fi
sleep 2
waited=$((waited + 2))
done
local current_ip
current_ip="$(get_ipv4 "$BRIDGE_MAIN")"
if [[ -n "$current_ip" ]] && has_dhcp_lease "$BRIDGE_MAIN"; then
# DHCP OK
clear_fallback
log "${BRIDGE_MAIN} : bail DHCP actif (${current_ip})."
elif is_static_fallback_active; then
# On est déjà en fallback : on retente périodiquement le DHCP
if [[ "$DHCP_RETRY_INTERVAL" != "0" ]]; then
log "Fallback actif — nouvelle tentative DHCP sur ${BRIDGE_MAIN}."
if try_dhcp "$BRIDGE_MAIN"; then
clear_fallback
log "Retour en DHCP réussi : $(get_ipv4 "$BRIDGE_MAIN")"
else
log "Toujours pas de serveur DHCP — maintien du fallback ${FALLBACK_IP}."
fi
fi
else
# Pas d'IP et pas encore de fallback
warn "${BRIDGE_MAIN} : aucune adresse IPv4. Tentative DHCP..."
if try_dhcp "$BRIDGE_MAIN"; then
log "Bail DHCP obtenu : $(get_ipv4 "$BRIDGE_MAIN")"
else
warn "Échec DHCP sur ${BRIDGE_MAIN} — bascule sur le fallback statique."
apply_fallback
fi
fi
# --- vmbr1 : DHCP uniquement, aucun fallback ---------------------------
if ip link show "$BRIDGE_SECONDARY" >/dev/null 2>&1; then
local ip2
ip2="$(get_ipv4 "$BRIDGE_SECONDARY")"
if [[ -z "$ip2" ]]; then
log "${BRIDGE_SECONDARY} : pas d'IP, tentative DHCP (sans fallback)."
try_dhcp "$BRIDGE_SECONDARY" >/dev/null 2>&1 \
&& log "${BRIDGE_SECONDARY} : $(get_ipv4 "$BRIDGE_SECONDARY")" \
|| log "${BRIDGE_SECONDARY} : aucun bail DHCP (comportement attendu si non raccordé)."
else
log "${BRIDGE_SECONDARY} : ${ip2}"
fi
fi
sync_hosts_locked
}
#-------------------------------------------------------------------------------
# Mise à jour de /etc/hosts
#-------------------------------------------------------------------------------
sync_hosts_locked() {
local ip_main ip_sec block tmp
ip_main="$(get_ipv4 "$BRIDGE_MAIN")"
ip_sec="$(get_ipv4 "$BRIDGE_SECONDARY")"
# PVE ne DOIT jamais se retrouver sans résolution du hostname.
if [[ -z "$ip_main" ]]; then
if is_static_fallback_active; then
ip_main="$FALLBACK_IP"
else
warn "Aucune IP sur ${BRIDGE_MAIN} — /etc/hosts conserve son contenu actuel."
return 1
fi
fi
block="${HOSTS_BEGIN}\n"
block+="${ip_main}\t${HOSTNAME_FQDN} ${HOSTNAME_SHORT}"
local a
for a in $HOSTS_EXTRA_ALIASES; do
[[ "$a" == "$HOSTNAME_SHORT" ]] && continue
block+=" ${a}"
done
block+="\n"
if [[ -n "$ip_sec" && "$ip_sec" != "$ip_main" ]]; then
block+="${ip_sec}\t${HOSTNAME_SHORT}-${BRIDGE_SECONDARY}\n"
fi
block+="${HOSTS_END}"
tmp="$(mktemp /etc/.hosts.XXXXXX)"
# 1) Base statique (loopback + IPv6) toujours présente
cat > "$tmp" <<EOF
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
EOF
# 2) Bloc géré dynamiquement
printf '%b\n\n' "$block" >> "$tmp"
# 3) Conservation des entrées personnalisées hors bloc et hors base
if [[ -f "$HOSTS_FILE" ]]; then
awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" '
$0==b {skip=1; next}
$0==e {skip=0; next}
skip {next}
/^127\.0\.0\.1/ {next}
/^::1|^fe00::|^ff00::|^ff02::/ {next}
/^# The following lines are desirable/ {next}
/^[[:space:]]*$/ {next}
{print}
' "$HOSTS_FILE" >> "$tmp"
fi
# Vérification de cohérence avant remplacement
if ! grep -qE "^${ip_main}[[:space:]]" "$tmp"; then
err "Contrôle d'intégrité échoué, /etc/hosts non modifié."
rm -f "$tmp"
return 1
fi
if [[ -f "$HOSTS_FILE" ]] && diff -q "$tmp" "$HOSTS_FILE" >/dev/null 2>&1; then
rm -f "$tmp"
log "/etc/hosts déjà à jour (${ip_main})."
return 0
fi
backup_file "$HOSTS_FILE"
chmod 0644 "$tmp"
mv "$tmp" "$HOSTS_FILE"
log "/etc/hosts mis à jour : ${HOSTNAME_FQDN} -> ${ip_main}"
# Recharge pveproxy/pvedaemon pour prendre en compte la nouvelle IP
if systemctl is-active --quiet pveproxy; then
systemctl reload-or-restart pveproxy pvedaemon 2>/dev/null \
|| warn "Rechargement de pveproxy/pvedaemon impossible."
fi
}
sync_hosts() {
require_root
acquire_lock
sync_hosts_locked
}
#-------------------------------------------------------------------------------
# Affichage de l'état
#-------------------------------------------------------------------------------
do_status() {
echo "=== pve-netconfig : état ==="
echo "Hostname : ${HOSTNAME_SHORT} (${HOSTNAME_FQDN})"
printf "%-8s : %s\n" "$BRIDGE_MAIN" "$(get_ipv4_cidr "$BRIDGE_MAIN" || echo '(aucune)')"
printf "%-8s : %s\n" "$BRIDGE_SECONDARY" "$(get_ipv4_cidr "$BRIDGE_SECONDARY" || echo '(aucune)')"
echo -n "Mode vmbr0 : "
if is_static_fallback_active; then
echo "FALLBACK STATIQUE (${FALLBACK_IP}/${FALLBACK_CIDR})"
elif has_dhcp_lease "$BRIDGE_MAIN"; then
echo "DHCP"
else
echo "indéterminé"
fi
echo
echo "--- Route par défaut ---"
ip route show default
echo
echo "--- Bloc géré dans /etc/hosts ---"
sed -n "/${HOSTS_BEGIN//\//\\/}/,/${HOSTS_END//\//\\/}/p" "$HOSTS_FILE" 2>/dev/null
}
#-------------------------------------------------------------------------------
# Point d'entrée
#-------------------------------------------------------------------------------
case "${1:-}" in
apply-interfaces) apply_interfaces ;;
sync-hosts) sync_hosts ;;
check) do_check ;;
status) do_status ;;
*)
cat <<EOF
Usage : $(basename "$0") {apply-interfaces|sync-hosts|check|status}
apply-interfaces (Ré)génère /etc/network/interfaces selon la configuration
check Vérifie le DHCP sur ${BRIDGE_MAIN}, applique le fallback si besoin,
puis synchronise /etc/hosts
sync-hosts Met à jour uniquement /etc/hosts d'après 'ip a'
status Affiche l'état réseau courant
EOF
exit 1
;;
esac