From 145b001927baf0f9a545ec2265aa605f6403188c Mon Sep 17 00:00:00 2001 From: SDE Informatique <1+sde@noreply.localhost> Date: Thu, 10 Sep 2026 13:08:10 +0000 Subject: [PATCH] Ajouter pve-netconfig.sh --- pve-netconfig.sh | 531 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 pve-netconfig.sh diff --git a/pve-netconfig.sh b/pve-netconfig.sh new file mode 100644 index 0000000..3f8c3f3 --- /dev/null +++ b/pve-netconfig.sh @@ -0,0 +1,531 @@ +#!/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 : +# 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" <> "$tmp" <> "$tmp" <> "$tmp" </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" <> "$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 <