Étiquette : ubuntu

19 juillet 2026 /

Après chaque réinstallation d’Ubuntu, je lance un seul script et je reviens devant une machine entièrement configurée : /home remonté sur son LVM, NAS monté, Docker/KVM, supervision, relais mail, VPN, VeraCrypt, tâches planifiées — et une validation automatique qui m’envoie son rapport par mail après le reboot. Ce script, je le fais évoluer depuis des années ; le voici publié en intégralité (version 8.9.21 pour Ubuntu 26.04 LTS), avec son fichier de configuration et son script de validation.

C’est un script personnel : il installe mes outils et reflète mon infrastructure. Il n’est pas fait pour être exécuté tel quel, mais pour être adapté — et toute la conception va dans ce sens : ce qui est propre à la machine vit dans un fichier .conf séparé, et le script est découpé en 41 sections qu’on peut lister, rejouer une par une ou tout simplement supprimer. Ce guide d’adaptation précède les trois fichiers complets.

Les trois fichiers

Fichier Rôle
postinstall.26.04_v2.sh Le script principal (41 sections, options --list / --from / --only)
postinstall.26.04_v2.conf Votre configuration réelle, secrets inclus — à créer depuis le modèle ci-dessous, jamais versionné
validate_postinstall.sh Validation post-reboot (lancée automatiquement par un timer systemd, rapport par mail)

Mise en route :

# 1. Copier les trois fichiers EN LOCAL sur la machine (pas depuis un montage réseau :
#    le script verrouille son .conf en root:600, impossible sur du CIFS → refus de démarrer)
# 2. Créer la configuration depuis le modèle
cp postinstall.26.04_v2.conf.example postinstall.26.04_v2.conf
vi postinstall.26.04_v2.conf        # renseigner tous les champs ⚠️

# 3. Découvrir les sections
bash postinstall.26.04_v2.sh --list

# 4. Lancer (avec journal complet)
sudo bash postinstall.26.04_v2.sh 2>&1 | tee postinstall.log

# Reprendre après interruption / rejouer une section
sudo bash postinstall.26.04_v2.sh --from 22
sudo bash postinstall.26.04_v2.sh --only 28

Ce que vous devez modifier — 1. le fichier .conf

Tout ce qui est propre à votre machine et vos comptes est dans le .conf. Le modèle publié plus bas contient des marqueurs ⚠️ À REMPLACER partout où une valeur est attendue :

Variables À mettre
UTILISATEUR, HOSTNAME_MACHINE Votre compte principal et le nom que prendra la machine (hostnamectl, Postfix, supervision)
REBOOT_AUTO false pour un premier essai sans redémarrage automatique
NAS_IP, NAS_USER, NAS_PASSWORD, NAS_PARTAGES Votre NAS et la liste de vos partages CIFS (chacun devient un montage fstab + un signet Nautilus). Pas de NAS ? Voir la table des sections ci-dessous.
LVM_HOME_DEVICE Le volume logique de votre /home (vérifier avec sudo lvdisplay). C’est le cœur de la section 1 : un /home sur son propre LVM survit aux réinstallations.
GMAIL_USER, GMAIL_APP_PASSWORD Votre adresse et un App Password Gmail (myaccount.google.com) — utilisés par Postfix pour que la machine sache envoyer des mails (alertes SMART, rapport de validation…)
VERACRYPT_* Chemin de votre conteneur chiffré et son mot de passe (alias de montage/démontage 1984/1984q — renommez-les à votre goût dans la section 8b)
CMA_TOKEN, CMA_ENDPOINT, CMA_HOST Token et endpoint de votre serveur Centreon — uniquement si vous supervisez avec Centreon
PRO_TOKEN Token Ubuntu Pro (ubuntu.com/pro/dashboard) — laisser vide pour sauter l’attachement, la section 2g s’ignore proprement
VPN_*_USER/PASS/CERT/KEY Identifiants, certificats et clés de vos connexions OpenVPN (chez moi : CyberGhost, 4 pays). Les blocs heredoc du modèle montrent où coller chaque certificat.
RDP_USER, RDP_PASS Identifiants GNOME Remote Desktop (section 26b)

Le script applique lui-même root:600 sur ce fichier au démarrage et refuse de s’exécuter s’il n’y parvient pas (cas typique : lancement depuis un montage CIFS où chmod est inopérant).

Ce que vous devez modifier — 2. les sections

Chaque section est une fonction section_<id>() déclarée dans le tableau SECTIONS en tête de script. Pour désactiver une section : supprimez sa ligne du tableau, c’est tout — le dispatcher, --list et la numérotation d’affichage suivent automatiquement. Les sections à examiner en priorité selon votre contexte :

Section Contenu À adapter si…
1 Montage /home sur LVM via UUID Votre /home n’est pas sur un volume logique dédié → supprimez-la
2 Dépôts et paquets (navigateurs, VirtualBox Oracle, outils…) La liste des paquets est personnelle — faites votre marché
2a, 23 Outils et installation Claude Code Vous n’utilisez pas Claude Code
2d, 2e NRPE + SNMP + agent Centreon, sudoers de supervision Pas de Centreon → supprimez ; autre supervision → remplacez
2g Ubuntu Pro (ESM + Livepatch) Se saute tout seul si PRO_TOKEN est vide
4 IP statique (fige le bail DHCP courant) + DNS Vous préférez rester en DHCP pur
5, 19 Montages NAS CIFS + signets Nautilus Pas de NAS → supprimez les deux
8 .bashrc + aliases La plus personnelle du lot : les alias (mise à jour d’une pile Docker distante, aide-mémoire, raccourcis) référencent mon environnement — réécrivez-les
8b, 24 Scripts VeraCrypt + installation Pas de conteneur chiffré
22 Postfix en relais Gmail Autre fournisseur mail → adaptez hôte SMTP et authentification
26, 26b Agent DWService, GNOME Remote Desktop Pas de prise en main à distance
27 Crontab : rappels quotidiens + scan ClamAV planifié Les rappels sont personnels
28 OpenVPN multi-pays via NetworkManager Autre fournisseur VPN → adaptez les serveurs remote et le bloc OVPN commun du .conf
29b Timer systemd de validation post-reboot À garder ! Ajustez plutôt validate_postinstall.sh aux sections que vous conservez

Note sur le caviardage : les valeurs visibles dans les fichiers publiés sont des exemples — utilisateur utilisateur, machine ma-machine, NAS 192.168.1.100, serveur de supervision 192.168.1.50, e-mail prenom.nom@gmail.com. Remplacez-les par les vôtres via le .conf ; aucun secret réel ne figure dans ces fichiers.

Après le reboot

Quatre minutes après le redémarrage, le timer systemd lance validate-postinstall.sh (en utilisateur, pas root) : il recontrôle chaque section — montages, services, groupes, versions — et envoie son rapport à GMAIL_USER, puis désactive son propre timer. Les quelques points non automatisables (authentification Claude Code, code DWService…) sont listés en fin de rapport comme actions manuelles.

Le script principal — postinstall.26.04_v2.sh (complet)

3 842 lignes, 41 sections. L’historique des versions en tête de fichier raconte aussi les pièges rencontrés au fil des années — il fait partie de la documentation.

#!/bin/bash
################################################################################
# Script de post-installation Ubuntu 26.04 LTS "Resolute Raccoon"
#
# Description : Configure automatiquement un système Ubuntu 26.04 fraîchement installé
#               avec montage /home sur LVM, montage NAS, optimisations système,
#               automatisations, Claude Code, DWService + VeraCrypt
#               + Fortune français + rappels quotidiens + Postfix Gmail + ClamAV planifié
#               + VPN OpenVPN (Suisse + USA + France + Russie)
#               + yt-dlp (dernière version GitHub) + alias mp3dl
#               + smartmontools + supervision Centreon (groupe monitoring, sudoers)
#
# Auteur      : Romain J.
# Fait avec amour par Romain J.
# Version     : 8.9.21
# Date        : 18 juillet 2026
#
# Usage       : sudo bash postinstall.26.04_v2.sh [options]
#   (sans option)        exécution complète de toutes les sections + reboot
#                        (nombre exact : --list, source unique = tableau SECTIONS)
#   -l, --list           liste les sections disponibles (id + titre)
#   -f, --from <id>      reprend l'exécution À PARTIR de la section <id>
#   -o, --only <id>      exécute UNIQUEMENT la section <id> (pas de reboot
#                        sauf --only 31)
#   -h, --help           affiche l'aide
#
# Checklist finale avant exécution
# 1. ⚠️ MODIFIER LE MOT DE PASSE NAS         → postinstall.26.04_v2.conf → NAS_PASSWORD
# 2. ⚠️ VÉRIFIER LE MOT DE PASSE APP GMAIL   → postinstall.26.04_v2.conf → GMAIL_APP_PASSWORD
# 3. ⚠️ VÉRIFIER LE MOT DE PASSE VERACRYPT   → postinstall.26.04_v2.conf → VERACRYPT_PASS
# 4. ⚠️ VÉRIFIER LES IDENTIFIANTS VPN        → postinstall.26.04_v2.conf → VPN_*_USER / VPN_*_PASS
# 5. ⚠️ VÉRIFIER LE TOKEN CMA                → postinstall.26.04_v2.conf → CMA_TOKEN
# 5b. ⚠️ VÉRIFIER LES IDENTIFIANTS RDP       → postinstall.26.04_v2.conf → RDP_USER / RDP_PASS
# 5c. ⚠️ VÉRIFIER LE TOKEN UBUNTU PRO        → postinstall.26.04_v2.conf → PRO_TOKEN
# 6. Vérifier le device LVM existe : sudo lvdisplay /dev/home-vg/home_lv
# 7. Vérifier connexion réseau filaire : nmcli connection show --active
# 8. Copier les 3 FICHIERS en LOCAL sur la machine à installer :
#    postinstall.26.04_v2.sh + postinstall.26.04_v2.conf + validate_postinstall.sh
#    ⚠️ TOUS les trois, même si une vieille copie traîne dans /home (le /home
#    LVM survit aux réinstallations — un validate périmé fausse la validation).
#    Le script applique LUI-MÊME root:600 sur le .conf au démarrage (v8.9.5).
# 9. Lancer avec logs : sudo bash postinstall.26.04_v2.sh 2>&1 | tee postinstall.log
#

# Sécurité bash : -u = erreur si variable indéfinie, -o pipefail = erreur si échec dans un pipeline
# -e volontairement omis : on veut que le script continue même si une étape échoue,
# les erreurs sont capturées via log_warn/log_error et résumées en fin d'exécution.
set -uo pipefail

# IFS restreint aux séparateurs tab+newline (protège contre les espaces dans les noms de fichiers/chemins)
IFS=$'\n\t'

# Version du script — source unique (évite la dérive entre l'en-tête et les messages)
readonly VERSION="8.9.21"

################################################################################
# TABLE DES SECTIONS + OPTIONS DE LIGNE DE COMMANDE
################################################################################

# Tableau ordonné "id|titre" — chaque id correspond à une fonction section_<id>()
# définie plus bas. Le dispatcher (fin de script) les exécute dans cet ordre.
readonly -a SECTIONS=(
    "1|Configuration du montage de /home sur LVM (via UUID) + optimisation SSD"
    "2|Mise à jour système, dépôts et paquets"
    "2a|Outils Claude Code (npm-global, ccstatusline)"
    "2b|Configuration Firefox Snap — restauration profil"
    "2c|Configuration plocate (updatedb.conf)"
    "2d|Monitoring — NRPE + SNMP + Centreon Monitoring Agent"
    "2e|Configuration supervision Centreon (groupe monitoring, sudoers)"
    "2f|Alertes SMART par mail (smartd)"
    "2g|Attachement Ubuntu Pro (ESM + Livepatch)"
    "3|Configuration Docker, KVM et groupes utilisateur"
    "4|Configuration réseau : IP statique (DHCP figé) + DNS"
    "5|Configuration des montages NAS"
    "6|Configuration GRUB"
    "7|Éditeur par défaut"
    "8|Configuration .bashrc + aliases (backup + écrasement)"
    "8b|Scripts VeraCrypt sécurisés"
    "9|Sudo sans mot de passe"
    "10|Flatpak et applications"
    "11|Mises à jour automatiques"
    "12|Nettoyage paquets"
    "13|Limitation journal systemd"
    "14|Configuration Nautilus/GNOME + extensions + thème terminal Ptyxis"
    "15|Désactivation veille/éteint écran"
    "16|Désactivation UFW"
    "17|Création script nettoyage automatique"
    "18|Création script mise à jour automatique (système)"
    "19|Dossiers XDG locaux + signets NAS Nautilus"
    "20|Vérification services"
    "21|Nettoyage intermédiaire"
    "22|Configuration Postfix relay Gmail"
    "23|Installation Claude Code"
    "24|Installation VeraCrypt"
    "25|Installation fortune-mod, cowsay et citations françaises"
    "26|Installation de l'agent DWService"
    "26b|GNOME Remote Desktop (RDP headless + partage de session)"
    "27|Configuration crontab rappels + scan ClamAV"
    "28|Configuration OpenVPN (Suisse + USA + France + Russie)"
    "29|Nettoyage final avant reboot"
    "29b|Validation post-reboot automatique (timer + mail)"
    "30|RÉSUMÉ FINAL"
    "31|Redémarrage du système"
)

# Affiche l'aide et quitte (utilisable sans sudo)
afficher_aide() {
    cat << AIDE
Usage : sudo bash $(basename "$0") [options]

Post-installation Ubuntu 26.04 LTS — v${VERSION}

Options :
  (aucune)          exécution complète des ${#SECTIONS[@]} sections + reboot final
  -l, --list        liste les sections disponibles (id + titre)
  -f, --from <id>   reprend l'exécution À PARTIR de la section <id>
                    (utile après une interruption — inclut résumé et reboot)
  -o, --only <id>   exécute UNIQUEMENT la section <id>
                    (pas de reboot sauf --only 31)
  -h, --help        affiche cette aide

Exemples :
  sudo bash $(basename "$0") --list
  sudo bash $(basename "$0") --from 22     # reprend à Postfix
  sudo bash $(basename "$0") --only 28     # rejoue uniquement les VPN
AIDE
    exit 0
}

# Liste les sections (id + titre) et quitte (utilisable sans sudo)
lister_sections() {
    echo "Sections disponibles (${#SECTIONS[@]}) — postinstall v${VERSION} :"
    local entree
    for entree in "${SECTIONS[@]}"; do
        printf '  %-4s %s\n' "${entree%%|*}" "${entree#*|}"
    done
    exit 0
}

# Vérifie qu'un id de section existe dans la table
section_existe() {
    local id="$1" entree
    for entree in "${SECTIONS[@]}"; do
        [[ "${entree%%|*}" == "$id" ]] && return 0
    done
    return 1
}

# ---- Parsing des options (AVANT la vérification root : -h/-l sans sudo) ----
FROM_ID=""
ONLY_ID=""
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help) afficher_aide ;;
        -l|--list) lister_sections ;;
        -f|--from)
            FROM_ID="${2:-}"
            [[ -z "$FROM_ID" ]] && { echo "[ERREUR] --from requiert un id de section (--list)" >&2; exit 1; }
            shift ;;
        -o|--only)
            ONLY_ID="${2:-}"
            [[ -z "$ONLY_ID" ]] && { echo "[ERREUR] --only requiert un id de section (--list)" >&2; exit 1; }
            shift ;;
        *)
            echo "[ERREUR] Option inconnue : $1 (voir --help)" >&2
            exit 1 ;;
    esac
    shift
done

if [[ -n "$FROM_ID" && -n "$ONLY_ID" ]]; then
    echo "[ERREUR] --from et --only sont mutuellement exclusives" >&2
    exit 1
fi
if [[ -n "$FROM_ID" ]] && ! section_existe "$FROM_ID"; then
    echo "[ERREUR] Section inconnue : '$FROM_ID' — voir la liste avec --list" >&2
    exit 1
fi
if [[ -n "$ONLY_ID" ]] && ! section_existe "$ONLY_ID"; then
    echo "[ERREUR] Section inconnue : '$ONLY_ID' — voir la liste avec --list" >&2
    exit 1
fi

# Vérification root AVANT toute écriture dans /var/log
# (sinon mkdir/tee échouent silencieusement en non-root)
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
    echo "[ERREUR] Ce script doit être exécuté en root : sudo bash $0" >&2
    exit 1
fi

################################################################################
# SYSTÈME DE LOG + SUIVI DES ANOMALIES
################################################################################

LOG_DIR="/var/log/postinstall"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/postinstall-$(date +%Y%m%d_%H%M%S).log"

exec > >(tee -a "$LOG_FILE") 2>&1

# Tableau pour collecter les warnings et erreurs — affiché en résumé final
declare -a ANOMALIES=()

log_info()    { echo "[$(date '+%F %T')] [INFO]    $*"; }
log_ok()      { echo "[$(date '+%F %T')] [OK]      $*"; }
log_warn()    {
    echo "[$(date '+%F %T')] [WARN]    $*"
    ANOMALIES+=("[ATTENTION] $*")
}
log_error()   {
    echo "[$(date '+%F %T')] [ERREUR]  $*"
    ANOMALIES+=("[ERREUR]    $*")
}
log_section() { echo ""; echo "[$(date '+%F %T')] ======== $* ========"; }

# Récapitulatif des anomalies — appelé par la section 30 (résumé final)
# et par le trap INT/TERM (interruption en cours d'exécution)
afficher_anomalies() {
    if [ ${#ANOMALIES[@]} -eq 0 ]; then
        echo "✅ AUCUNE ANOMALIE DÉTECTÉE DURANT L'EXÉCUTION"
    else
        echo "❌ ${#ANOMALIES[@]} ANOMALIE(S) DÉTECTÉE(S) — À VÉRIFIER :"
        local item
        for item in "${ANOMALIES[@]}"; do
            echo "  • $item"
        done
    fi
}

log_info "Démarrage post-installation v${VERSION} — log : $LOG_FILE"

################################################################################
# MODE NON-INTERACTIF APT/DEBCONF (v8.9.1)
################################################################################

# Le script tourne via « sudo bash … | tee » : stdin est indisponible, tout
# écran debconf (postfix mailer_type, etc.) bloquerait indéfiniment.
# Exporté AVANT toute commande apt — hérité par tous les apt install du script.
export DEBIAN_FRONTEND=noninteractive

################################################################################
# CHARGEMENT DE LA CONFIGURATION EXTERNE
################################################################################

# Le fichier postinstall.conf doit se trouver dans le même répertoire que ce script,
# ou son chemin peut être passé via la variable d'environnement POSTINSTALL_CONF.
CONF_FILE="${POSTINSTALL_CONF:-$(dirname "$(realpath "$0")")/postinstall.26.04_v2.conf}"

if [ ! -f "$CONF_FILE" ]; then
    echo "[ERREUR] Fichier de configuration introuvable : $CONF_FILE"
    echo "         Le script attend postinstall.26.04_v2.conf dans son répertoire"
    echo "         (ou via la variable POSTINSTALL_CONF=/chemin/vers/mon.conf)"
    exit 1
fi

# Sécurisation du .conf : le fichier est SOURCÉ en root et contient des
# secrets. Le script tourne en root (sudo) → il applique LUI-MÊME root:600
# au lieu d'exiger une préparation manuelle, source d'erreurs (v8.9.5 —
# au test réel, chown/chmod avaient été appliqués au .sh au lieu du .conf).
# On préserve 400 si déjà en place (lecture seule volontaire).
CONF_PERMS=$(stat -c "%a" "$CONF_FILE")
chown root:root "$CONF_FILE" 2>/dev/null || true
[ "$CONF_PERMS" != "400" ] && { chmod 600 "$CONF_FILE" 2>/dev/null || true; }

# Filet de sécurité : en usage normal (script + .conf copiés en local sur la
# machine à installer), l'application ci-dessus réussit toujours et ce
# contrôle est silencieux. Il ne se déclenche que si le chown/chmod n'a pas
# pris (ex. lancement par mégarde depuis le NAS CIFS, où ils sont inopérants).
CONF_OWNER=$(stat -c "%U" "$CONF_FILE")
CONF_PERMS=$(stat -c "%a" "$CONF_FILE")
if [ "$CONF_OWNER" != "root" ] || { [ "$CONF_PERMS" != "600" ] && [ "$CONF_PERMS" != "400" ]; }; then
    echo "[ERREUR] Impossible de sécuriser $CONF_FILE (propriétaire : $CONF_OWNER, permissions : $CONF_PERMS — attendu root:600)" >&2
    echo "         Le .conf contient des secrets sourcés en root — lancement depuis le NAS CIFS ?" >&2
    echo "         Copiez le répertoire en LOCAL sur la machine à installer puis relancez." >&2
    exit 1
fi

# Sourcing du fichier de configuration
# shellcheck source=postinstall.26.04_v2.conf
source "$CONF_FILE"
log_info "Configuration chargée depuis : $CONF_FILE"

# ---- Variables globales calculées une seule fois (C2 + U1 v8.8.24) ----
TS=$(date +%Y%m%d_%H%M%S)           # Timestamp partagé par tous les backups
START=$(date +%s)                    # Pour calcul de la durée totale au résumé final

# ---- Vérification des variables critiques du .conf (R1 v8.8.24) ----
# Détection early-fail : crash explicite si une variable critique est manquante
# (sinon `set -u` ferait crasher quelque part au milieu du script)
VARS_CRITIQUES=(
    UTILISATEUR HOSTNAME_MACHINE
    NAS_IP NAS_USER NAS_PASSWORD NAS_CREDENTIALS
    LVM_HOME_DEVICE
    GMAIL_USER GMAIL_APP_PASSWORD
    VERACRYPT_PASS_FILE VERACRYPT_PASS VERACRYPT_CONTAINER VERACRYPT_MOUNTPOINT
    VPN_SUISSE_USER VPN_SUISSE_PASS VPN_USA_USER VPN_USA_PASS
    VPN_FRANCE_USER VPN_FRANCE_PASS VPN_RUSSIE_USER VPN_RUSSIE_PASS
    VPN_COMMON_OVPN VPN_COMMON_CA
    VPN_SUISSE_CERT VPN_SUISSE_KEY VPN_USA_CERT VPN_USA_KEY
    VPN_FRANCE_CERT VPN_FRANCE_KEY VPN_RUSSIE_CERT VPN_RUSSIE_KEY
    CMA_TOKEN CMA_ENDPOINT CMA_HOST
)
MISSING=()
for v in "${VARS_CRITIQUES[@]}"; do
    [ -z "${!v:-}" ] && MISSING+=("$v")
done
# Vérification spécifique pour le tableau NAS_PARTAGES — declare -p d'abord :
# sous set -u, ${#NAS_PARTAGES[@]} sur un tableau absent ferait crasher le
# script avec un message brut "variable sans liaison" avant l'early-fail
if ! declare -p NAS_PARTAGES &>/dev/null || [[ ${#NAS_PARTAGES[@]} -eq 0 ]]; then
    MISSING+=("NAS_PARTAGES")
fi
if [ ${#MISSING[@]} -gt 0 ]; then
    log_error "Variables manquantes dans $CONF_FILE : ${MISSING[*]}"
    log_error "Compléter le .conf avant de relancer le script."
    exit 1
fi
log_ok "Variables critiques du .conf : ${#VARS_CRITIQUES[@]} validées"

# Dériver BASHRC_USER depuis UTILISATEUR (variable calculée, pas configurable)
# Placé APRÈS la vérification : si UTILISATEUR manquait, le message explicite
# ci-dessus s'affiche au lieu d'un crash brut "variable sans liaison" (set -u)
BASHRC_USER="/home/$UTILISATEUR/.bashrc"

# PATH étendu pour propager les binaires npm-global via sudo -u (sections 2a et 23)
# Défini ici (et non en section 2a) : avec --only 23, une définition locale à la
# section 2a laisserait la variable indéfinie → crash sous set -u
NPM_PATH="/home/${UTILISATEUR}/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# ---- Wrapper apt update standardisé (C1 v8.8.24, fix v8.8.25) ----
# Réduit le bruit sans masquer les vraies erreurs/warnings.
# Capture le code retour réel d'`apt update` (avant le pipe) pour que les
# chaînes `apt_update_silent || log_warn ...` se déclenchent en cas d'échec.
# Note : on évite le pipe direct car `|| true` final écrase PIPESTATUS.
apt_update_silent() {
    local output rc
    output=$(apt update -o Dpkg::Use-Pty=0 2>&1)
    rc=$?
    echo "$output" | grep -E '^(Err|W:)' || true
    return "$rc"
}

# ---- Wrapper d'ajout de dépôt apt (L2 v8.8.24) ----
# Factorise le pattern Steam/Brave/Opera (3 occurrences identiques avant v8.8.24)
# Arguments : nom_dépôt, URL clé GPG, ligne deb, chemin keyring
ajouter_depot_apt() {
    local nom="$1" url_key="$2" deb_line="$3" keyring="$4"
    local list_file="/etc/apt/sources.list.d/${nom}.list"
    if [ ! -f "$list_file" ]; then
        log_info "Ajout dépôt $nom..."
        curl -fsSL "$url_key" | gpg --dearmor -o "$keyring" \
            || log_warn "$nom : téléchargement clé GPG échoué"
        echo "$deb_line" > "$list_file"
        apt_update_silent || log_warn "Update dépôt $nom échoué"
    else
        log_info "Dépôt $nom déjà configuré"
    fi
}

# ---- Wrapper snap install avec nouvelle tentative (v8.9.11) ----
# Le Snap Store échoue par intermittence (« unexpected EOF » en plein
# téléchargement, constaté au test réel du 2026-07-03 sur codium) :
# une 2e tentative après une courte pause absorbe presque toujours l'aléa.
# Usage : installer_snap <paquet> [options snap install]
installer_snap() {
    local paquet="$1"; shift
    if snap install "$paquet" "$@"; then
        return 0
    fi
    log_info "$paquet : snap install en échec — nouvelle tentative dans 15 s..."
    sleep 15
    snap install "$paquet" "$@"
}

# ---- Wrapper gsettings (L1 v8.8.24) ----
# Factorise les 17 appels gsettings via sudo -u UTILISATEUR + DBUS_SESSION_BUS_ADDRESS
# Usage : gset org.gnome.nautilus.preferences show-hidden-files true
# La variable DBUS_PATH doit être définie avant l'appel (par la section appelante).
gset() {
    sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="${DBUS_PATH:-unix:path=/run/user/${USER_UID:-1000}/bus}" \
        gsettings set "$@"
}

################################################################################
# VÉRIFICATIONS PRÉALABLES
################################################################################

log_section "VÉRIFICATIONS PRÉALABLES"

# (vérification root déjà effectuée en tête de script, avant la création du log)

# Lit une clé de /etc/os-release SANS sourcing (v8.9.3 — cause racine enfin
# identifiée et reproduite) : ce script déclare « readonly VERSION » et
# os-release assigne VERSION="26.04 LTS…" → erreur readonly FATALE qui tue le
# sous-shell avant tout echo, d'où les valeurs vides aux tests réels
# v8.9.0/v8.9.1 (garde OS « blanche », VeraCrypt « Ubuntu /24.04/22.04 »).
lire_os_release() {
    grep -oP "^${1}=\"?\K[^\"]+" /etc/os-release 2>/dev/null || true
}

# ---- Garde version OS : ce script cible Ubuntu 26.04 (v8.9.0) ----
# Lancé par mégarde sur une autre version (paquets, codenames, GNOME différents),
# il faut une confirmation explicite pour continuer.
OS_ID=$(lire_os_release ID)
OS_ID=${OS_ID:-inconnu}
OS_VERSION=$(lire_os_release VERSION_ID)
OS_VERSION=${OS_VERSION:-inconnue}
if [[ "$OS_ID" == "ubuntu" && "$OS_VERSION" == "26.04" ]]; then
    log_ok "OS cible confirmé : Ubuntu 26.04"
else
    log_warn "OS détecté : ${OS_ID} ${OS_VERSION} — ce script cible Ubuntu 26.04"
    reponse_os=""
    read -r -p "Continuer quand même ? (o/N) " reponse_os || true
    if [[ ! "$reponse_os" =~ ^[oO]$ ]]; then
        log_error "Exécution annulée — OS non cible (${OS_ID} ${OS_VERSION})"
        exit 1
    fi
    log_warn "Exécution forcée sur ${OS_ID} ${OS_VERSION} (confirmé par l'utilisateur)"
fi

if ! id "$UTILISATEUR" &>/dev/null; then
    log_error "L'utilisateur $UTILISATEUR n'existe pas"
    exit 1
fi

USER_UID=$(id -u "$UTILISATEUR")
USER_GID=$(id -g "$UTILISATEUR")
log_ok "Utilisateur détecté : $UTILISATEUR (UID: $USER_UID, GID: $USER_GID)"

# ---- Nom de machine depuis le .conf (HOSTNAME_MACHINE — v8.9.0) ----
# Aligne le hostname système sur la configuration (postfix myhostname, sondes
# Centreon, mails de rapport) quel que soit le nom saisi à l'installation.
if [[ "$(hostname)" == "$HOSTNAME_MACHINE" ]]; then
    log_ok "Nom de machine déjà correct : $HOSTNAME_MACHINE"
else
    hostnamectl set-hostname "$HOSTNAME_MACHINE" \
        && log_ok "Nom de machine défini : $HOSTNAME_MACHINE (hostnamectl)" \
        || log_warn "hostnamectl set-hostname $HOSTNAME_MACHINE échoué"
    # 127.0.1.1 doit suivre le nouveau nom (résolution locale du hostname)
    if grep -qE '^127\.0\.1\.1[[:space:]]' /etc/hosts; then
        sed -i -E "s|^127\.0\.1\.1[[:space:]].*|127.0.1.1\t${HOSTNAME_MACHINE}|" /etc/hosts
    else
        printf '127.0.1.1\t%s\n' "$HOSTNAME_MACHINE" >> /etc/hosts
    fi
    log_ok "/etc/hosts : 127.0.1.1 → $HOSTNAME_MACHINE"
fi

if [ "$NAS_PASSWORD" = "VOTRE_MOT_DE_PASSE_ICI" ]; then
    log_error "Veuillez modifier le mot de passe NAS dans le script avant de l'exécuter"
    exit 1
fi

# Détection réseau étendue : eno, enp, eth, wl
CONNEXION_NET=$(nmcli -t -f NAME,DEVICE connection show --active | grep -E 'enp|eth|eno|wl' | cut -d: -f1 | head -n1)
if [ -z "$CONNEXION_NET" ]; then
    log_error "Aucune connexion réseau filaire/wifi active détectée"
    log_error "Résultat nmcli : $(nmcli -t -f NAME,DEVICE connection show --active)"
    exit 1
fi
log_ok "Connexion réseau active : $CONNEXION_NET"

################################################################################
# 1. CONFIGURATION DU MONTAGE DE /HOME SUR LVM VIA UUID + OPTIMISATION SSD
################################################################################

section_1() {


# Un seul backup pour toute la section (TS global C2)
FSTAB_BACKUP="/etc/fstab.backup.${TS}"
cp /etc/fstab "$FSTAB_BACKUP"
log_info "Backup fstab : $FSTAB_BACKUP"

# ---- Optimisation SSD : ajout de noatime sur / si pas déjà présent ----
if grep -qP "^\S+\s+/\s+ext4" /etc/fstab && ! grep -qP "^\S+\s+/\s+ext4.*noatime" /etc/fstab; then
    sed -i -E '/^\S+\s+\/\s+ext4/ s/(defaults|errors=remount-ro)/\1,noatime/' /etc/fstab
    log_ok "noatime ajouté pour la racine / (optimisation SSD)"
else
    log_ok "/ : noatime déjà présent ou partition non ext4 — aucune modification"
fi

# ---- Montage /home sur LVM avec noatime ----
# Vérification du device avant toute action (R2 v8.8.24) : si le device LVM
# n'existe pas, on saute proprement la sous-section plutôt que d'écrire
# une entrée fstab cassée et de tenter un mount voué à l'échec.
if [ ! -b "$LVM_HOME_DEVICE" ]; then
    log_warn "Device LVM absent ou inaccessible : $LVM_HOME_DEVICE"
    log_warn "  → Sous-section /home sur LVM ignorée (corrigez LVM_HOME_DEVICE dans le .conf)"
    log_warn "  → Pour vérifier : sudo lvdisplay"
else
    HOME_UUID=$(blkid -s UUID -o value "$LVM_HOME_DEVICE" 2>/dev/null || true)

    if [ -z "$HOME_UUID" ]; then
        log_warn "Impossible de récupérer l'UUID de $LVM_HOME_DEVICE"
        log_warn "Utilisation du nom de device : $LVM_HOME_DEVICE"
        LVM_ENTRY="$LVM_HOME_DEVICE /home ext4 defaults,noatime 0 2"
    else
        log_ok "UUID trouvé pour /home : $HOME_UUID"
        LVM_ENTRY="UUID=$HOME_UUID /home ext4 defaults,noatime 0 2"
    fi

    # Pattern strict : début de ligne, non-commenté, premier champ + espaces + /home + espaces
    # (évite de matcher /home_old, /var/home, etc.)
    HOME_PATTERN='^[^#][^[:space:]]*[[:space:]]+/home[[:space:]]'

    if grep -qE "$HOME_PATTERN" /etc/fstab && ! grep -qF "$LVM_ENTRY" /etc/fstab; then
        # Délimiteur | échappé (\|…|d) : HOME_PATTERN contient un / non échappé
        # qui terminait l'adresse sed /…/ → la commande échouait (rc=1) et
        # l'ancienne entrée /home restait, dupliquant le point de montage
        sed -i -E "\|${HOME_PATTERN}|d" /etc/fstab \
            || log_warn "fstab : suppression de l'ancienne entrée /home échouée — vérifier les doublons"
    fi

    if grep -qF "$LVM_ENTRY" /etc/fstab; then
        log_ok "Entrée LVM déjà présente dans /etc/fstab"
    else
        echo "" >> /etc/fstab
        echo "# Partition /home sur LVM (via UUID) — noatime pour SSD" >> /etc/fstab
        echo "$LVM_ENTRY" >> /etc/fstab
        log_ok "Entrée LVM ajoutée à /etc/fstab (avec noatime)"

        # Erreur visible si mount échoue : on conserve stderr pour diagnostic
        if mount -a; then
            log_ok "/home monté immédiatement"
        else
            log_warn "Montage immédiat échoué – redémarrage requis"
        fi
    fi
fi

}

################################################################################
# 2. MISE À JOUR SYSTÈME, DÉPÔTS DOCKER & STEAM, PAQUETS ET NAVIGATEURS
################################################################################

section_2() {


apt clean || true
rm -rf /var/lib/apt/lists/* || true

log_info "apt update + upgrade..."
apt_update_silent
apt upgrade -y \
    || { log_error "apt upgrade échoué — réseau ou dépôts défaillants"; exit 1; }

# Dépôt Docker Engine officiel
# Docker ne publie pas immédiatement pour les nouvelles versions Ubuntu :
# fallback explicite sur "noble" (24.04 LTS) jusqu'à publication officielle 26.04
log_info "Ajout dépôt Docker..."
apt install -y ca-certificates curl || true
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
    || log_warn "Téléchargement clé Docker échoué"
chmod a+r /etc/apt/keyrings/docker.asc

UBUNTU_CODENAME=$(lire_os_release VERSION_CODENAME)
case "$UBUNTU_CODENAME" in
    noble|jammy|focal)
        DOCKER_CODENAME="$UBUNTU_CODENAME"
        ;;
    *)
        # 26.04 "resolute" pas encore supporté par Docker → fallback noble
        DOCKER_CODENAME="noble"
        log_info "Codename '$UBUNTU_CODENAME' non supporté par Docker — fallback sur '$DOCKER_CODENAME'"
        ;;
esac

rm -f /etc/apt/sources.list.d/docker.*
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $DOCKER_CODENAME stable" \
    > /etc/apt/sources.list.d/docker.list
apt_update_silent || log_warn "Update dépôt Docker échoué, continuation..."

log_info "Installation Docker Engine..."
apt install -y --no-install-recommends \
    docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
    || log_warn "Erreur Docker, continuation..."

# Dépôt Steam officiel (via wrapper L2 v8.8.24)
ajouter_depot_apt "steam" \
    "http://repo.steampowered.com/steam/archive/stable/steam.gpg" \
    "deb [arch=amd64 signed-by=/usr/share/keyrings/steam.gpg] http://repo.steampowered.com/steam/ stable steam" \
    "/usr/share/keyrings/steam.gpg"
apt install -y steam || log_warn "Erreur installation Steam, continuation..."

# Dépôt VirtualBox Oracle officiel (v8.9.19)
# Remplace le paquet virtualbox des dépôts Ubuntu : son virtualbox-dkms suit
# mal les nouveaux noyaux HWE (7.0.16 incompatible noyau 7.0 → postinst noyau
# en échec, initramfs jamais généré, kernel panic au boot — incident 2026-07-18).
# Le paquet Oracle embarque l'ISO Additions Invité (virtualbox-guest-additions-iso inutile)
# et recompile ses modules via le service vboxdrv à chaque nouveau noyau (pas de DKMS).
case "$UBUNTU_CODENAME" in
    resolute|noble|jammy)
        VBOX_CODENAME="$UBUNTU_CODENAME"
        ;;
    *)
        # Codename pas encore publié par Oracle → fallback noble (24.04 LTS)
        VBOX_CODENAME="noble"
        log_info "Codename '$UBUNTU_CODENAME' non supporté par le dépôt VirtualBox — fallback sur '$VBOX_CODENAME'"
        ;;
esac
ajouter_depot_apt "virtualbox" \
    "https://www.virtualbox.org/download/oracle_vbox_2016.asc" \
    "deb [arch=amd64 signed-by=/usr/share/keyrings/oracle-virtualbox-2016.gpg] https://download.virtualbox.org/virtualbox/debian $VBOX_CODENAME contrib" \
    "/usr/share/keyrings/oracle-virtualbox-2016.gpg"
apt install -y virtualbox-7.2 || log_warn "Erreur installation VirtualBox 7.2, continuation..."
# Groupe requis par le paquet Oracle pour l'USB — prise d'effet à la reconnexion
usermod -aG vboxusers "$UTILISATEUR" || log_warn "Ajout de $UTILISATEUR au groupe vboxusers échoué"

# Paquets principaux — TABLEAU (robuste au word-splitting et à IFS=$'\n\t')
# Note 26.04 : musescore3 absent des dépôts officiels (installé via Flatpak à la section 10)
#              chromium-browser (deb) supprimé → snap (section 2, ci-dessous)
#              gnome-screensaver supprimé depuis Ubuntu 24.10 — verrouillage via loginctl lock-session
#              tintii (plugin GIMP obsolète) et xsane (frontend X11) retirés
#              libfuse2 → libfuse2t64 (transition time_t 64 bits, 24.04+)
#              nagios-plugins-* → monitoring-plugins-* (paquets nagios-plugins transitionnels supprimés)
#              qemu-kvm → qemu-system-x86 ; p7zip-rar, pinta et
#              gnome-shell-extension-dashtodock absents des dépôts 26.04 — retirés
#              (dash-to-dock : installer via Extension Manager) (v8.9.1, test réel)
#              virtualbox + virtualbox-guest-additions-iso → paquet Oracle
#              virtualbox-7.2 (bloc dépôt ci-dessus, v8.9.19)
log_info "Installation paquets principaux..."

# Préconfiguration debconf de postfix (v8.9.1) : sans preseed, l'écran
# interactif mailer_type bloquait l'installation groupée ci-dessous
# (stdin indisponible via « | tee ») et laissait dpkg dans un état interrompu
# → cascade d'échecs sur tous les paquets suivants (67 manquants au test réel)
echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections
echo "postfix postfix/mailname string $(hostname)" | debconf-set-selections
PAQUETS_PRINCIPAUX=(
    torbrowser-launcher filezilla transmission plocate
    postfix mailutils
    gnome-software gnome-software-plugin-flatpak audacity calibre cdrdao dia gimp
    gnome-tweaks guake gparted hplip imagemagick inkscape keepassxc manpages-fr
    nautilus-image-converter net-tools okular xournalpp 7zip rar pdfsam
    printer-driver-cups-pdf sharutils synaptic thunderbird timidity evince libreoffice-gnome
    ubuntu-restricted-extras unace unrar uudeview vlc whois
    gnome-shell-extensions cifs-utils openssh-server clamav clamav-daemon clamav-freshclam
    vim htop glances iotop unattended-upgrades flatpak rsync gdebi libfuse2t64 ansible
    qemu-system-x86 libvirt-daemon-system gedit pwgen tumbler ffmpegthumbnailer
    gir1.2-gdkpixbuf-2.0 libvirt-clients bridge-utils virt-manager shellcheck
    gnome-shell-extension-gsconnect hydrapaper
    gnome-shell-extension-manager ncal figlet mpv
    default-jdk scanmem git wakeonlan nodejs npm tree ncdu jq
    network-manager-openvpn network-manager-openvpn-gnome pv snmpd
    python3 python3-pip python3-venv fortune-mod cowsay lolcat nagios-nrpe-server nagios-nrpe-plugin
    monitoring-plugins-basic monitoring-plugins-standard geany smartmontools
)

# Installation groupée (rapide) ; un seul nom invalide ferait avorter toute la
# transaction, d'où la reprise individuelle des paquets restés manquants.
apt install -y "${PAQUETS_PRINCIPAUX[@]}" \
    || log_warn "Installation groupée incomplète — reprise paquet par paquet"

# Vérifie qu'un paquet est bien installé
paquet_est_installe() {
    dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q '^install ok installed'
}

# Compteur OK/KO + 2e passe individuelle (U3 v8.8.24) — diagnostic précis sans casser le flux
# Sortie apt VISIBLE (v8.9.1) : la redirection >/dev/null masquait la progression
# et rendait tout blocage/échec indiagnosticable en direct (test réel)
APT_OK=0; APT_KO=0; APT_KO_LIST=""
for pkg in "${PAQUETS_PRINCIPAUX[@]}"; do
    paquet_est_installe "$pkg" || apt install -y "$pkg" || true
    if paquet_est_installe "$pkg"; then
        APT_OK=$((APT_OK + 1))
    else
        APT_KO=$((APT_KO + 1))
        APT_KO_LIST+=" $pkg"
    fi
done
APT_TOTAL=$((APT_OK + APT_KO))
if [ "$APT_KO" -eq 0 ]; then
    log_ok "Paquets principaux installés : $APT_OK/$APT_TOTAL (tous présents)"
else
    log_warn "Paquets principaux : $APT_OK/$APT_TOTAL ok — $APT_KO manquant(s) :$APT_KO_LIST"
fi

# Activation SSH
log_info "Activation SSH..."
systemctl enable --now ssh 2>/dev/null \
    || systemctl enable --now openssh-server 2>/dev/null \
    || log_warn "SSH : aucun service trouvé"

# VSCodium
log_info "Installation VSCodium..."
installer_snap codium --classic || log_warn "VSCodium snap échoué (2 tentatives)"

# Signal
log_info "Installation Signal..."
installer_snap signal-desktop || log_warn "Signal snap échoué (2 tentatives)"

# Chromium — snap (le paquet deb chromium-browser a été supprimé des dépôts Ubuntu 26.04)
log_info "Installation Chromium (snap)..."
installer_snap chromium || log_warn "Erreur installation Chromium snap (2 tentatives), continuation..."
log_ok "Chromium installé (snap)"

# Google Chrome — wget et apt chaînés : si wget échoue, apt n'est pas appelé
log_info "Installation Google Chrome..."
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/google-chrome.deb \
    && apt install -y /tmp/google-chrome.deb \
    || log_warn "Erreur installation Chrome (wget ou apt), continuation..."
rm -f /tmp/google-chrome.deb
rm -f /etc/apt/sources.list.d/google-chrome.list /etc/apt/sources.list.d/google.list

# Patch cron Google Chrome pour forcer Architectures: amd64 et éviter le warning i386
# Idempotent (N1 v8.8.34) : le cron n'est ré-exécuté que si le patch vient d'être appliqué
if [[ -f /etc/cron.daily/google-chrome ]]; then
    if ! grep -q "Architectures: amd64" /etc/cron.daily/google-chrome; then
        sed -i 's/Components: main$/Components: main\nArchitectures: amd64/' /etc/cron.daily/google-chrome
        log_info "Patch cron Google Chrome appliqué (Architectures: amd64)"
        /etc/cron.daily/google-chrome
    else
        log_info "Patch cron Google Chrome déjà appliqué"
    fi
fi

apt_update_silent

# Brave Browser (via wrapper L2 v8.8.24)
ajouter_depot_apt "brave-browser-release" \
    "https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg" \
    "deb [arch=amd64 signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main" \
    "/usr/share/keyrings/brave-browser-archive-keyring.gpg"
apt install -y brave-browser || log_warn "Erreur installation Brave, continuation..."
log_ok "Brave Browser installé"

# Opera Browser (via wrapper L2 v8.8.24)
ajouter_depot_apt "opera-stable" \
    "https://deb.opera.com/archive.key" \
    "deb [arch=amd64 signed-by=/usr/share/keyrings/opera-archive-keyring.gpg] https://deb.opera.com/opera-stable/ stable non-free" \
    "/usr/share/keyrings/opera-archive-keyring.gpg"
apt install -y opera-stable || log_warn "Erreur installation Opera, continuation..."
log_ok "Opera Browser installé"

# yt-dlp — binaire officiel GitHub (version apt toujours trop ancienne)
log_info "Installation yt-dlp (binaire GitHub)..."
apt remove -y yt-dlp 2>/dev/null || true   # Supprimer l'ancienne version apt si présente
rm -f /usr/bin/yt-dlp                       # Nettoyer l'éventuel lien apt résiduel
wget -q https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
    && chmod a+rx /usr/local/bin/yt-dlp \
    && hash -r \
    && log_ok "yt-dlp installé ($(yt-dlp --version 2>/dev/null || echo 'version inconnue'))" \
    || log_warn "yt-dlp : installation échouée — installer manuellement"

log_ok "Paquets et dépôts installés (ou au maximum possible)"

}

################################################################################
# 2a. OUTILS CLAUDE CODE
################################################################################

section_2a() {


# --- Outils Claude Code ---
# npm global sans sudo
sudo -u "$UTILISATEUR" mkdir -p "/home/${UTILISATEUR}/.npm-global"
sudo -u "$UTILISATEUR" npm config set prefix "/home/${UTILISATEUR}/.npm-global"
grep -qxF 'export PATH="$HOME/.npm-global/bin:$PATH"' "/home/${UTILISATEUR}/.bashrc" \
    || echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "/home/${UTILISATEUR}/.bashrc"

# NPM_PATH (binaires npm-global via sudo -u env) : défini globalement après
# le chargement du .conf — voir le commentaire à sa définition (v8.9.0)

# ccstatusline (status-bar tokens)
sudo -u "$UTILISATEUR" env PATH="$NPM_PATH" \
    npm install -g ccstatusline \
    && log_ok "ccstatusline installé" \
    || log_warn "ccstatusline : installation échouée (lancer manuellement après connexion)"

# Injection statusLine dans ~/.claude/settings.json (non-interactif)
sudo -u "$UTILISATEUR" python3 - << 'PYEOF'
import json, os

settings_path = os.path.expanduser("~/.claude/settings.json")
os.makedirs(os.path.dirname(settings_path), exist_ok=True)

try:
    with open(settings_path, "r") as f:
        settings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
    settings = {}

settings["statusLine"] = {
    "type": "command",
    "command": "npx -y ccstatusline@latest",
    "padding": 0
}

with open(settings_path, "w") as f:
    json.dump(settings, f, indent=2)

print("✓ statusLine injecté dans ~/.claude/settings.json")
PYEOF

log_ok "Outils Claude Code configurés"

}

################################################################################
# 2b. CONFIGURATION FIREFOX SNAP — Restauration du profil utilisateur
# Note : Firefox snap stocke profiles.ini dans ~/snap/firefox/common/.mozilla/firefox/
# Le fichier est réécrit proprement pour garantir l'idempotence (pas de duplication).
################################################################################

section_2b() {


SNAP_PROFILES_DIR="/home/$UTILISATEUR/snap/firefox/common/.mozilla/firefox"
PROFILES_INI="$SNAP_PROFILES_DIR/profiles.ini"
PROFIL_CIBLE="b4r78nv8.default"

if [ -f "$PROFILES_INI" ]; then
    # Idempotence : ne modifier que si le profil cible n'est pas déjà actif Default=1
    if grep -qF "Path=$PROFIL_CIBLE" "$PROFILES_INI" \
       && awk -v p="$PROFIL_CIBLE" '
           /^\[/  {section=$0}
           /^Path=/ {path=$0}
           /^Default=1/ && path == "Path="p {found=1; exit}
           END {exit !found}
         ' "$PROFILES_INI"; then
        log_ok "Firefox Snap : profil '$PROFIL_CIBLE' déjà actif — aucune modification"
    else
        log_info "profiles.ini Firefox Snap détecté — réécriture pour activer '$PROFIL_CIBLE'"
        cp "$PROFILES_INI" "$PROFILES_INI.backup.${TS}"

        cat <<EOF > "$PROFILES_INI"
[General]
StartWithLastProfile=1
Version=2

[Profile0]
Name=default
IsRelative=1
Path=$PROFIL_CIBLE
Default=1
EOF
        chown "$UTILISATEUR":"$UTILISATEUR" "$PROFILES_INI"
        log_ok "Firefox Snap : profil '$PROFIL_CIBLE' défini comme profil par défaut"
        log_info "Données, extensions et historique Firefox restaurés au prochain lancement"
    fi
else
    log_warn "profiles.ini Firefox Snap non trouvé — Firefox n'a pas encore été lancé ou profil absent"
    log_warn "Si Firefox a déjà été utilisé, vérifier : $SNAP_PROFILES_DIR"
fi

}

################################################################################
# 2c. CONFIGURATION PLOCATE (updatedb.conf)
################################################################################

section_2c() {


cat <<'EOF' > /etc/updatedb.conf
PRUNE_BIND_MOUNTS="no"
PRUNEPATHS="/tmp /var/spool /media /run/media /var/lib/os-prober /var/lib/ceph /home/.ecryptfs /var/lib/schroot"
PRUNEFS="NFS afs binfmt_misc ceph cgroup cgroup2 coda configfs curlftpfs debugfs devfs devpts devtmpfs ecryptfs ftpfs fuse.ceph fuse.cryfs fuse.encfs fuse.glusterfs fuse.gocryptfs fuse.gvfsd-fuse fuse.mfs fuse.rclone fuse.rozofs fuse.sshfs fusectl fusesmb hugetlbfs iso9660 lustre lustre_lite mfs mqueue ncpfs nfs nfs4 ocfs ocfs2 proc pstore rpc_pipefs securityfs shfs smbfs sysfs tmpfs tracefs udev udf usbfs"
EOF

updatedb &
log_ok "/etc/updatedb.conf configuré — indexation lancée en arrière-plan (/run/media ajouté)"
log_info "Si le reboot final interrompt l'indexation, le cron quotidien plocate la refera"


}

################################################################################
# 2d. MONITORING — NRPE, SNMP, CMA CENTREON
################################################################################

section_2d() {


# ─── PARTIE A — NRPE ────────────────────────────────────────────────────────
log_info "Configuration NRPE (nagios-nrpe-server)..."

tee /etc/nagios/nrpe.cfg > /dev/null << 'EOF'
log_facility=daemon
debug=0

pid_file=/run/nagios/nrpe.pid

server_port=5666

nrpe_user=nagios
nrpe_group=nagios

allowed_hosts=192.168.1.50,192.168.122.10,127.0.0.1

dont_blame_nrpe=1
allow_bash_command_substitution=0

command_timeout=60
connection_timeout=300

disable_syslog=0

# Commandes NRPE
command[check_users]=/usr/lib/nagios/plugins/check_users -w 5 -c 10
command[check_load]=/usr/lib/nagios/plugins/check_load -w 7,6,5 -c 9,8,7
command[check_disk_root]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /
command[check_disk_home]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /home
command[check_zombie_procs]=/usr/lib/nagios/plugins/check_procs -w 5 -c 10 -s Z
command[check_total_procs]=/usr/lib/nagios/plugins/check_procs -w 150 -c 200
command[check_procs]=/usr/lib/nagios/plugins/check_procs -w 600 -c 1000
command[check_ssh]=/usr/lib/nagios/plugins/check_ssh -4 -p 22 127.0.0.1
command[check_swap]=/usr/lib/nagios/plugins/check_swap -w 90 -c 10
command[check_mem.sh]=/usr/lib/nagios/plugins/check_mem.sh 80 90
command[check_mem.pl]=/usr/lib/nagios/plugins/check_mem.pl -u -w 80 -c 90
command[check_service]=/usr/lib/nagios/plugins/check_service.sh $ARG1$
command[check_apt]=/usr/lib/nagios/plugins/check_apt
command[check_entropie]=/usr/lib/nagios/plugins/check_entropie.sh
command[check_erreur_dmesg]=/usr/lib/nagios/plugins/check_erreurs_dmesg.sh
command[check_interface_reseau]=/usr/lib/nagios/plugins/check_erreurs_interface.sh -i $ARG1$
command[check_clef_ssh]=/usr/lib/nagios/plugins/check_cles_ssh.sh -u $ARG1$
command[check_sudoers]=/usr/lib/nagios/plugins/check_sudoers.sh
command[check_cron]=/usr/lib/nagios/plugins/check_cron.sh -f $ARG1$
command[check_lvm_vg]=/usr/lib/nagios/plugins/check_lvm.sh -v $ARG1$ -z
command[check_disque_inode]=/usr/lib/nagios/plugins/check_disque_inodes.sh -m $ARG1$
command[check_disque_io]=/usr/lib/nagios/plugins/check_disque_io.sh -d $ARG1$
command[check_disque_smart]=/usr/lib/nagios/plugins/check_disque_smart.sh -d $ARG1$
command[check_grub]=/usr/lib/nagios/plugins/check_grub.sh
command[check_utilisateurs]=/usr/lib/nagios/plugins/check_utilisateurs.sh -u $ARG1$
command[check_nvidia]=/usr/lib/nagios/plugins/check_gpu_nvidia.sh

# Fichiers supplémentaires
include=/etc/nagios/nrpe_local.cfg
include_dir=/etc/nagios/nrpe.d/

# --- NRPE4 : connecteur Centreon (centreon-plugins via NRPE) ---
command[check_centreon_plugins]=/usr/lib/centreon/plugins/centreon_linux_local.pl --plugin=$ARG1$ --mode=$ARG2$ $ARG3$
EOF
log_ok "nrpe.cfg écrit"

log_info "Création des sondes personnalisées Nagios/NRPE..."

tee /usr/lib/nagios/plugins/check_mem.sh > /dev/null << 'EOF'
#!/bin/bash
warning=$1
critical=$2
used=$(free | awk '/Mem:/ {print $3}')
total=$(free | awk '/Mem:/ {print $2}')
pourcentage=$((used * 100 / total))
if [ "$pourcentage" -ge "$critical" ]; then
    echo "CRITICAL - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
    exit 2
elif [ "$pourcentage" -ge "$warning" ]; then
    echo "WARNING - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
    exit 1
else
    echo "OK - RAM utilisée: ${pourcentage}% | ram=${pourcentage}%;${warning};${critical};0;100"
    exit 0
fi
EOF
chmod 755 /usr/lib/nagios/plugins/check_mem.sh

cat > /usr/lib/nagios/plugins/check_service.sh << 'EOF'
#!/bin/bash
# Sonde Centreon - vérification d'un service systemd
# Usage: check_service.sh <nom_du_service>
if [ -z "${1:-}" ]; then
    echo "UNKNOWN ==> Aucun service spécifié"
    exit 3
fi
SERVICE="$1"
# systemctl cat retourne 1 si l'unit n'existe pas (test fiable, sans faux positif grep)
if ! systemctl cat "${SERVICE}.service" &>/dev/null; then
    echo "UNKNOWN ==> service $SERVICE introuvable"
    exit 3
fi
STATE=$(systemctl is-active "$SERVICE")
case "$STATE" in
    active)     echo "OK ==> service $SERVICE UP"; exit 0 ;;
    activating) echo "WARNING ==> service $SERVICE en cours de démarrage"; exit 1 ;;
    *)          echo "CRITICAL ==> service $SERVICE DOWN (état: $STATE)"; exit 2 ;;
esac
EOF
chmod 755 /usr/lib/nagios/plugins/check_service.sh

tee /usr/lib/nagios/plugins/check_mem.pl > /dev/null << 'EOF'
#!/usr/bin/perl -w
use strict;
use Getopt::Std;

my %opts;
getopts('uw:c:', \%opts);

my $warn = $opts{w} || 80;
my $crit = $opts{c} || 90;

open(my $fh, '<', '/proc/meminfo') or die "Impossible de lire /proc/meminfo: $!";
my %mem;
while (<$fh>) { $mem{$1} = $2 if /^(\w+):\s+(\d+)/; }
close($fh);

my $total     = $mem{MemTotal};
my $available = $mem{MemAvailable} || ($mem{MemFree} + ($mem{Buffers}||0) + ($mem{Cached}||0));
my $used      = $opts{u} ? ($total - $available) : ($total - $mem{MemFree});
my $pct       = int($used * 100 / $total);
my $used_mb   = int($used  / 1024);
my $total_mb  = int($total / 1024);
my $perf      = "ram=${pct}%;${warn};${crit};0;100";

if    ($pct >= $crit) { print "CRITICAL - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 2; }
elsif ($pct >= $warn) { print "WARNING  - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 1; }
else                  { print "OK       - RAM: ${pct}% (${used_mb}Mo/${total_mb}Mo) | $perf\n"; exit 0; }
EOF
chmod 755 /usr/lib/nagios/plugins/check_mem.pl

log_ok "Sondes check_mem.sh, check_mem.pl, check_service.sh créées"

mkdir -p /etc/default
echo "NRPE_OPTS=''" > /etc/default/nagios-nrpe-server
systemctl daemon-reload \
    || log_warn "daemon-reload échoué (section nrpe)"
systemctl restart nagios-nrpe-server \
    || log_warn "nagios-nrpe-server : redémarrage échoué"

log_ok "Warning NRPE_OPTS retiré"

systemctl enable --now nagios-nrpe-server >> "$LOG_FILE" 2>&1 \
    && log_ok "nagios-nrpe-server activé et démarré (port 5666)" \
    || log_warn "nagios-nrpe-server : activation échouée"

# ─── PARTIE B — SNMP ────────────────────────────────────────────────────────
log_info "Configuration snmpd..."

cat > /etc/snmp/snmpd.conf << 'EOF'
sysLocation    Home Lab
sysContact     utilisateur
sysServices    72
master agentx
agentaddress udp:161
view   systemonly  included   .1.3.6.1.2.1.1
view   systemonly  included   .1.3.6.1.2.1.25.1
rocommunity  public 192.168.122.0/24
rocommunity  public default -V systemonly
rocommunity6 public default -V systemonly
rouser authPrivUser authpriv -V systemonly
disk / 10%
disk /home 10%
disk /boot 10%
includeDir /etc/snmp/snmpd.conf.d
EOF

systemctl enable snmpd >> "$LOG_FILE" 2>&1 || log_warn "snmpd : activation échouée"
systemctl restart snmpd >> "$LOG_FILE" 2>&1 || log_warn "snmpd : redémarrage échoué"
log_ok "snmpd configuré"

# ─── PARTIE C — CMA (Centreon Monitoring Agent) — mode TLS ──────────────────
log_info "Installation Centreon Monitoring Agent (CMA) en mode TLS..."

apt install -y lsb-release ca-certificates apt-transport-https \
    software-properties-common curl gpg gnupg2 wget \
    || log_warn "CMA : certaines dépendances ont échoué"

log_info "Ajout dépôt Centreon (bookworm forcé — seul codename supporté)..."
rm -f /etc/apt/sources.list.d/centreon.list /etc/apt/sources.list.d/centreon-plugins.list
# Nettoyage de l'ancienne clé à confiance globale (méthode dépréciée trusted.gpg.d)
rm -f /etc/apt/trusted.gpg.d/centreon.gpg

# Clé GPG dans un keyring dédié, référencé par signed-by= dans les dépôts
# (évite la confiance globale de trusted.gpg.d, cohérent avec Docker/Steam/Brave)
wget -q -O- https://apt-key.centreon.com \
    | gpg --dearmor \
    | tee /usr/share/keyrings/centreon-archive-keyring.gpg > /dev/null \
    && log_ok "Clé GPG Centreon ajoutée (/usr/share/keyrings/centreon-archive-keyring.gpg)" \
    || log_warn "CMA : ajout clé GPG Centreon échoué"
chmod a+r /usr/share/keyrings/centreon-archive-keyring.gpg

# Dépôt 25.10 (v8.9.4) : l'agent DOIT être aligné sur la version du serveur —
# un agent 24.10 sur serveur 25.10 se connecte mais l'auto-création d'hôte
# (create_host_auto) ne fonctionne pas (métadonnées host_template/ips absentes
# du protocole 24.10, vérifié au test réel). Nouveau format d'URL en 25.10 :
# suite « bookworm-25.10-stable » sous apt-standard/ (l'ancien
# apt-standard-25.10-stable renvoie 404).
tee /etc/apt/sources.list.d/centreon.list > /dev/null << 'EOF'
deb [signed-by=/usr/share/keyrings/centreon-archive-keyring.gpg] https://packages.centreon.com/apt-standard/ bookworm-25.10-stable main
EOF

tee /etc/apt/sources.list.d/centreon-plugins.list > /dev/null << 'EOF'
deb [signed-by=/usr/share/keyrings/centreon-archive-keyring.gpg] https://packages.centreon.com/apt-plugins-stable/ bookworm main
EOF

apt_update_silent || log_warn "CMA : apt update après dépôt Centreon échoué"

apt install -y centreon-monitoring-agent centreon-plugin-operatingsystems-linux-local \
    && log_ok "centreon-monitoring-agent installé" \
    || log_warn "CMA : installation échouée — vérifier le dépôt Centreon"

# sudo-rs (Ubuntu 25.10+) ne connaît pas « requiretty » : le fichier sudoers
# livré par centreon-plugin-operatingsystems-linux-sudoers (dépendance des
# plugins) déclenche un warning à CHAQUE sudo — non bloquant mais bruyant.
# La ligne est inutile (requiretty n'est plus un défaut depuis sudo 1.8.12)
# → commentée. Pas un conffile : une mise à jour du paquet peut la restaurer,
# d'où ce sed idempotent (v8.9.7).
SUDOERS_CENTREON="/etc/sudoers.d/sudoersCentreonLinuxPlugins"
if [[ -f "$SUDOERS_CENTREON" ]] && grep -q '^Defaults:.*!requiretty' "$SUDOERS_CENTREON"; then
    sed -i 's|^Defaults:\(.*\)!requiretty|# requiretty inconnu de sudo-rs — commenté par postinstall\n# Defaults:\1!requiretty|' "$SUDOERS_CENTREON"
    visudo -cf "$SUDOERS_CENTREON" >/dev/null 2>&1 \
        && log_ok "sudoersCentreonLinuxPlugins : ligne requiretty commentée (compat sudo-rs)" \
        || log_warn "sudoersCentreonLinuxPlugins : visudo invalide après modification — à vérifier"
fi

mkdir -p /etc/centreon-monitoring-agent

# Certificat TLS public du serveur Centreon (mode encryption: full)
# Copie littérale de /etc/centreon-monitoring-agent/centreon-agent.crt (prod)
cat > /etc/centreon-monitoring-agent/centreon-agent.crt << 'EOF'
-----BEGIN CERTIFICATE-----
MIIDIjCCAgqgAwIBAgIUFnsEM0CuM8J2OvpkRg36ZzgOqUIwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIY2VudHJlb24wHhcNMjYwNTMxMDk0NDIxWhcNMjcwNTMx
MDk0NDIxWjATMREwDwYDVQQDDAhjZW50cmVvbjCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBALznDxbTRMqWy8suODUr1o+TOzNn6f8/BHvW3Q2zKWIZg4V8
9df3aqO3jt0bUsnPhSQ4UjnCFxxLeM9HYPW5+oEwxre90rqR6QliTT/qsq4WHCPT
1EiDuwfclIB/obAPcYBM9aji4XaCBJyAhqsycmrvf1SSAYP7gUszD8ykieJVSqNk
U3MBSa4Azhyg44ObeGd2qCmtQQ3+Dp2jOmVFRKDpp6dSe4z8juizqoVYb2wMC3Z8
8OOiYwNUF3Bwauf0wCQm6VBDWA/0WW9Hs5y/iZWi8m0AH4lGsFLPl6drXIoA8SQd
wlLoZWSKAUo3RKwXB+wBIxRq7PxHyVliKPcJn/MCAwEAAaNuMGwwHQYDVR0OBBYE
FOBHjBpRhhGyZ6Z/Afj588QzTWt0MB8GA1UdIwQYMBaAFOBHjBpRhhGyZ6Z/Afj5
88QzTWt0MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0RBBIwEIIIY2VudHJlb26HBMCo
AS4wDQYJKoZIhvcNAQELBQADggEBAGni5tTfokCD+XcJK/HVB4A9D5V/Uvyel+Jg
88xOp3bnPNM4n82wlT7GtTtpr2yXM5LFBMxI8xNNIYtmmmJc/lghdIC/7gWZkkMG
wKBXyCNpjefSpw4TVfrEKoQZ5QndUmAfoUGd/uDmUXk9fgMbrzzrPaSmNluYtdF7
SUjnV61xOCdJSI3UHg9jZeaHeDqiKI+mRdeMCKXEgOqdcm+BR/nLN10woLynJyY4
zGm6Lk7A6Z7qLMusWvBW9zC9paV8JTme0XnXqAa/FS2lv0T9D0adEjmTPTclOlEt
l+MTXLrzbj1wBvpYZoINQAfJfk8gzMV7SPG+7F+pNZF7c1jsAHI=
-----END CERTIFICATE-----
EOF
chmod 644 /etc/centreon-monitoring-agent/centreon-agent.crt
log_ok "centreon-agent.crt installé (certificat TLS)"

# Le certificat CMA est référencé directement par centagent.json (clé ca_certificate) :
# inutile de l'ajouter au store système (cela ferait confiance à une CA "centreon"
# pour TOUT le TLS du système). On purge l'éventuel résidu d'une exécution antérieure.
if [ -f /usr/local/share/ca-certificates/centreon-cma.crt ]; then
    rm -f /usr/local/share/ca-certificates/centreon-cma.crt
    update-ca-certificates --fresh >> "$LOG_FILE" 2>&1 \
        && log_ok "Ancienne CA Centreon retirée du store système (confiance TLS réduite)" \
        || log_warn "update-ca-certificates --fresh (purge) échoué"
fi

# Grep ancré : matche uniquement une entrée 'centreon' isolée (mot complet)
if ! grep -qE '^[^#]\S*\s+centreon(\s|$)' /etc/hosts; then
    echo "192.168.1.50  centreon" >> /etc/hosts
    log_ok "/etc/hosts : entrée 'centreon' (192.168.1.50) ajoutée"
else
    log_info "/etc/hosts : entrée 'centreon' déjà présente"
fi

# umask 077 : centagent.json contient le token JWT (secret) — création en 600 puis 640
(
    umask 077
    cat > /etc/centreon-monitoring-agent/centagent.json << EOF
{
  "log_level": "debug",
  "endpoint": "${CMA_ENDPOINT}",
  "host": "${CMA_HOST}",
  "log_type": "file",
  "log_file": "/var/log/centreon-monitoring-agent/centagent.log",
  "encryption": "full",
  "ca_certificate": "/etc/centreon-monitoring-agent/centreon-agent.crt",
  "token": "${CMA_TOKEN}",
  "plugins_directory": "/usr/lib/centreon/plugins/src",
  "custom_check_file": "/etc/centreon-monitoring-agent/custom_checks.cfg"
}
EOF
)
# Groupe centreon-monitoring-agent (v8.9.3) : le service tourne sous cet
# utilisateur (User= du .service) — avec root:root 640 le fichier était
# ILLISIBLE pour lui → centagent en échec « Permission denied » au test réel
chown root:centreon-monitoring-agent /etc/centreon-monitoring-agent/centagent.json \
    || log_warn "centagent.json : chown root:centreon-monitoring-agent échoué (CMA non installé ?)"
chmod 640 /etc/centreon-monitoring-agent/centagent.json
log_ok "centagent.json configuré (token depuis .conf)"

tee /etc/centreon-monitoring-agent/custom_checks.cfg > /dev/null << 'EOF'
[custom_checks]
test_ok = /usr/local/bin/cma_test_ok.sh
test_ok_2 = /usr/local/bin/cma_test_ok.sh
test_ok_3 = /usr/local/bin/cma_test_ok.sh
test_ok_4 = /usr/local/bin/cma_test_ok.sh
test_ok_5 = /usr/local/bin/cma_test_ok.sh
check_service = /usr/lib/nagios/plugins/check_service.sh $ARG1$
check_service_ssh = /usr/lib/nagios/plugins/check_service.sh ssh
EOF
log_ok "custom_checks.cfg configuré"

tee /usr/local/bin/cma_test_ok.sh > /dev/null << 'EOF'
#!/bin/bash
echo "OK - CMA custom script works"
exit 0
EOF
chmod +x /usr/local/bin/cma_test_ok.sh
log_ok "cma_test_ok.sh créé"

systemctl enable --now centagent \
    && log_ok "CMA centagent activé et démarré" \
    || log_warn "centagent : activation échouée — vérifier /var/log/centreon-monitoring-agent/centagent.log"

systemctl is-active --quiet centagent \
    && log_ok "CMA centagent actif" \
    || log_warn "CMA centagent inactif — vérifier que le serveur Centreon est joignable sur centreon:4317"

log_ok "Section monitoring terminée — NRPE (port 5666) + SNMP + CMA configurés"

}

################################################################################
# 2e. CONFIGURATION SUPERVISION CENTREON — GROUPE, PERMISSIONS, SUDOERS
################################################################################

section_2e() {


# ─── Création du groupe monitoring ──────────────────────────────────────────
if ! getent group monitoring > /dev/null 2>&1; then
    groupadd monitoring \
        && log_ok "Groupe monitoring créé" \
        || log_warn "Groupe monitoring : groupadd échoué"
else
    log_info "Groupe monitoring déjà présent"
fi

# ─── Ajout nagios au groupe monitoring ──────────────────────────────────────
# tr + grep -qx (v8.9.10) : avec grep -qw, le tiret est une frontière de mot
# → « monitoring » matchait DANS « centreon-monitoring-agent » (groupe ajouté
# par le postinst CMA) : faux positif, usermod jamais exécuté, sudoers
# %monitoring inopérant au test réel (« nagios is not allowed to run sudo »).
if id nagios > /dev/null 2>&1; then
    if id -nG nagios | tr ' ' '\n' | grep -qx monitoring; then
        log_info "nagios déjà dans le groupe monitoring"
    else
        usermod -aG monitoring nagios \
            && log_ok "nagios ajouté au groupe monitoring" \
            || log_warn "nagios : usermod -aG monitoring échoué"
    fi
else
    log_warn "Utilisateur nagios absent — NRPE non installé ?"
fi

# ─── Ajout centreon-monitoring-agent au groupe monitoring ───────────────────
if id centreon-monitoring-agent > /dev/null 2>&1; then
    # tr + grep -qx : même faux positif que nagios (le groupe primaire
    # « centreon-monitoring-agent » contient le mot « monitoring »)
    if id -nG centreon-monitoring-agent | tr ' ' '\n' | grep -qx monitoring; then
        log_info "centreon-monitoring-agent déjà dans le groupe monitoring"
    else
        usermod -aG monitoring centreon-monitoring-agent \
            && log_ok "centreon-monitoring-agent ajouté au groupe monitoring" \
            || log_warn "centreon-monitoring-agent : usermod -aG monitoring échoué"
    fi
else
    log_warn "Utilisateur centreon-monitoring-agent absent — CMA non installé ?"
fi

# ─── Répertoire de cache Centreon ───────────────────────────────────────────
mkdir -p /var/cache/centreon
chown nagios\:monitoring /var/cache/centreon \
    || log_warn "/var/cache/centreon : chown nagios:monitoring échoué"
chmod 770 /var/cache/centreon
log_ok "/var/cache/centreon créé (nagios:monitoring, 770)"

# ─── Fichier sudoers monitoring-plugins-custom ──────────────────────────────
MONITORING_SUDOERS="/etc/sudoers.d/monitoring-plugins-custom"
log_info "Création ${MONITORING_SUDOERS}..."

# Validation visudo AVANT installation (même pattern que la section 9) :
# un fichier malformé déposé directement dans /etc/sudoers.d/ casserait TOUT sudo.
MONITORING_TMP=$(mktemp)
cat > "${MONITORING_TMP}" << 'SUDOERS_EOF'
# Droits sudo pour les sondes Centreon custom - Romain J.
# Groupe monitoring : nagios + centreon-monitoring-agent
# Généré par postinstall.26.04_v2.sh
#
# Principe : chaque commande est restreinte aux arguments strictement nécessaires
# pour éviter une privilege escalation en cas de compromis d'un compte du groupe.
#
# Limite connue : dans sudoers, le joker * matche aussi les espaces — il
# autorise donc des arguments supplémentaires. Accepté : binaires en lecture
# seule, usage personnel ; un durcissement exigerait des chemins de
# périphériques explicites, non portables.
#
# Compatibilité sudo-rs (sudo par défaut d'Ubuntu 25.10+, v8.9.3) :
#   - joker PARTIEL interdit dans les arguments (/dev/* rejeté, * seul accepté)
#     → smartctl/mdadm passent de « /dev/* » à « * » (delta sécurité minime :
#     l'ancien * matchait déjà les espaces, cf. limite ci-dessus)
#   - « : » séparateur de liste → échappé dans « chown nagios\:monitoring »
# Syntaxes validées par visudo -cf (sudo-rs 0.2.13) sur Ubuntu 26.04 réel.

# check_grub.sh — seulement le hash de la config grub
%monitoring ALL=(root) NOPASSWD: /usr/bin/sha256sum /boot/grub/grub.cfg
%monitoring ALL=(root) NOPASSWD: /usr/bin/sha256sum /boot/grub2/grub.cfg

# check_lvm.sh — lecture seule (vgs/lvs sans options dangereuses)
%monitoring ALL=(root) NOPASSWD: /sbin/vgs "", /sbin/vgs -o *
%monitoring ALL=(root) NOPASSWD: /sbin/lvs "", /sbin/lvs -o *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/vgs "", /usr/sbin/vgs -o *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/lvs "", /usr/sbin/lvs -o *

# check_sudoers.sh — uniquement la validation
%monitoring ALL=(root) NOPASSWD: /usr/sbin/visudo -c

# check_disque_smart.sh — lecture seule (info/health/attributs)
%monitoring ALL=(root) NOPASSWD: /usr/sbin/smartctl -H *, /usr/sbin/smartctl -i *, /usr/sbin/smartctl -A *, /usr/sbin/smartctl --all *

# check_raid.sh — lecture seule (detail/examine)
%monitoring ALL=(root) NOPASSWD: /sbin/mdadm --detail *, /sbin/mdadm --examine *
%monitoring ALL=(root) NOPASSWD: /usr/sbin/mdadm --detail *, /usr/sbin/mdadm --examine *

# check_pare_feu.sh et check_statut_pare_feu.sh — lecture seule
%monitoring ALL=(root) NOPASSWD: /usr/sbin/nft list ruleset, /usr/sbin/nft -j list ruleset, /usr/sbin/nft list table *
%monitoring ALL=(root) NOPASSWD: /sbin/iptables -L, /sbin/iptables -L -n, /sbin/iptables -L -n -v
%monitoring ALL=(root) NOPASSWD: /sbin/iptables-save

# /var/cache/centreon (création si absent) — chemin fixe, pas de wildcard
# « \: » : sudo-rs traite « : » comme séparateur de liste s'il n'est pas échappé
%monitoring ALL=(root) NOPASSWD: /bin/mkdir -p /var/cache/centreon
%monitoring ALL=(root) NOPASSWD: /bin/chown nagios\:monitoring /var/cache/centreon
SUDOERS_EOF

# ─── Validation puis installation atomique ──────────────────────────────────
if visudo -cf "${MONITORING_TMP}" >/dev/null 2>&1; then
    install -m 0440 -o root -g root "${MONITORING_TMP}" "${MONITORING_SUDOERS}"
    log_ok "${MONITORING_SUDOERS} installé (chmod 440, syntaxe validée par visudo)"
else
    log_error "Syntaxe sudoers invalide — ${MONITORING_SUDOERS} non installé (sudo préservé)"
fi
rm -f "${MONITORING_TMP}"

log_ok "Section supervision Centreon terminée"


}

################################################################################
# 2f. ALERTES SMART PAR MAIL (SMARTD)
################################################################################

section_2f() {

# smartd (smartmontools, installé en section 2) surveille la santé SMART des
# disques mais sa conf par défaut alerte root en local — on alerte GMAIL_USER
# via le relay postfix (section 22). smartd-runner est le mécanisme Debian
# standard de dispatch des alertes (mail + scripts de /etc/smartmontools/run.d).
if [ -f /etc/smartd.conf ]; then
    cp /etc/smartd.conf "/etc/smartd.conf.backup.${TS}"
    LIGNE_DEVICESCAN="DEVICESCAN -d removable -n standby -m ${GMAIL_USER} -M exec /usr/share/smartmontools/smartd-runner"
    if grep -qxF "$LIGNE_DEVICESCAN" /etc/smartd.conf; then
        log_ok "smartd : DEVICESCAN déjà configuré pour ${GMAIL_USER}"
    elif grep -qE '^DEVICESCAN' /etc/smartd.conf; then
        sed -i -E "s|^DEVICESCAN.*|${LIGNE_DEVICESCAN}|" /etc/smartd.conf
        log_ok "smartd : DEVICESCAN remplacé (alertes mail → ${GMAIL_USER})"
    else
        echo "$LIGNE_DEVICESCAN" >> /etc/smartd.conf
        log_ok "smartd : DEVICESCAN ajouté (alertes mail → ${GMAIL_USER})"
    fi
    systemctl restart smartmontools 2>/dev/null \
        || systemctl restart smartd 2>/dev/null \
        || log_warn "smartd : redémarrage échoué — vérifier systemctl status smartmontools"
    log_ok "Alertes SMART par mail configurées (panne disque imminente → ${GMAIL_USER})"
else
    log_warn "/etc/smartd.conf absent — smartmontools non installé ? Alertes SMART ignorées"
fi

}

################################################################################
# 2g. ATTACHEMENT UBUNTU PRO (ESM-INFRA, ESM-APPS, LIVEPATCH)
# Le client `pro` (ubuntu-advantage-tools) est préinstallé sur Ubuntu 26.04.
# L'attachement souscrit la machine au contrat Ubuntu Pro et débloque les
# correctifs de sécurité étendus (ESM) et le patch de noyau à chaud (Livepatch).
################################################################################

section_2g() {

# Section sautée proprement si le token n'est pas renseigné (rétro-compatibilité
# avec les .conf antérieurs + machines sans abonnement Pro) — variable non
# critique (absente volontairement des VARS_CRITIQUES)
if [[ -z "${PRO_TOKEN:-}" || "${PRO_TOKEN}" == *PURGE* ]]; then
    log_warn "PRO_TOKEN absent/non renseigné dans le .conf — Ubuntu Pro non configuré"
    return 0
fi

# Client `pro` préinstallé sur Ubuntu 26.04 — installation de rattrapage sinon
if ! command -v pro &>/dev/null; then
    apt install -y ubuntu-advantage-tools \
        || { log_warn "ubuntu-advantage-tools : installation échouée — Ubuntu Pro ignoré"; return 0; }
fi

# Idempotence : ne pas réattacher une machine déjà attachée — `pro attach`
# échouerait avec « This machine is already attached »
if pro status --format json 2>/dev/null | grep -qE '"attached":[[:space:]]*true'; then
    log_ok "Ubuntu Pro : machine déjà attachée — pas de réattachement"
else
    if pro attach "$PRO_TOKEN" >/dev/null 2>&1; then
        log_ok "Ubuntu Pro : machine attachée au contrat"
    else
        log_warn "Ubuntu Pro : attachement échoué — vérifier le token et la connectivité (pro attach ...)"
        return 0
    fi
fi

# Activation explicite des services (l'attach auto-active déjà les services par
# défaut : on confirme/complète, idempotent — un service déjà actif renvoie un
# code non nul inoffensif consigné en INFO)
for service in esm-infra esm-apps livepatch; do
    if pro enable "$service" --assume-yes >/dev/null 2>&1; then
        log_ok "Ubuntu Pro : $service activé"
    else
        log_info "Ubuntu Pro : $service déjà actif ou indisponible sur ce contrat/noyau"
    fi
done

# Récapitulatif lisible dans le journal
pro status 2>/dev/null | grep -E 'esm-infra|esm-apps|livepatch' || true

}

################################################################################
# 3. CONFIGURATION DOCKER, KVM ET GROUPES
################################################################################

section_3() {


usermod -aG docker "$UTILISATEUR" || log_warn "Groupe docker absent"
usermod -aG libvirt "$UTILISATEUR" 2>/dev/null || log_warn "Groupe libvirt absent"
usermod -aG kvm "$UTILISATEUR" || log_warn "Groupe kvm absent"

systemctl enable --now docker || log_warn "Docker service non démarré"

systemctl enable --now libvirtd 2>/dev/null \
    || systemctl enable --now virtqemud 2>/dev/null \
    || log_warn "libvirtd/virtqemud : aucun service trouvé"

virsh net-autostart default 2>/dev/null || true
virsh net-start default 2>/dev/null || true

# ==============================
# Restauration VM Centreon KVM
# ==============================

VM_NAME="centreon_25.10_debian12"
VM_XML="/home/$UTILISATEUR/kvm/debian12/centreon_25.10_debian12.xml"

# --- Définition de la VM si elle n'existe pas déjà
# log_info si XML absent (v8.9.12) : seules les machines hébergeant la VM
# Centreon possèdent ce fichier dans leur /home — son absence sur les autres
# n'est pas une anomalie (elle polluait le rapport final au test réel)
if [ -f "$VM_XML" ]; then
    virsh list --all | grep -q "$VM_NAME" || \
        virsh define "$VM_XML" || log_warn "virsh define $VM_XML échoué"
    virsh autostart "$VM_NAME" || log_warn "virsh autostart $VM_NAME échoué"
else
    log_info "Pas de VM Centreon à restaurer sur cette machine ($VM_XML absent)"
fi

# --- Activation service libvirt-guests
systemctl enable libvirt-guests || log_warn "libvirt-guests : activation échouée"
systemctl start libvirt-guests || log_warn "libvirt-guests : démarrage échoué"

# --- Configuration arrêt propre des VM au shutdown host
LIBVIRT_CONF_FILE="/etc/default/libvirt-guests"

# Idempotent : supprimer toute ligne ON_SHUTDOWN (commentée ou non, quelle que soit la valeur)
# puis ajouter notre directive. Évite les doublons sur ré-exécution.
if [ -f "$LIBVIRT_CONF_FILE" ]; then
    sed -i -E '/^[[:space:]]*#?[[:space:]]*ON_SHUTDOWN=/d' "$LIBVIRT_CONF_FILE"
    echo 'ON_SHUTDOWN=shutdown' >> "$LIBVIRT_CONF_FILE"
    log_ok "libvirt-guests : ON_SHUTDOWN=shutdown configuré"
else
    log_warn "$LIBVIRT_CONF_FILE introuvable — libvirt-daemon-system non installé ?"
fi

log_ok "Docker, KVM et groupes configurés"

}

################################################################################
# 4. CONFIGURATION RÉSEAU : IP STATIQUE (DHCP FIGÉ) + DNS
################################################################################

section_4() {


# ---- Passage de l'IP DHCP en statique + DNS (v8.9.15, demande Romain) ----
# L'IP obtenue par DHCP à l'installation est figée : l'adresse ne changera
# plus au gré des baux (indispensable pour Centreon, SSH, sondes…).
# Deux chemins selon l'origine de la connexion :
#   - « netplan-* » (installeur subiquity 26.04) : les nmcli modify NE
#     PERSISTENT PAS (la connexion est régénérée depuis /etc/netplan/*.yaml,
#     vérifié au test réel : rc=0 mais method reste auto) → fichier de
#     surcharge 99-postinstall-ip-statique.yaml (IP + passerelle + DNS)
#     + netplan apply. L'adresse étant identique au bail, pas de coupure.
#   - connexion NM native : nmcli modify + connection up en place.
NETPLAN_STATIQUE="/etc/netplan/99-postinstall-ip-statique.yaml"
ip_cidr=$(nmcli -g IP4.ADDRESS connection show "$CONNEXION_NET" 2>/dev/null | head -1)
passerelle=$(nmcli -g IP4.GATEWAY connection show "$CONNEXION_NET" 2>/dev/null | head -1)
device_net=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | grep -F "$CONNEXION_NET:" | cut -d: -f2 | head -1)

if [[ -f "$NETPLAN_STATIQUE" ]]; then
    log_ok "IP statique déjà configurée ($NETPLAN_STATIQUE présent) : $ip_cidr"
elif [[ -z "$ip_cidr" || -z "$passerelle" || -z "$device_net" ]]; then
    log_warn "IP statique : IP/passerelle/interface introuvables — connexion laissée en DHCP"
elif [[ "$CONNEXION_NET" == netplan-* ]]; then
    cat > "$NETPLAN_STATIQUE" << YAML
# Généré par postinstall.26.04_v2.sh — IP du bail DHCP figée en statique
# (surcharge le dhcp4: true de l'installeur, fusion netplan par ordre lexical)
network:
  version: 2
  renderer: NetworkManager
  ethernets:
    $device_net:
      dhcp4: false
      dhcp6: false
      addresses: [$ip_cidr]
      routes:
        - to: default
          via: $passerelle
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
YAML
    chmod 600 "$NETPLAN_STATIQUE"
    if netplan generate 2>/dev/null && netplan apply 2>/dev/null; then
        log_ok "IP DHCP figée en statique via netplan : $ip_cidr (passerelle $passerelle, DNS 8.8.8.8/8.8.4.4)"
    else
        rm -f "$NETPLAN_STATIQUE"
        log_warn "IP statique : netplan generate/apply échoué — fichier retiré, connexion laissée en DHCP"
    fi
else
    nmcli connection modify "$CONNEXION_NET" \
        ipv4.method manual \
        ipv4.addresses "$ip_cidr" \
        ipv4.gateway "$passerelle" \
        ipv4.dns "8.8.8.8 8.8.4.4" \
        ipv4.ignore-auto-dns yes \
        && log_ok "IP DHCP figée en statique via nmcli : $ip_cidr (passerelle $passerelle)" \
        || log_warn "IP statique : nmcli modify échoué — connexion laissée en DHCP"
    # Réactivation EN PLACE (up sans down) : applique IP + DNS sans coupure
    # (l'adresse figée est identique à celle du bail DHCP)
    nmcli connection up "$CONNEXION_NET" >/dev/null 2>&1 \
        || log_warn "nmcli : réactivation $CONNEXION_NET échouée"
fi

# Contrôle de résolution avant de poursuivre
if getent hosts google.com >/dev/null 2>&1; then
    log_ok "DNS Google configurés et résolution opérationnelle"
else
    log_warn "DNS configurés mais résolution KO — vérifier la connexion réseau"
fi

}

################################################################################
# 5. MONTAGE NAS VIA FSTAB
################################################################################

section_5() {


# umask 077 : credentials CIFS créés directement en 600 (pas de fenêtre 644)
(
    umask 077
    # printf %s : aucune interprétation de $, ` ou \ dans les valeurs
    # (un heredoc non quoté corromprait/exécuterait un mot de passe contenant ces caractères)
    {
        printf 'username=%s\n' "$NAS_USER"
        printf 'password=%s\n' "$NAS_PASSWORD"
    } > "$NAS_CREDENTIALS"
)
chown root:root "$NAS_CREDENTIALS"

added_count=0
for dossier in "${NAS_PARTAGES[@]}"; do
    MOUNT_LINE="//${NAS_IP}/${dossier} /mnt/nas/${dossier} cifs credentials=${NAS_CREDENTIALS},iocharset=utf8,vers=3.1.1,uid=${USER_UID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.mount-timeout=5 0 0"
    if ! grep -q "^//${NAS_IP}/${dossier}[[:space:]]" /etc/fstab; then
        echo "$MOUNT_LINE" >> /etc/fstab
        added_count=$((added_count + 1))
    fi
done

if [ $added_count -gt 0 ]; then
    log_ok "$added_count montages NAS ajoutés à fstab"
else
    log_ok "Montages NAS déjà présents"
fi

log_info "Création des dossiers /mnt/nas..."
# Généré depuis NAS_PARTAGES (source unique de vérité — plus de liste en dur)
mkdir -p /mnt/nas
for dossier in "${NAS_PARTAGES[@]}"; do
    mkdir -p "/mnt/nas/${dossier}"
done
# Note : sur CIFS, chmod/chown ne sont pas persistants (cf. options uid/gid de fstab)
chown "$UTILISATEUR":"$UTILISATEUR" /mnt/nas/* 2>/dev/null || true
log_ok "Dossiers /mnt/nas créés (${#NAS_PARTAGES[@]} partages, depuis NAS_PARTAGES)"

}

################################################################################
# 6. CONFIGURATION GRUB DUAL-BOOT
################################################################################

section_6() {


cp /etc/default/grub "/etc/default/grub.backup.${TS}"

# Patch idempotent : test explicite avant sed.
# sed -i retourne 0 même si aucune substitution n'est faite → le `|| echo >>`
# n'était jamais déclenché si la directive était absente/commentée (bug v8.8.22).
patch_grub() {
    local key="$1" value="$2"
    if grep -qE "^${key}=" /etc/default/grub; then
        sed -i "s|^${key}=.*|${key}=${value}|" /etc/default/grub
    else
        echo "${key}=${value}" >> /etc/default/grub
    fi
}
patch_grub GRUB_TIMEOUT_STYLE menu
patch_grub GRUB_TIMEOUT 10
patch_grub GRUB_DISABLE_OS_PROBER false

update-grub || log_warn "update-grub échoué — vérifier la configuration GRUB"

log_ok "GRUB configuré pour dual-boot"

}

################################################################################
# 7. ÉDITEUR PAR DÉFAUT
################################################################################

section_7() {


if update-alternatives --set editor /usr/bin/vim.basic 2>/dev/null; then
    log_ok "Éditeur par défaut : vim.basic"
elif update-alternatives --set editor /usr/bin/vim 2>/dev/null; then
    log_ok "Éditeur par défaut : vim"
else
    VIM_ALT=$(update-alternatives --list editor 2>/dev/null | grep -i vim | head -1 || true)
    if [ -n "$VIM_ALT" ]; then
        update-alternatives --set editor "$VIM_ALT"
        log_ok "Éditeur par défaut : $VIM_ALT"
    else
        log_warn "vim non trouvé dans les alternatives, configuration ignorée"
    fi
fi

}

################################################################################
# 8. CONFIGURATION .BASHRC + ALIASES
# Stratégie : backup horodaté + écrasement intégral
#   - Backup .bashrc.backup.<TS> et .bash_aliases.backup.<TS> si existants
#   - Réécriture intégrale des fichiers user (cat >)
#   - Nettoyage : suppression .bashrc.postinstall / .bash_aliases.postinstall
#     (résidus éventuels de v8.8.19 — stratégie abandonnée en v8.8.20)
#   - chown UTILISATEUR + chmod 644
################################################################################

section_8() {


# ALIASES_USER et non BASH_ALIASES : BASH_ALIASES est un tableau associatif
# RÉSERVÉ de bash (il alimente les alias du shell) — l'écraser fonctionnait par
# chance (assignation implicite à l'élément [0]) mais shellcheck SC2178 a raison
ALIASES_USER="/home/$UTILISATEUR/.bash_aliases"

# ---- Backup horodaté si fichiers existants (TS global C2 v8.8.24) ----
if [ -f "$BASHRC_USER" ]; then
    cp "$BASHRC_USER" "${BASHRC_USER}.backup.${TS}"
    log_info "Backup : ${BASHRC_USER}.backup.${TS}"
fi
if [ -f "$ALIASES_USER" ]; then
    cp "$ALIASES_USER" "${ALIASES_USER}.backup.${TS}"
    log_info "Backup : ${ALIASES_USER}.backup.${TS}"
fi

# ---- Nettoyage des résidus v8.8.19 (stratégie .postinstall abandonnée) ----
rm -f "/home/$UTILISATEUR/.bashrc.postinstall" "/home/$UTILISATEUR/.bash_aliases.postinstall"

# ---- 1) Écriture intégrale de ~/.bashrc ----
cat > "$BASHRC_USER" << 'EOF'
# ~/.bashrc : géré par postinstall.26.04_v2.sh
# (réécrit à chaque exécution du script — backup horodaté dans ~/.bashrc.backup.*)

# Shells interactifs uniquement
case $- in
    *i*) ;;
      *) return;;
esac

# ==== HISTORIQUE BASH OPTIMISÉ ====
export HISTTIMEFORMAT="%F %T "
export HISTSIZE=500000
export HISTFILESIZE=1000000
export HISTCONTROL=ignoreboth:erasedups
export HISTIGNORE="ls:ll:la:l:pwd:cd:cd -:exit:clear:history*:bg:fg:jobs"

shopt -s histappend histverify checkwinsize globstar autocd direxpand nocaseglob
PROMPT_COMMAND='history -a; history -r'

[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# Prompt dynamique avec info Git
# \001/\002 émis directement (équivalents internes de \[ \]) : bash décode les
# séquences \[ \] de PS1 AVANT l'expansion de $(prompt_git) — la sortie d'une
# substitution de commande n'est jamais re-décodée, echo -e "\[" affichait
# des \[ littéraux dans le prompt.
prompt_git() {
    git rev-parse --is-inside-work-tree &>/dev/null || return
    local branch
    branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
    [ -z "$branch" ] && return
    if git diff --quiet 2>/dev/null; then
        printf '\001\033[0;32m\002 [%s]' "$branch"
    else
        printf '\001\033[0;31m\002 [%s]' "$branch"
    fi
}

if [ -x /usr/bin/tput ] && tput setaf 1 >/dev/null 2>&1; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w$(prompt_git)\[\033[00m\]\$ '
else
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(prompt_git)\$ '
fi

case "$TERM" in
xterm*|rxvt*)
    PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
    ;;
esac

# Couleurs ls/grep
if [ -x /usr/bin/dircolors ]; then
    test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
    alias ls='ls --color=auto'
    alias grep='grep --color=auto -n'
    alias fgrep='fgrep --color=auto'
    alias egrep='egrep --color=auto'
fi

alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

# Bash completion
if ! shopt -oq posix; then
    if [ -f /usr/share/bash-completion/bash_completion ]; then
        . /usr/share/bash-completion/bash_completion
    elif [ -f /etc/bash_completion ]; then
        . /etc/bash_completion
    fi
fi

export PATH="$PATH:$HOME/.local/bin:$HOME/.npm-global/bin"

# Fortune + cowsay aléatoire
if command -v fortune &>/dev/null && command -v cowsay &>/dev/null; then
    shopt -s nullglob
    cow_files=(/usr/share/cowsay/cows/*.cow)
    shopt -u nullglob
    if (( ${#cow_files[@]} > 0 )); then
        selected_cow="${cow_files[RANDOM % ${#cow_files[@]}]}"
        fortune fr 2>/dev/null | cowsay -f "${selected_cow##*/}" | lolcat
    else
        fortune fr 2>/dev/null | lolcat
    fi
fi

# Chargement des aliases
[ -f ~/.bash_aliases ] && . ~/.bash_aliases
EOF

# ---- 2) Écriture intégrale de ~/.bash_aliases ----
cat > "$ALIASES_USER" << 'EOF'
# ~/.bash_aliases : géré par postinstall.26.04_v2.sh
# (réécrit à chaque exécution du script — backup horodaté dans ~/.bash_aliases.backup.*)

# === Navigation ===
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias ~='cd ~'

# === Sécurité ===
alias rm='rm -I'
alias cp='cp -ip'
alias mv='mv -i'

# === Outils ===
alias df='df -h'
alias du='du -h --max-depth=1'
alias free='free -h'
alias path='echo -e ${PATH//:/\\n}'

# === Git ===
alias gs='git status'
alias ga='git add'
alias gc='git commit -m'
alias gl='git log --oneline --graph --decorate --all'
alias gco='git checkout'
alias gd='git diff'
alias cdgit='cd /mnt/nas/sauvegarde/git/'

# === Sauvegarde scripts ===
alias backupbash='/mnt/nas/sauvegarde/git/scripts/backup_bash.sh'
alias restorebash='/mnt/nas/sauvegarde/git/scripts/restore_bash.sh'
alias backuppostinstall='/mnt/nas/sauvegarde/git/scripts/backup_postinstall.sh'
alias restorepostinstall='/mnt/nas/sauvegarde/git/scripts/restore_postinstall.sh'

# === Divers ===
alias cls='clear'
alias h='history'
# IP publique : double-quote externe pour permettre les single-quotes du sed
alias ipext="wget -q -O - checkip.dyndns.org | sed -e 's/[^[:digit:]|.]//g'"

# === VeraCrypt — utilise les scripts générés en section [8b/31] ===
# (mot de passe lu depuis ~/.veracrypt_pass — voir ~/.mount_1984.sh)
alias 1984='~/.mount_1984.sh'
alias 1984q='~/.umount_1984.sh'

# === Alert ===
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'

# === Mise à jour complète ===
alias update='sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo snap refresh && flatpak update -y --noninteractive'

# === Verrouillage (Wayland/GNOME 50) ===
alias lok='loginctl lock-session'

# === Reboot / poweroff ===
alias reboot='sudo reboot'
alias poweroff='sudo poweroff'

# === ClamAV ===
# clamscan  : standalone, tourne en root, supporte -r et --exclude-dir
# clamdscan : client du daemon, SANS -r. Pour un fichier précis rapidement.
# Le NAS (/mnt) possède son propre antivirus — exclu du scan automatique.
# Variables réutilisées (DRY) pour les exclusions
CLAM_EXCLUDES='--exclude-dir="^/mnt" --exclude-dir="^/media" --exclude-dir="^/run/media" --exclude-dir="^/snap" --exclude-dir="^/proc" --exclude-dir="^/sys" --exclude-dir="^/dev" --exclude-dir="^/run" --exclude-dir="^/boot" --exclude-dir="^/var/lib/docker" --exclude-dir="^/var/cache/apt"'
CLAM_MEDIA_EXCL='--exclude="\.(mkv|mp4|avi|mov|wmv|flv|webm|m4v|mpg|mpeg|ts|m2ts|vob|iso|img|bin|nrg|dmg|mp3|flac|ogg|aac|wav|wma|m4a|jpg|jpeg|png|gif|bmp|tiff|webp|svg|raw|cr2|nef|arw)$"'

alias cvup='sudo freshclam'
alias cvupd='sudo systemctl restart clamav-freshclam'
alias cvstat='sudo systemctl status clamav-freshclam'
alias cvscan='eval sudo clamscan -r -i --bell $CLAM_EXCLUDES'
alias cvsfast='sudo clamdscan -i --fdpass'
alias cvlog='eval sudo clamscan -r -i --log=/var/log/clamav/scan.log $CLAM_EXCLUDES'
alias cvq='eval sudo clamscan -r --move=$HOME/quarantaine -i --bell $CLAM_EXCLUDES'
alias cvconf='clamconf | less'
alias cvver='clamscan --version'
alias cvdaemon='sudo systemctl status clamav-daemon'
alias cvstart='sudo systemctl start clamav-daemon'
alias cvrestart='sudo systemctl restart clamav-daemon'
alias cvlogs='sudo journalctl -u clamav-daemon -n 80 --no-pager | less'
alias cvfreshlogs='sudo journalctl -u clamav-freshclam -n 60 --no-pager | less'
alias cvnas='eval sudo clamscan -r -i --bell $CLAM_MEDIA_EXCL /mnt/nas/'
alias cvnasslow='sudo clamscan -r -i --bell /mnt/nas/'
alias cvslow='sudo clamscan -r -i --bell'
alias cvslowq='sudo clamscan -r --move=$HOME/quarantaine -i'
alias cvnaslog='eval sudo clamscan -r -i $CLAM_MEDIA_EXCL --log=/var/log/clamav/nas.log /mnt/nas/'
alias cvnasq='eval sudo clamscan -r --move=$HOME/quarantaine -i --bell $CLAM_MEDIA_EXCL /mnt/nas/'

# === yt-dlp ===
alias mp3dl='yt-dlp -x --audio-format mp3 --audio-quality 0 --embed-thumbnail'
alias ytdlpup='sudo yt-dlp -U'

# === VeraCrypt — mise à jour manuelle (URL dynamique selon version Ubuntu) ===
veracryptup() {
    local ubuntu_ver
    # « ; » + fallback : si le sourcing échoue, la variable restait vide (v8.9.1)
    ubuntu_ver=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")
    [ -z "$ubuntu_ver" ] && ubuntu_ver="26.04"
    local url=""
    local data
    data=$(curl -s https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest)
    local v
    for v in "$ubuntu_ver" "24.04" "22.04"; do
        url=$(echo "$data" | grep "browser_download_url.*console.*Ubuntu-${v}.*amd64.deb" | head -n1 | cut -d'"' -f4)
        [ -n "$url" ] && break
    done
    if [ -z "$url" ]; then
        echo "VeraCrypt : aucune URL trouvée pour Ubuntu $ubuntu_ver / 24.04 / 22.04" >&2
        return 1
    fi
    echo "Téléchargement : $url"
    wget -q "$url" -O /tmp/veracrypt.deb \
        && sudo apt install -y /tmp/veracrypt.deb \
        && rm -f /tmp/veracrypt.deb \
        && echo "VeraCrypt mis à jour"
}

# === Divers ===
alias pw='pwgen -s -y -c -n 20 16 -1'

# Cours de l'or (XAU/EUR) — LC_ALL=C comme variable d'environnement (pas en argument awk)
alias or='data=$(curl -s https://api.exchangerate.fun/latest); rate=$(echo "$data" | jq -r ".rates.XAU"); price=$(LC_ALL=C awk -v r="$rate" "BEGIN {printf \"%.2f\", 1/r}"); echo -e "💰 \033[1mOr (XAU/EUR)\033[0m : \033[1m${price}\033[0m € / once"'

# Cours de l'ADA (EUR)
alias ada='data=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=cardano&vs_currencies=eur"); price=$(echo "$data" | jq -r ".cardano.eur"); echo -e "💎 \033[1mCardano (ADA/EUR)\033[0m : \033[1m${price}\033[0m €"'

# === Wake-on-LAN ===
alias wol='wakeonlan 9c:5c:8e:be:13:e5'

# === VPN ===
alias vpnsuisse='nmcli connection up "Suisse"'
alias vpnusa='nmcli connection up "USA"'
alias vpnfr='nmcli connection up "France"'
alias vpnru='nmcli connection up "Russie"'
alias vpnoff='for c in Suisse USA France Russie; do nmcli connection down "$c" 2>/dev/null; done'
alias vpnstat='nmcli connection show --active | grep -E "Suisse|USA|France|Russie" || echo "Aucun VPN actif"'

# === Supervision Centreon ===
# Réinitialise le hash de référence de la sonde check_grub après une
# modification légitime de grub.cfg (mise à jour noyau, update-grub…)
# Sonde déployée séparément via Centreon — l'alias est inerte tant qu'elle est absente
alias grub-reinit='sudo /usr/lib/nagios/plugins/check_grub.sh -i'

# === Docker NAS ===
# Met à jour la pile transmission-vpn (gluetun + transmission + autoheal) sur le NAS principal
# Nécessite les clés SSH échangées et sudo sans mot de passe sur 192.168.1.100
alias maj-transmission='ssh -p 22 utilisateur@192.168.1.100 "sudo /volume1/docker/transmission-vpn/maj.sh"'

EOF

# Aliases agents Claude Code : générés par boucle (DRY) — append au fichier
{
    echo "# === Agents Claude Code (générés par boucle - suffixe 48 = Opus 4.8, f5 = Fable 5) ==="
    for domaine in centreon:Centreon infra:Infrastructure scriptbash:ScriptBash sysadmin:Sysadmin webdev:WebDev; do
        nom="${domaine%%:*}"
        rep="${domaine##*:}"
        echo "alias agent-${nom}-48='cd ~/Agents/${rep} && claude --dangerously-skip-permissions --model claude-opus-4-8'"
        echo "alias agent-${nom}-f5='cd ~/Agents/${rep} && claude --dangerously-skip-permissions --model claude-fable-5'"
    done
    echo ""
    cat <<'AIDE_EOF'
# Aide agents Claude Code
alias agent-help='echo -e "\e[1;33m=== AGENTS CLAUDE CODE ===\e[0m\n\n\e[1;36mLANCEMENT DES AGENTS (suffixe 48 = Opus 4.8, f5 = Fable 5)\e[0m\n\e[0;37m  agent-centreon-48/f5\e[0m\n\e[0;37m  agent-infra-48/f5\e[0m\n\e[0;37m  agent-scriptbash-48/f5\e[0m\n\e[0;37m  agent-sysadmin-48/f5\e[0m\n\e[0;37m  agent-webdev-48/f5\e[0m\n\n\e[1;36mSLASH COMMANDS (à taper dans une session agent)\e[0m\n\e[0;37m  /refs            \e[0;90m→ liste toutes les références disponibles\e[0m\n\e[0;37m  /clapi           \e[0;90m→ référence CLAPI/API REST Centreon\e[0m\n\e[0;37m  /sonde           \e[0;90m→ template sonde Nagios/Centreon\e[0m\n\e[0;37m  /template_bash   \e[0;90m→ squelette complet d'\''un script bash (structure + aide)\e[0m\n\e[0;37m  /ansible         \e[0;90m→ référence Ansible\e[0m\n\e[0;37m  /commandes       \e[0;90m→ référence surveillance Infrastructure\e[0m\n\e[0;37m  /templates       \e[0;90m→ fonctions utilitaires Sysadmin (sudo, SSH, sauvegarde)\e[0m\n\n\e[1;36mPLUGIN CAVEMAN\e[0m\n\e[0;37m  /caveman          \e[0;90m→ activer mode caveman (réponses courtes -75% tokens)\e[0m\n\e[0;37m  /caveman off      \e[0;90m→ désactiver caveman\e[0m\n\e[0;37m  /caveman ultra    \e[0;90m→ mode ultra-compact\e[0m"'

# Audit système
alias audit-local='bash ~/Agents/Scripts/diagnostics/audit_localhost.sh'
AIDE_EOF
} >> "$ALIASES_USER"

# ---- 3) Permissions ----
chown "$UTILISATEUR":"$UTILISATEUR" "$BASHRC_USER" "$ALIASES_USER"
chmod 644 "$BASHRC_USER" "$ALIASES_USER"

# Dossier de quarantaine ClamAV — cible des alias cvq/cvslowq/cvnasq
# (le tilde de --move=~/... n'étant pas expansé, les alias utilisent $HOME)
mkdir -p "/home/$UTILISATEUR/quarantaine"
chown "$UTILISATEUR":"$UTILISATEUR" "/home/$UTILISATEUR/quarantaine"

log_ok ".bashrc et .bash_aliases écrasés (backups : .backup.${TS})"

}

################################################################################
# 8b. SCRIPTS VERACRYPT SÉCURISÉS (.mount_1984.sh / .umount_1984.sh)
# — Mot de passe lu depuis ~/.veracrypt_pass (non exposé dans les aliases)
################################################################################

section_8b() {


MOUNT_SCRIPT="/home/$UTILISATEUR/.mount_1984.sh"
UMOUNT_SCRIPT="/home/$UTILISATEUR/.umount_1984.sh"

# Création du script de montage si absent
# --text --non-interactive + --keyfiles="" (v8.9.8) : le binaire console
# (veracrypt-console, seul disponible en fallback 26.04) demandait
# interactivement « Enter keyfile [none]: » — tout paramètre non fourni
# devient un prompt. En non-interactif, tout est passé en argument.
if [ ! -f "$MOUNT_SCRIPT" ]; then
    cat <<EOF > "$MOUNT_SCRIPT"
#!/bin/bash
# Volume déjà monté ? → message clair et sortie 0 (v8.9.9) : veracrypt-console
# renverrait sinon le message trompeur « L'emplacement du volume est indisponible »
if findmnt $VERACRYPT_MOUNTPOINT >/dev/null 2>&1; then
    echo "Volume déjà monté sur $VERACRYPT_MOUNTPOINT (démonter avec : 1984q)"
    exit 0
fi
veracrypt --text --non-interactive --mount \\
    $VERACRYPT_CONTAINER \\
    $VERACRYPT_MOUNTPOINT \\
    --password="\$(cat $VERACRYPT_PASS_FILE)" \\
    --pim=0 \\
    --keyfiles="" \\
    --protect-hidden=no
EOF
    chmod 700 "$MOUNT_SCRIPT"
    chown "$UTILISATEUR":"$UTILISATEUR" "$MOUNT_SCRIPT"
    log_ok "Script .mount_1984.sh créé"
else
    log_ok "Script .mount_1984.sh déjà présent — non modifié"
fi

# Création du script de démontage si absent
if [ ! -f "$UMOUNT_SCRIPT" ]; then
    cat <<EOF > "$UMOUNT_SCRIPT"
#!/bin/bash
veracrypt --text -d $VERACRYPT_CONTAINER
EOF
    chmod 700 "$UMOUNT_SCRIPT"
    chown "$UTILISATEUR":"$UTILISATEUR" "$UMOUNT_SCRIPT"
    log_ok "Script .umount_1984.sh créé"
else
    log_ok "Script .umount_1984.sh déjà présent — non modifié"
fi

# Créer ~/.veracrypt_pass avec le mot de passe si absent ou vide
# umask 077 : fichier créé directement en 600 (pas de fenêtre 644)
if [ ! -f "$VERACRYPT_PASS_FILE" ] || [ ! -s "$VERACRYPT_PASS_FILE" ]; then
    (
        umask 077
        printf '%s\n' "$VERACRYPT_PASS" > "$VERACRYPT_PASS_FILE"
    )
    chown "$UTILISATEUR":"$UTILISATEUR" "$VERACRYPT_PASS_FILE"
    log_ok "Fichier ~/.veracrypt_pass créé (chmod 600 via umask)"
else
    log_ok "Fichier ~/.veracrypt_pass déjà présent et non vide — non modifié"
fi

# Créer le point de montage VeraCrypt si absent
if [ ! -d "$VERACRYPT_MOUNTPOINT" ]; then
    mkdir -p "$VERACRYPT_MOUNTPOINT"
    chown "$UTILISATEUR":"$UTILISATEUR" "$VERACRYPT_MOUNTPOINT"
    log_ok "Point de montage VeraCrypt créé : $VERACRYPT_MOUNTPOINT"
else
    log_ok "Point de montage VeraCrypt déjà présent : $VERACRYPT_MOUNTPOINT"
fi

}

################################################################################
# 9. SUDO SANS MOT DE PASSE
################################################################################

section_9() {


# Validation visudo AVANT activation : un fichier sudoers malformé sous
# /etc/sudoers.d/ casserait TOUT sudo. On écrit en temporaire, on valide,
# puis on installe atomiquement en 0440 root:root.
SUDOERS_USER="/etc/sudoers.d/90-${UTILISATEUR}"
SUDOERS_TMP=$(mktemp)
printf '%s ALL=(ALL) NOPASSWD:ALL\n' "$UTILISATEUR" > "$SUDOERS_TMP"
if visudo -cf "$SUDOERS_TMP" >/dev/null 2>&1; then
    install -m 0440 -o root -g root "$SUDOERS_TMP" "$SUDOERS_USER"
    log_ok "Sudo NOPASSWD activé (${SUDOERS_USER}, syntaxe validée par visudo)"
else
    log_error "Syntaxe sudoers invalide pour ${SUDOERS_USER} — non installé (sudo préservé)"
fi
rm -f "$SUDOERS_TMP"

}

################################################################################
# 10. FLATPAK ET APPLICATIONS
# Note 26.04 : musescore3 absent des dépôts apt officiels → Flatpak org.musescore.MuseScore
################################################################################

section_10() {


hash -r

FLATPAK_BIN=$(command -v flatpak || echo "")
if [ -z "$FLATPAK_BIN" ] || [ ! -x "$FLATPAK_BIN" ]; then
    log_warn "flatpak introuvable dans PATH, tentative de réinstallation..."
    apt install -y flatpak || log_warn "Réinstallation flatpak échouée"
    hash -r
    FLATPAK_BIN=$(command -v flatpak || echo "/usr/bin/flatpak")
fi

if [ -x "$FLATPAK_BIN" ]; then
    "$FLATPAK_BIN" remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo \
        || log_warn "Flathub : remote-add échoué — les installations Flatpak vont échouer"

    # Tableau d'applications : installation groupée (rapide) puis reprise app
    # par app — une seule ref invalide avorte TOUTE la transaction flatpak,
    # même pattern que les paquets APT de la section 2 (R1 v8.8.36)
    FLATPAK_APPS=(
        com.discordapp.Discord
        org.videolan.VLC
        com.bambulab.BambuStudio
        com.usebottles.bottles
        io.github.linx_systems.ClamUI
        org.telegram.desktop
        org.musescore.MuseScore
    )
    "$FLATPAK_BIN" install -y flathub "${FLATPAK_APPS[@]}" \
        || log_warn "Installation Flatpak groupée incomplète — reprise app par app"

    FP_OK=0; FP_KO=0; FP_KO_LIST=""
    for app in "${FLATPAK_APPS[@]}"; do
        "$FLATPAK_BIN" info "$app" &>/dev/null \
            || "$FLATPAK_BIN" install -y flathub "$app" >/dev/null 2>&1 || true
        if "$FLATPAK_BIN" info "$app" &>/dev/null; then
            FP_OK=$((FP_OK + 1))
        else
            FP_KO=$((FP_KO + 1))
            FP_KO_LIST+=" $app"
        fi
    done
    if [ "$FP_KO" -eq 0 ]; then
        log_ok "Apps Flatpak installées : $FP_OK/${#FLATPAK_APPS[@]} (toutes présentes)"
    else
        log_warn "Apps Flatpak : $FP_OK/${#FLATPAK_APPS[@]} ok — $FP_KO manquante(s) :$FP_KO_LIST"
    fi
else
    log_error "flatpak introuvable même après réinstallation — étape ignorée"
fi

}

################################################################################
# 11. MISES À JOUR AUTOMATIQUES
################################################################################

section_11() {


echo unattended-upgrades unattended-upgrades/enable_auto_updates boolean true | debconf-set-selections
dpkg-reconfigure -f noninteractive unattended-upgrades
log_ok "Mises à jour de sécurité automatiques activées"

}

################################################################################
# 12. NETTOYAGE PAQUETS
################################################################################

section_12() {


apt autoremove -y && apt autoclean -y
log_ok "Paquets obsolètes supprimés"

}

################################################################################
# 13. LIMITATION JOURNAL SYSTEMD
################################################################################

section_13() {


grep -qxF 'SystemMaxUse=500M' /etc/systemd/journald.conf || echo 'SystemMaxUse=500M' >> /etc/systemd/journald.conf
systemctl restart systemd-journald || log_warn "Redémarrage journald échoué"
log_ok "Journal limité à 500 Mo"

}

################################################################################
# 14. CONFIGURATION NAUTILUS, GNOME, EXTENSIONS + THÈME TERMINAL PTYXIS
# Note 26.04 : GNOME Terminal remplacé par Ptyxis (depuis Ubuntu 25.10)
#   - Le thème Solarized Dark se configure via dconf org.gnome.Ptyxis
#   - L'ID de profil Ptyxis se lit via dconf read /org/gnome/Ptyxis/default-profile-uuid
#   - La session est Wayland-only (pas de fallback X11)
################################################################################

section_14() {


if command -v gsettings &>/dev/null; then
    DBUS_PATH="unix:path=/run/user/${USER_UID}/bus"

    # ---- Nautilus (via wrapper gset() — L1 v8.8.24) ----
    gset org.gnome.nautilus.preferences show-hidden-files true \
        || log_warn "gset Nautilus show-hidden-files ignoré (DBUS/session indisponible ?)"
    gset org.gnome.nautilus.preferences default-folder-viewer 'list-view' \
        || log_warn "gset Nautilus default-folder-viewer ignoré (DBUS/session indisponible ?)"
    gset org.gnome.desktop.thumbnailers disable-all false \
        || log_warn "gset thumbnailers disable-all ignoré (DBUS/session indisponible ?)"
    gset org.gnome.nautilus.preferences show-image-thumbnails 'always' \
        || log_warn "gset Nautilus show-image-thumbnails ignoré (DBUS/session indisponible ?)"
    gset org.gnome.nautilus.preferences thumbnail-limit 1048576 \
        || log_warn "gset Nautilus thumbnail-limit ignoré (DBUS/session indisponible ?)"
    log_ok "Préférences Nautilus configurées"

    # ---- Extensions GNOME depuis extensions.gnome.org (v8.9.12) ----
    GNOME_VERSION=$(gnome-shell --version 2>/dev/null | grep -oP '\d+' | head -n1 || echo "50")
    log_info "Version GNOME Shell détectée : $GNOME_VERSION"

    # Installe une extension depuis l'API extensions.gnome.org (URL dynamique
    # selon la version du shell). Usage : installer_extension_ego <pk> <nom>
    installer_extension_ego() {
        local pk="$1" nom="$2" url
        url=$(curl -s "https://extensions.gnome.org/extension-info/?pk=${pk}&shell_version=${GNOME_VERSION}" \
            | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    print('https://extensions.gnome.org' + d.get('download_url', ''))
except:
    print('')
" 2>/dev/null || echo "")
        if [ -z "$url" ] || [ "$url" = "https://extensions.gnome.org" ]; then
            log_warn "$nom : URL introuvable pour GNOME $GNOME_VERSION (installer via Extension Manager)"
            return 1
        fi
        sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
            gnome-extensions install --force "$url" 2>/dev/null \
            && log_ok "$nom installé" \
            || log_warn "$nom : installation échouée (installer via Extension Manager)"
    }

    log_info "Installation clipboard-indicator..."
    installer_extension_ego 779 "clipboard-indicator" || true
    # dash-to-dock retiré des dépôts 26.04 (paquet deb) → même canal (v8.9.12)
    log_info "Installation dash-to-dock..."
    installer_extension_ego 307 "dash-to-dock" || true

    # ---- Activation des extensions ----
    # Détection FILESYSTEM (v8.9.12) : « gnome-extensions list » ne reflète les
    # nouvelles extensions qu'après rechargement du shell — au test réel, même
    # clipboard-indicator fraîchement installé était déclaré absent.
    # Activation ADDITIVE via gsettings : les UUID sont ajoutés à
    # enabled-extensions sans écraser les extensions Ubuntu déjà actives
    # (ubuntu-dock, appindicators…) ; prise en compte à la prochaine session.
    log_info "Activation des extensions GNOME..."
    EXT_USER_DIR="/home/$UTILISATEUR/.local/share/gnome-shell/extensions"
    for ext in dash-to-dock@micxgx.gmail.com \
               gsconnect@andyholmes.github.io \
               clipboard-indicator@tudmotu.com; do
        if [ -d "/usr/share/gnome-shell/extensions/$ext" ] || [ -d "$EXT_USER_DIR/$ext" ]; then
            actuelles=$(sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
                gsettings get org.gnome.shell enabled-extensions 2>/dev/null || echo "[]")
            nouvelles=$(printf '%s' "$actuelles" | python3 -c "
import sys, ast
try:
    lst = ast.literal_eval(sys.stdin.read().strip() or '[]')
except Exception:
    lst = []
if not isinstance(lst, list):
    lst = []
if '$ext' not in lst:
    lst.append('$ext')
print(str(lst))
" 2>/dev/null || echo "")
            if [ -n "$nouvelles" ] && sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
                gsettings set org.gnome.shell enabled-extensions "$nouvelles" 2>/dev/null; then
                log_ok "Extension activée (effective à la prochaine session) : $ext"
            else
                log_warn "Extension $ext : activation gsettings échouée (activer via Extension Manager)"
            fi
        else
            log_warn "Extension absente : $ext (installer via Extension Manager si besoin)"
        fi
    done

    # ---- Thème Solarized Dark pour Ptyxis (remplace GNOME Terminal depuis Ubuntu 25.10) ----
    # La palette Solarized Dark est appliquée via dconf sur le profil Ptyxis par défaut.
    # Le schéma de couleurs est défini comme une entrée "user-defined" dans org.gnome.Ptyxis.
    log_info "Application thème Solarized Dark sur Ptyxis..."

    PTYXIS_PROFILE_UUID=$(sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
        dconf read /org/gnome/Ptyxis/default-profile-uuid 2>/dev/null | tr -d "'" || true)

    if [ -z "$PTYXIS_PROFILE_UUID" ]; then
        log_warn "Profil Ptyxis introuvable — Ptyxis n'a peut-être pas encore été lancé"
        log_warn "Après le premier lancement de Ptyxis, appliquer manuellement :"
        log_warn "  UUID=\$(dconf read /org/gnome/Ptyxis/default-profile-uuid | tr -d \"'\")"
        log_warn "  dconf write /org/gnome/Ptyxis/Profiles/\$UUID/palette \"'Solarized'\""
    else
        PTYXIS_PATH="/org/gnome/Ptyxis/Profiles/$PTYXIS_PROFILE_UUID/"
        # Ptyxis dispose d'un thème Solarized natif activable via la clé "palette"
        sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
            dconf write "${PTYXIS_PATH}palette" "'Solarized'"
        sudo -u "$UTILISATEUR" DBUS_SESSION_BUS_ADDRESS="$DBUS_PATH" \
            dconf write "${PTYXIS_PATH}dark-theme" "true"
        log_ok "Thème Solarized Dark appliqué à Ptyxis (profil : $PTYXIS_PROFILE_UUID)"
    fi

    # Activer les secondes et le jour de la semaine dans l'horloge GNOME
    gset org.gnome.desktop.interface clock-show-seconds true \
        || log_warn "gset horloge clock-show-seconds ignoré (DBUS/session indisponible ?)"
    gset org.gnome.desktop.interface clock-show-weekday true \
        || log_warn "gset horloge clock-show-weekday ignoré (DBUS/session indisponible ?)"
    log_ok "Horloge GNOME : secondes et jour de la semaine activés"

    log_ok "Configuration GNOME, extensions et thème terminal Ptyxis terminés"
else
    log_warn "gsettings indisponible — configuration GNOME ignorée"
fi

}

################################################################################
# 15. DÉSACTIVATION VEILLE
################################################################################

section_15() {


if command -v gsettings &>/dev/null; then
    DBUS_PATH="unix:path=/run/user/${USER_UID}/bus"
    # Wrapper gset() — L1 v8.8.24
    gset org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 0 \
        || log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
    gset org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout 0 \
        || log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
    gset org.gnome.desktop.session idle-delay 0 \
        || log_warn "gset session idle-delay ignoré (DBUS/session indisponible ?)"
    gset org.gnome.settings-daemon.plugins.power idle-dim false \
        || log_warn "gsettings power ignoré (schéma absent sur GNOME 50)"
    log_ok "Veille désactivée"
fi

}

################################################################################
# 16. DÉSACTIVATION UFW
################################################################################

section_16() {


ufw disable || log_warn "UFW déjà désactivé ou absent"
log_ok "UFW désactivé"

}

################################################################################
# 17. SCRIPT NETTOYAGE AUTOMATIQUE
################################################################################

section_17() {


cat <<'SCRIPT' > /usr/local/bin/cleanup.sh
#!/bin/bash
set -euo pipefail

echo "=== Nettoyage système $(date) ==="

# DPkg::Lock::Timeout : attendre le verrou dpkg au lieu d'échouer immédiatement
# (update-system et cleanup-system peuvent partir au même boot, Persistent=true)
apt -o DPkg::Lock::Timeout=600 autoremove -y && apt -o DPkg::Lock::Timeout=600 autoclean -y

snap set system refresh.retain=2 || true
snap list --all | awk '/disabled/{print $1" --revision "$3}' | xargs -r snap remove || true

flatpak uninstall --unused -y || true

# Nettoyage Docker uniquement si le service est actif
if systemctl is-active --quiet docker; then
    docker system prune -f
    docker builder prune -f --keep-storage 10GB
else
    echo "Docker inactif — nettoyage Docker ignoré"
fi

find /tmp /var/tmp -type f -atime +7 -delete 2>/dev/null || true
find /home -maxdepth 3 -path "*/.cache/thumbnails" -type f -atime +30 -delete 2>/dev/null || true
find /var/log -type f -name "*.log.*" -mtime +30 -delete 2>/dev/null || true

echo "=== Fin nettoyage $(date) ==="
SCRIPT

chmod +x /usr/local/bin/cleanup.sh

cat <<'EOF' > /etc/systemd/system/cleanup-system.timer
[Unit]
Description=Nettoyage hebdomadaire

[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

cat <<'EOF' > /etc/systemd/system/cleanup-system.service
[Unit]
Description=Nettoyage système
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
Nice=19
EOF

systemctl daemon-reload
systemctl enable --now cleanup-system.timer
log_ok "Nettoyage automatique planifié (dimanche 3h)"

}

################################################################################
# 18. SCRIPT MISE À JOUR AUTOMATIQUE (SYSTÈME)
# — Sans Ollama/Open WebUI (remplacés par Claude Code)
# — OnBootSec=5min pour laisser la session GNOME s'établir
################################################################################

section_18() {


cat <<'SCRIPT' > /usr/local/bin/update-system.sh
#!/bin/bash
set -euo pipefail

echo "=== Mise à jour système $(date) ==="

echo "• Mise à jour APT..."
# DPkg::Lock::Timeout : attendre le verrou dpkg (unattended-upgrades ou
# cleanup-system peuvent tourner en même temps au boot, Persistent=true)
apt -o DPkg::Lock::Timeout=600 update && apt -o DPkg::Lock::Timeout=600 upgrade -y \
    || echo "  → APT en échec — poursuite des autres mises à jour"

echo "• Mise à jour Snap..."
snap refresh || true

echo "• Mise à jour Flatpak..."
flatpak update -y --noninteractive || true

echo "• Mise à jour Claude Code..."
npm update -g @anthropic-ai/claude-code 2>/dev/null \
    && echo "  ✓ Claude Code mis à jour" \
    || echo "  → Claude Code : mise à jour ignorée (non installé ou erreur)"

echo "• Mise à jour yt-dlp..."
yt-dlp -U 2>/dev/null \
    && echo "  ✓ yt-dlp mis à jour" \
    || echo "  → yt-dlp : mise à jour ignorée (non installé ou erreur)"

# Mise à jour VeraCrypt (binaire GitHub) — URL dynamique selon Ubuntu + fallback
# || true OBLIGATOIRE : sous set -euo pipefail, une assignation dont le pipeline
# échoue (VeraCrypt absent, réseau HS, API GitHub rate-limitée) tuerait le
# script ici et sauterait le message final (même esprit que N3 v8.8.34)
VERACRYPT_INSTALLED=$(dpkg -l veracrypt 2>/dev/null | awk '/^ii/{print $3}') || true
DATA=$(curl -s https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest) || true
VERACRYPT_LATEST=$(echo "$DATA" | grep '"tag_name"' | cut -d'"' -f4 | sed 's/VeraCrypt_//') || true

if [[ -n "$VERACRYPT_LATEST" && "$VERACRYPT_INSTALLED" != "$VERACRYPT_LATEST" ]]; then
    echo "[$(date '+%F %T')] [INFO] Mise à jour VeraCrypt : $VERACRYPT_INSTALLED → $VERACRYPT_LATEST"
    # « ; » + fallback : si le sourcing échoue, la variable restait vide (v8.9.1)
    UBUNTU_VER=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")
    [ -z "$UBUNTU_VER" ] && UBUNTU_VER="26.04"
    URL=""
    for v in "$UBUNTU_VER" "24.04" "22.04"; do
        # || true : grep sans correspondance ferait sortir le script (set -e + pipefail)
        URL=$(echo "$DATA" | grep "browser_download_url.*console.*Ubuntu-${v}.*amd64.deb" | head -n1 | cut -d'"' -f4) || true
        [ -n "$URL" ] && break
    done
    if [ -n "$URL" ]; then
        wget -q "$URL" -O /tmp/veracrypt.deb \
            && apt -o DPkg::Lock::Timeout=600 install -y /tmp/veracrypt.deb \
            && echo "[$(date '+%F %T')] [OK] VeraCrypt mis à jour vers $VERACRYPT_LATEST" \
            || echo "[$(date '+%F %T')] [WARN] Mise à jour VeraCrypt échouée"
        rm -f /tmp/veracrypt.deb
    else
        echo "[$(date '+%F %T')] [WARN] VeraCrypt : aucune URL pour Ubuntu ${UBUNTU_VER}/24.04/22.04"
    fi
else
    echo "[$(date '+%F %T')] [OK] VeraCrypt à jour ($VERACRYPT_INSTALLED)"
fi

echo "=== Fin mise à jour $(date) ==="
SCRIPT

chmod +x /usr/local/bin/update-system.sh

cat <<'EOF' > /etc/systemd/system/update-system.service
[Unit]
Description=Mise à jour automatique système
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-system.sh
Nice=19
EOF

cat <<'EOF' > /etc/systemd/system/update-system.timer
[Unit]
Description=Mise à jour quotidienne système

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
OnBootSec=5min

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now update-system.timer
log_ok "Mise à jour automatique planifiée (quotidienne, délai 5min au boot)"

}

################################################################################
# 19. REDIRECTION DOSSIERS VERS NAS + SIGNETS
################################################################################

section_19() {


USER_DIRS_FILE="/home/$UTILISATEUR/.config/user-dirs.dirs"
USER_DIRS_CONF="/home/$UTILISATEUR/.config/user-dirs.conf"

# ---- Dossiers XDG : tous pointent vers $HOME (locaux) ----
# Ne pas pointer XDG_DOCUMENTS_DIR / MUSIC / PICTURES vers le NAS :
# cela supprime les entrées "Documents", "Musique", "Images" de la barre latérale
# et provoque un mélange dans les signets Nautilus.
# Les dossiers NAS sont accessibles via les signets file:///mnt/nas/ ci-dessous.
cat <<EOF > "$USER_DIRS_FILE"
XDG_DESKTOP_DIR="\$HOME/Bureau"
XDG_DOWNLOAD_DIR="\$HOME/Téléchargements"
XDG_TEMPLATES_DIR="\$HOME/Modèles"
XDG_PUBLICSHARE_DIR="\$HOME/Public"
XDG_DOCUMENTS_DIR="\$HOME/Documents"
XDG_MUSIC_DIR="\$HOME/Musique"
XDG_PICTURES_DIR="\$HOME/Images"
XDG_VIDEOS_DIR="\$HOME/Vidéos"
EOF

cat <<EOF > "$USER_DIRS_CONF"
enabled=False
EOF

chown "$UTILISATEUR":"$UTILISATEUR" "$USER_DIRS_FILE" "$USER_DIRS_CONF"

# Créer explicitement chaque dossier XDG local et appliquer les droits
for dir in Bureau Téléchargements Modèles Public Documents Musique Images Vidéos; do
    mkdir -p "/home/$UTILISATEUR/$dir"
    chown "$UTILISATEUR":"$UTILISATEUR" "/home/$UTILISATEUR/$dir"
done
log_ok "Dossiers XDG locaux créés : Bureau, Téléchargements, Modèles, Public, Documents, Musique, Images, Vidéos"

# Pas de xdg-user-dirs-update : no-op avec enabled=False dans user-dirs.conf
# (les dossiers et user-dirs.dirs sont gérés statiquement ci-dessus)
sudo -u "$UTILISATEUR" nautilus -q 2>/dev/null || true

# ---- Signets Nautilus : URI file:///mnt/nas/<partage> ----
# On utilise file:// pointant sur les montages CIFS déjà authentifiés via fstab.
# Les URI smb:// forcent Nautilus à se connecter via GVfs et redemandent le mot
# de passe NAS — à éviter puisque le montage fstab est déjà actif et authentifié.
# Les noms documents/musique/photographie sont suffixés "NAS" pour les distinguer
# des dossiers locaux XDG homonymes rétablis dans $HOME ci-dessus.
# veracrypt1 est listé en premier (montage local, pas NAS).
BOOKMARKS_FILE="/home/$UTILISATEUR/.config/gtk-3.0/bookmarks"
mkdir -p "$(dirname "$BOOKMARKS_FILE")"

# Backup unique si fichier existe (préserve customisations utilisateur)
[ -f "$BOOKMARKS_FILE" ] && cp "$BOOKMARKS_FILE" "${BOOKMARKS_FILE}.backup.${TS}"
touch "$BOOKMARKS_FILE"

declare -A NOM_SIGNET
NOM_SIGNET[animation]="🎬 Animation"
NOM_SIGNET[documents]="📄 Documents NAS"
NOM_SIGNET[downloads]="⬇️ Téléchargements NAS"
NOM_SIGNET[films]="🎥 Films"
NOM_SIGNET[Imprimante]="🖨️ Imprimante"
NOM_SIGNET[jeux]="🎮 Jeux"
NOM_SIGNET[livres]="📚 Livres"
NOM_SIGNET[musique]="🎵 Musique NAS"
NOM_SIGNET[perso]="👤 Perso"
NOM_SIGNET[photographie]="📸 Photographie NAS"
NOM_SIGNET[sauvegarde]="💾 Sauvegarde"
NOM_SIGNET[seriestv]="📺 Séries TV"
NOM_SIGNET[surveillance]="📹 Surveillance"
NOM_SIGNET[Technique]="🔧 Technique"

# Ajout idempotent : on n'ajoute que les signets absents (préserve les bookmarks user)
ajoute_signet() {
    local uri="$1"
    local nom="$2"
    if ! grep -qF "$uri" "$BOOKMARKS_FILE"; then
        printf '%s %s\n' "$uri" "$nom" >> "$BOOKMARKS_FILE"
    fi
}

ajoute_signet "file:///media/veracrypt1" "🔐 VeraCrypt"
for dossier in "${NAS_PARTAGES[@]}"; do
    ajoute_signet "file:///mnt/nas/${dossier}" "${NOM_SIGNET[$dossier]:-$dossier}"
done

chown "$UTILISATEUR":"$UTILISATEUR" "$BOOKMARKS_FILE"
log_ok "Signets NAS ajoutés sans écraser les bookmarks user existants"

}

################################################################################
# 20. VÉRIFICATIONS SERVICES
################################################################################

section_20() {


# Boucle de vérification des services critiques (R3 v8.8.24)
# Format spécial pour libvirt : libvirtd OU virtqemud (selon la version)
SRV_OK=0; SRV_KO=0; SRV_KO_LIST=""
for svc in ssh docker postfix nagios-nrpe-server snmpd centagent clamav-freshclam smartmontools; do
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
        log_ok "Service actif : $svc"
        SRV_OK=$((SRV_OK + 1))
    else
        log_warn "Service inactif : $svc"
        SRV_KO=$((SRV_KO + 1))
        SRV_KO_LIST+=" $svc"
    fi
done

# libvirt traité à part (deux noms possibles selon la version)
if systemctl is-active --quiet libvirtd 2>/dev/null || systemctl is-active --quiet virtqemud 2>/dev/null; then
    log_ok "Service actif : libvirt (libvirtd ou virtqemud)"
    SRV_OK=$((SRV_OK + 1))
else
    log_warn "Service inactif : libvirt (ni libvirtd ni virtqemud)"
    SRV_KO=$((SRV_KO + 1))
    SRV_KO_LIST+=" libvirt"
fi

# Vérification binaire ansible (pas un service)
if command -v ansible &>/dev/null; then
    log_ok "Ansible installé ($(ansible --version | head -1))"
else
    log_warn "Ansible absent"
fi

SRV_TOTAL=$((SRV_OK + SRV_KO))
if [ "$SRV_KO" -eq 0 ]; then
    log_ok "Services critiques : $SRV_OK/$SRV_TOTAL actifs"
else
    log_warn "Services critiques : $SRV_OK/$SRV_TOTAL actifs — inactifs :$SRV_KO_LIST"
fi

}

################################################################################
# 21. NETTOYAGE INTERMÉDIAIRE
################################################################################

section_21() {


apt autoremove -y && apt autoclean -y
log_ok "Nettoyage effectué"

}

################################################################################
# 22. CONFIGURATION POSTFIX RELAY GMAIL
################################################################################

section_22() {


if ! command -v postfix &>/dev/null; then
    log_warn "Postfix absent, installation forcée..."
    # Type « select » (v8.9.1) : main_mailer_type est un template select,
    # le type « string » ne préchargeait pas la réponse → écran interactif
    debconf-set-selections <<< "postfix postfix/main_mailer_type select Internet Site"
    debconf-set-selections <<< "postfix postfix/mailname string $(hostname)"
    DEBIAN_FRONTEND=noninteractive apt install -y postfix mailutils \
        || log_error "Installation Postfix échouée — relay Gmail non configuré"
    hash -r
fi

# Plus d'exit 1 ici (v8.8.32) : sans Postfix on saute la configuration du relay
# mais les sections suivantes (23-31, dont le résumé final) s'exécutent.
if command -v postfix &>/dev/null; then
    mkdir -p /etc/postfix

    SASL_PASSWD="/etc/postfix/sasl_passwd"

    # umask 077 : sasl_passwd contient le mot de passe Gmail App — créé directement en 600
    (
        umask 077
        # printf %s : pas d'interprétation de $, ` ou \ dans le mot de passe applicatif Gmail
        printf '[smtp.gmail.com]:587\t%s:%s\n' "$GMAIL_USER" "$GMAIL_APP_PASSWORD" > "$SASL_PASSWD"
    )
    chown root:root "$SASL_PASSWD"

    # postmap génère .db hérite des permissions du parent (postfix gère lui-même)
    postmap "$SASL_PASSWD" \
        && chmod 600 "${SASL_PASSWD}.db" \
        || log_error "postmap échoué — relay Gmail cassé, .db absent"

    cat <<'EOF' > /etc/postfix/main.cf
# ---------------- Configuration de base ----------------
compatibility_level = 3.6
myhostname = ma-machine.local
mydestination = $myhostname, localhost
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
inet_interfaces = loopback-only
inet_protocols = all
# ---------------- Pas de réception de mails ----------------
biff = no
append_dot_mydomain = no
readme_directory = no
# ---------------- Relay vers Gmail (smarthost) ----------------
relayhost = [smtp.gmail.com]:587
# ---------------- Authentification SASL ----------------
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_mechanism_filter = plain, login
# ---------------- Sécurité TLS ----------------
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# ---------------- Restrictions ----------------
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
# ---------------- Divers ----------------
mailbox_size_limit = 0
recipient_delimiter = +
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
EOF

    # myhostname depuis le .conf (HOSTNAME_MACHINE — v8.9.0) : le heredoc
    # ci-dessus est quoté ('EOF', pour préserver ${data_directory} destiné à
    # postfix), d'où le sed après écriture plutôt qu'une expansion directe
    sed -i "s/^myhostname = .*/myhostname = ${HOSTNAME_MACHINE}.local/" /etc/postfix/main.cf

    if grep -q "^root:" /etc/aliases; then
        sed -i "s|^root:.*|root:    $UTILISATEUR|" /etc/aliases
    else
        echo "root:    $UTILISATEUR" >> /etc/aliases
    fi
    newaliases || log_warn "newaliases échoué — vérifier /etc/aliases"

    systemctl enable --now postfix || log_warn "Postfix enable échoué"
    systemctl restart postfix || log_warn "Postfix restart échoué"

    echo "Test Postfix post-install $(date)" | mail -s "[postinstall] Postfix OK - $(hostname)" "$GMAIL_USER" \
        && log_ok "Mail de test envoyé à $GMAIL_USER" \
        || log_warn "Envoi test échoué – vérifier : journalctl -u postfix -n 30"

    log_ok "Postfix configuré (relay Gmail)"
else
    log_error "Postfix indisponible — section 22 ignorée (rapport final par mail indisponible)"
fi

}

################################################################################
# 23. INSTALLATION CLAUDE CODE
################################################################################

section_23() {


# Node.js >= 18 requis par Claude Code (v8.9.1) : au test réel, npm et
# l'installation Claude Code échouaient — la version nodejs des dépôts Ubuntu
# peut être trop ancienne → passage par le dépôt NodeSource (Node 22 LTS)
node_ver=$(node --version 2>/dev/null | grep -oP '^v\K[0-9]+') || true
if [[ -z "${node_ver:-}" || "${node_ver}" -lt 18 ]]; then
    log_info "Node.js absent ou < 18 (version : ${node_ver:-absente}) — installation via NodeSource 22.x"
    if curl -fsSL https://deb.nodesource.com/setup_22.x | bash -; then
        apt install -y nodejs || log_warn "nodejs (NodeSource) : installation échouée"
        hash -r
    else
        log_warn "Dépôt NodeSource : configuration échouée — npm/Claude Code risquent d'échouer"
    fi
fi

# Mise à jour npm vers version stable
npm install -g npm@latest || log_warn "Mise à jour npm échouée, continuation..."

# Installation Claude Code via npm
if npm install -g @anthropic-ai/claude-code; then
    log_ok "Claude Code installé ($(claude --version 2>/dev/null || echo 'version inconnue'))"
    log_ok "Lancer avec : claude"
    log_ok "Authentification requise au premier lancement : claude auth"
else
    log_warn "Installation Claude Code échouée — installer manuellement après reboot : npm install -g @anthropic-ai/claude-code"
fi

# caveman (plugin Claude Code - réduction tokens) — déplacé ici en v8.8.32 :
# en section 2a il s'installait AVANT Claude Code et échouait systématiquement
# --yes + </dev/null (v8.9.2) : npx demandait « Ok to proceed? » et attendait —
# blocage indiagnosticable car sudo-rs (26.04) alloue un NOUVEAU pty : la
# réponse tapée dans le terminal n'atteint jamais le prompt. </dev/null coupe
# aussi tout prompt ultérieur de claudepluginhub (échec géré par log_warn).
sudo -u "$UTILISATEUR" env PATH="$NPM_PATH" \
    npx --yes claudepluginhub juliusbrussee/caveman --plugin caveman </dev/null \
    && log_ok "Plugin caveman installé" \
    || log_warn "Plugin caveman : installation échouée (lancer manuellement après connexion)"

}

################################################################################
# 24. INSTALLATION VERACRYPT
################################################################################

section_24() {


VERACRYPT_VER=$(curl -s "https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest" \
    | grep '"tag_name"' | grep -oP '(?<=VeraCrypt_)[\d.]+') || true

if [ -n "$VERACRYPT_VER" ]; then
    # Détection version Ubuntu + fallback (VeraCrypt suit la LTS, pas la dernière release)
    # lire_os_release (v8.9.3) : le sourcing était tué par « readonly VERSION »
    # → « aucun paquet trouvé pour Ubuntu /24.04/22.04 » aux tests réels
    UBUNTU_VER=$(lire_os_release VERSION_ID)
    if [[ -z "$UBUNTU_VER" ]]; then
        UBUNTU_VER="26.04"
        log_warn "VERSION_ID vide dans /etc/os-release — fallback 26.04"
    fi
    INSTALLED=0
    for v in "$UBUNTU_VER" "24.04" "22.04"; do
        VERACRYPT_DEB="veracrypt-console-${VERACRYPT_VER}-Ubuntu-${v}-amd64.deb"
        URL="https://github.com/veracrypt/VeraCrypt/releases/download/VeraCrypt_${VERACRYPT_VER}/${VERACRYPT_DEB}"
        if wget -q --spider "$URL"; then
            wget -q "$URL" -O /tmp/veracrypt.deb \
                && apt install -y /tmp/veracrypt.deb \
                && log_ok "VeraCrypt ${VERACRYPT_VER} installé (paquet Ubuntu-${v})" \
                && INSTALLED=1 \
                && break \
                || log_warn "VeraCrypt : install paquet Ubuntu-${v} échouée — tentative suivante"
        fi
    done
    [ "$INSTALLED" -eq 0 ] && log_warn "VeraCrypt : aucun paquet trouvé pour Ubuntu ${UBUNTU_VER}/24.04/22.04"
    rm -f /tmp/veracrypt.deb
else
    log_warn "VeraCrypt : version introuvable via GitHub — installer manuellement depuis https://www.veracrypt.fr"
fi

}

################################################################################
# 25. INSTALLATION FORTUNE-MOD + COWSAY + FORTUNES FRANÇAISES
################################################################################

section_25() {


# Bibliothèque de citations PERSO (v8.9.14) : fortunes-fr a disparu des dépôts
# Ubuntu 26.04 et le .deb du pool 24.04 finira par disparaître aussi — les
# 30 catégories de textes (contenu GPL du paquet) sont désormais VERSIONNÉES
# dans le dépôt git du NAS, à côté de ce script. Installation : copie des
# textes + index .dat régénérés par strfile (fortune-mod, toujours dans les
# dépôts). Plus AUCUNE dépendance à un paquet fortunes-fr.
# Source des citations : le dépôt git du NAS (source de vérité, toujours à
# jour), avec REPLI sur une copie locale fortunes-fr/ à côté du script si le
# NAS est indisponible (v8.9.17). Surchargeable via FORTUNES_FR_SRC.
if [[ -z "${FORTUNES_FR_SRC:-}" ]]; then
    FORTUNES_FR_SRC="/mnt/nas/sauvegarde/git/scripts/Postinstall_Ubuntu_26.04/Script_avec_variables_dans_un_fichier_conf/fortunes-fr"
    if [[ ! -d "$FORTUNES_FR_SRC" ]]; then
        FORTUNES_FR_SRC="$(dirname "$(realpath "$0")")/fortunes-fr"
        log_info "Source NAS indisponible — repli sur la copie locale : $FORTUNES_FR_SRC"
    fi
fi
FORTUNES_FR_DEST="/usr/share/games/fortunes/fr"
if [[ -d "$FORTUNES_FR_SRC" ]] && command -v strfile &>/dev/null; then
    mkdir -p "$FORTUNES_FR_DEST"
    nb_categories=0
    for src in "$FORTUNES_FR_SRC"/*; do
        [[ -f "$src" ]] || continue
        nom=$(basename "$src")
        cp "$src" "$FORTUNES_FR_DEST/$nom" \
            && strfile "$FORTUNES_FR_DEST/$nom" "$FORTUNES_FR_DEST/$nom.dat" >/dev/null 2>&1 \
            && nb_categories=$((nb_categories + 1)) \
            || log_warn "fortunes-fr : échec sur la catégorie $nom"
    done
    if [[ $nb_categories -gt 0 ]]; then
        log_ok "Fortune-mod + cowsay + citations françaises installés ($nb_categories catégories depuis le NAS)"
    else
        log_warn "fortunes-fr : aucune catégorie installée depuis $FORTUNES_FR_SRC"
    fi
else
    log_warn "fortunes-fr : source NAS introuvable ($FORTUNES_FR_SRC) ou strfile absent — citations françaises non installées"
fi

}

################################################################################
# 26. INSTALLATION DE L'AGENT DWSERVICE
################################################################################

section_26() {


# echo "1" (v8.9.1) : l'installeur affiche un menu (1=Installer, 2=Exécuter,
# 3=Refuser) et attend une réponse — stdin indisponible via « | tee »,
# le script restait bloqué indéfiniment. Réponse « Installer » passée en stdin.
# log_ok conditionné à la réussite effective (message mensonger au test réel).
if curl -fsSL https://www.dwservice.net/download/dwagent.sh -o /tmp/dwagent.sh \
    && chmod +x /tmp/dwagent.sh \
    && echo "1" | /tmp/dwagent.sh -console; then
    log_ok "Agent DWService installé en mode console"
else
    log_warn "DWService : téléchargement ou installation échoué"
fi
rm -f /tmp/dwagent.sh

}

################################################################################
# 26b. GNOME REMOTE DESKTOP — RDP NATIF WAYLAND (v8.9.16)
# Deux modes complémentaires (identifiants RDP_USER/RDP_PASS depuis le .conf) :
#   - SYSTÈME (headless) : connexion possible dès l'écran de login, sans
#     session ouverte — administration à distance de la machine.
#   - SESSION : partage de la session graphique ouverte de $UTILISATEUR.
# GNOME gère le port 3389 en handover : le daemon système délègue au daemon
# de session quand une session partagée est active.
################################################################################

section_26b() {


# Section sautée proprement si les identifiants manquent (rétro-compatibilité
# avec les .conf antérieurs à v8.9.16 — variables non critiques)
if [[ -z "${RDP_USER:-}" || -z "${RDP_PASS:-}" ]]; then
    log_warn "RDP_USER/RDP_PASS absents du .conf — GNOME Remote Desktop non configuré"
    return 0
fi

# Préinstallé sur Ubuntu 26.04 desktop — installation de rattrapage sinon
paquet_est_installe gnome-remote-desktop \
    || apt install -y gnome-remote-desktop \
    || log_warn "gnome-remote-desktop : installation échouée"

# Wrapper : commande exécutée dans le contexte utilisateur (bus + runtime)
grd_user() {
    sudo -u "$UTILISATEUR" env \
        DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${USER_UID}/bus" \
        XDG_RUNTIME_DIR="/run/user/${USER_UID}" "$@"
}

# ---- Mode SYSTÈME (headless) ----
# Le daemon système n'écoute PAS sans certificat TLS (vérifié au test réel :
# « TLS certificate: (null) », port 3389 fermé) → auto-signé 10 ans dans
# /var/lib/gnome-remote-desktop (home de l'utilisateur de service).
GRD_DIR="/var/lib/gnome-remote-desktop"
if [[ ! -f "$GRD_DIR/tls.crt" ]]; then
    openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
        -subj "/CN=${HOSTNAME_MACHINE}" \
        -out "$GRD_DIR/tls.crt" -keyout "$GRD_DIR/tls.key" 2>/dev/null \
        && log_ok "Certificat TLS RDP système généré (auto-signé, 10 ans)" \
        || log_warn "RDP système : génération du certificat TLS échouée"
    chown gnome-remote-desktop:gnome-remote-desktop "$GRD_DIR/tls.crt" "$GRD_DIR/tls.key" 2>/dev/null || true
    chmod 600 "$GRD_DIR/tls.key" 2>/dev/null || true
fi
grdctl --system rdp set-tls-cert "$GRD_DIR/tls.crt" || log_warn "RDP système : set-tls-cert échoué"
grdctl --system rdp set-tls-key "$GRD_DIR/tls.key" || log_warn "RDP système : set-tls-key échoué"
grdctl --system rdp set-credentials "$RDP_USER" "$RDP_PASS" || log_warn "RDP système : set-credentials échoué"
grdctl --system rdp enable || log_warn "RDP système : enable échoué"
systemctl enable --now gnome-remote-desktop.service || log_warn "RDP : service système non démarré"
systemctl restart gnome-remote-desktop.service || true
sleep 2
if ss -tln | grep -q ':3389'; then
    log_ok "RDP headless actif — port 3389 en écoute (connexion possible dès l'écran de login)"
else
    log_warn "RDP : port 3389 pas en écoute — vérifier : journalctl -u gnome-remote-desktop"
fi

# ---- Mode SESSION (partage de la session de $UTILISATEUR) ----
# Le daemon de session a son propre certificat (~/.local/share/…). Les
# credentials vont dans le TROUSSEAU : il doit être déverrouillé — OK quand le
# script est lancé depuis la session graphique ; sinon (SSH) le set-credentials
# échoue (« locked collection ») → consigne manuelle loguée.
GRD_USER_DIR="/home/$UTILISATEUR/.local/share/gnome-remote-desktop"
sudo -u "$UTILISATEUR" mkdir -p "$GRD_USER_DIR"
if [[ ! -f "$GRD_USER_DIR/rdp-tls.crt" ]]; then
    sudo -u "$UTILISATEUR" openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
        -subj "/CN=${HOSTNAME_MACHINE}" \
        -out "$GRD_USER_DIR/rdp-tls.crt" -keyout "$GRD_USER_DIR/rdp-tls.key" 2>/dev/null \
        && sudo -u "$UTILISATEUR" chmod 600 "$GRD_USER_DIR/rdp-tls.key" \
        && log_ok "Certificat TLS RDP session généré" \
        || log_warn "RDP session : génération du certificat échouée"
fi
grd_user grdctl rdp set-tls-cert "$GRD_USER_DIR/rdp-tls.crt" || log_warn "RDP session : set-tls-cert échoué"
grd_user grdctl rdp set-tls-key "$GRD_USER_DIR/rdp-tls.key" || log_warn "RDP session : set-tls-key échoué"
if grd_user grdctl rdp set-credentials "$RDP_USER" "$RDP_PASS" 2>/dev/null; then
    log_ok "RDP session : identifiants enregistrés dans le trousseau"
else
    log_warn "RDP session : trousseau verrouillé — après ouverture de session, lancer : grdctl rdp set-credentials $RDP_USER '<mot de passe du .conf>'"
fi
grd_user grdctl rdp enable || log_warn "RDP session : enable échoué"
grd_user grdctl rdp disable-view-only || true
grd_user systemctl --user enable --now gnome-remote-desktop.service 2>/dev/null || true

log_ok "GNOME Remote Desktop configuré — client RDP (Remmina/mstsc) vers ${HOSTNAME_MACHINE}:3389"

}

################################################################################
# 27. CONFIGURATION CRONTAB RAPPELS QUOTIDIENS + SCAN CLAMAV HEBDOMADAIRE
# Note 26.04 : Ubuntu 26.04 est Wayland-only (GNOME 50).
#   - DISPLAY=:0 est conservé pour XWayland (les apps X11 continuent de fonctionner)
#   - WAYLAND_DISPLAY=wayland-0 est ajouté pour notify-send natif Wayland
#   - Les deux variables garantissent la compatibilité dans les deux cas
################################################################################

section_27() {


# Rappels user : fichier dans /etc/cron.d/ (idempotent, ne touche pas au crontab user)
# Format /etc/cron.d : <minute> <heure> <jour> <mois> <jour_semaine> <user> <commande>
CRON_RAPPELS="/etc/cron.d/postinstall-rappels"
DBUS_ADDR="unix:path=/run/user/${USER_UID}/bus"
NOTIFY_ENV="DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=${DBUS_ADDR}"

{
    echo "# Géré par postinstall.26.04_v2.sh — ne pas modifier"
    echo "# Rappels boulot (lun-ven 04h00 → 09h00, toutes les 30 min) et coucher (22h30 → 02h00)"
    echo "# DISPLAY=:0 (XWayland) + WAYLAND_DISPLAY=wayland-0 → compatibilité GNOME 50 Wayland"
    echo "SHELL=/bin/bash"
    echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    echo ""
    echo "# === Rappels matin (boulot) — lun-ven, 04:00 à 09:00 par tranches de 30 min ==="
    for h in 4 5 6 7 8; do
        for m in 0 30; do
            echo "${m} ${h} * * 1-5 ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au boulot, il est l'heure]\" --hint=string:sound-name:alarm-clock-elapsed"
        done
    done
    echo "0 9 * * 1-5 ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au boulot, il est l'heure]\" --hint=string:sound-name:alarm-clock-elapsed"
    echo ""
    echo "# === Rappels soir (coucher) — tous les jours, 22:30 à 02:00 ==="
    echo "30 22 * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
    for h in 23 0 1; do
        for m in 0 30; do
            echo "${m} ${h} * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
        done
    done
    echo "0 2 * * * ${UTILISATEUR} ${NOTIFY_ENV} notify-send \"[Prépare toi à aller au lit, il est tard]\" --hint=string:sound-name:alarm-clock-elapsed"
} > "$CRON_RAPPELS"

chown root:root "$CRON_RAPPELS"
chmod 644 "$CRON_RAPPELS"
log_ok "Crontab rappels installé dans $CRON_RAPPELS (ne touche pas au crontab user)"

# Crontab root : scan ClamAV hebdomadaire — fichier dédié /etc/cron.d/ (préserve crontab root existant)
# Tous les % sont échappés (\%) car cron interprète % non-escape comme fin de commande + newline.
CRON_CLAMAV="/etc/cron.d/postinstall-clamav"
cat > "$CRON_CLAMAV" << EOF
# Géré par postinstall.26.04_v2.sh — Scan antivirus ClamAV hebdomadaire
# samedi 14h00 (freshclam) + 14h30 (scan système local)
# /mnt exclu : le NAS possède son propre antivirus
# Note 26.04 : /run/media remplace /media pour les montages auto de périphériques externes
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

0 14 * * 6 root /usr/bin/freshclam --quiet 2>/dev/null || true
30 14 * * 6 root LOG=/tmp/clamav-\$(date +\%Y\%m\%d).log; /usr/bin/clamscan -r -i --exclude-dir="^/mnt" --exclude-dir="^/media" --exclude-dir="^/run/media" --exclude-dir="^/snap" --exclude-dir="^/proc" --exclude-dir="^/sys" --exclude-dir="^/dev" --exclude-dir="^/run" --exclude-dir="^/boot" --exclude-dir="^/var/lib/docker" --exclude-dir="^/var/cache/apt" / > "\$LOG" 2>&1; find /tmp -name 'clamav-*.log' -mtime +30 -delete 2>/dev/null; grep -q "FOUND" "\$LOG" && printf 'Menaces detectees sur \%s le \%s :\n\n\%s\n\nLog complet : \%s\n' "\$(hostname)" "\$(date +'\%d/\%m/\%Y \%H:\%M')" "\$(grep FOUND \"\$LOG\")" "\$LOG" | mail -s "[ClamAV] Menace detectee sur \$(hostname)" $GMAIL_USER
EOF

chown root:root "$CRON_CLAMAV"
chmod 644 "$CRON_CLAMAV"
log_ok "Scan ClamAV hebdomadaire planifié dans $CRON_CLAMAV (sam. 14h00 freshclam + 14h30 scan)"

# Activation explicite des services ClamAV (v8.9.3) : sur 26.04, le paquet
# laisse clamav-freshclam DISABLED — le service tournait pendant l'installation
# (démarré par le postinst du paquet) mais ne revenait pas après reboot
systemctl enable --now clamav-freshclam \
    && log_ok "clamav-freshclam activé (mise à jour des signatures au boot)" \
    || log_warn "clamav-freshclam : activation échouée"
systemctl enable --now clamav-daemon \
    || log_warn "clamav-daemon : activation échouée"

}

################################################################################
# 28. CONFIGURATION OPENVPN (Suisse + USA + France + Russie)
################################################################################

section_28() {


# ====================== FONCTION ======================
# Les certificats / clés / CA sont stockés dans postinstall.26.04_v2.conf (v8.8.24)
# Le mapping nom → remote CyberGhost et indirection variable est fait via nameref bash.
configure_vpn() {
    local name="$1"
    local upper="${name^^}"
    local user pass ovpn ca cert key remote

    case "$name" in
        Suisse) remote="87-1-ch.cg-dialup.net 443" ;;
        USA)    remote="87-1-us.cg-dialup.net 443" ;;
        France) remote="87-1-fr.cg-dialup.net 443" ;;
        Russie) remote="87-1-ru.cg-dialup.net 443" ;;
        *)      echo "Erreur : VPN inconnu ($name)"; return 1 ;;
    esac

    # Récupération via indirection (variables sourcées depuis le .conf)
    local user_var="VPN_${upper}_USER" pass_var="VPN_${upper}_PASS"
    local cert_var="VPN_${upper}_CERT" key_var="VPN_${upper}_KEY"
    user="${!user_var}"
    pass="${!pass_var}"
    cert="${!cert_var}"
    key="${!key_var}"
    ca="$VPN_COMMON_CA"
    # OVPN spécifique = OVPN commun + ligne remote propre au serveur
    ovpn="$VPN_COMMON_OVPN
remote $remote"

    # Utiliser /home/$UTILISATEUR — $HOME vaut /root quand on tourne en sudo
    local cert_dir="/home/$UTILISATEUR/.cert/nm-openvpn-${name,,}"
    mkdir -p "$cert_dir"
    chown "$UTILISATEUR":"$UTILISATEUR" "$cert_dir"
    chmod 700 "$cert_dir"

    log_info "Configuration VPN : $name"

    # Idempotence : supprimer toute connexion préexistante portant ce nom
    # ou nommée "openvpn" résiduelle d'un import précédent
    nmcli connection show "$name"    &>/dev/null && nmcli connection delete "$name"    >/dev/null 2>&1
    nmcli connection show "openvpn"  &>/dev/null && nmcli connection delete "openvpn"  >/dev/null 2>&1

    # umask 077 sur le répertoire temporaire : clé privée jamais en 644
    local tmp_dir
    tmp_dir=$(mktemp -d)
    chmod 700 "$tmp_dir"
    (
        umask 077
        printf '%s\n' "$ovpn" > "$tmp_dir/openvpn.ovpn"
        printf '%s\n' "$ca"   > "$tmp_dir/ca.crt"
        printf '%s\n' "$cert" > "$tmp_dir/client.crt"
        printf '%s\n' "$key"  > "$tmp_dir/client.key"
    )

    nmcli connection import type openvpn file "$tmp_dir/openvpn.ovpn" >/dev/null 2>&1 || true
    sleep 1

    # Renommer la connexion "openvpn" (nom auto issu du fichier .ovpn) en "$name"
    if nmcli -g NAME connection show | grep -qx "openvpn"; then
        nmcli connection modify "openvpn" connection.id "$name" 2>/dev/null || true
    fi

    # Copier les fichiers de certificats dans cert_dir (umask déjà 077)
    (
        umask 077
        cp "$tmp_dir/ca.crt"     "$cert_dir/${name}-ca.crt"
        cp "$tmp_dir/client.crt" "$cert_dir/${name}-client.crt"
        cp "$tmp_dir/client.key" "$cert_dir/${name}-client.key"
    )
    chown "$UTILISATEUR":"$UTILISATEUR" "$cert_dir/${name}-"*

    # Configurer vpn.data SANS le password (pas exposé dans ps aux)
    nmcli connection modify "$name" \
        vpn.data "ca=$cert_dir/${name}-ca.crt,cert=$cert_dir/${name}-client.crt,key=$cert_dir/${name}-client.key,connection-type=password-tls,username=$user,password-flags=0,auth=SHA256,remote-cert-tls=server,ping=5" \
        || log_warn "nmcli modify VPN $name échoué"

    # Password injecté via l'éditeur nmcli (commandes set/save/quit sur stdin —
    # secret absent de la ligne de commande, donc invisible dans ps aux).
    # Repli sur `modify +vpn.secrets` si l'éditeur échoue (le secret y est alors
    # visible dans ps aux, mais seulement en dernier recours).
    printf 'set vpn.secrets password %s\nsave\nquit\n' "$pass" \
        | nmcli connection edit "$name" >/dev/null 2>&1 \
        || nmcli connection modify "$name" +vpn.secrets "password=$pass" >/dev/null 2>&1 \
        || log_warn "Injection password VPN $name échouée"

    nmcli connection reload || log_warn "nmcli reload VPN $name échoué"

    # Garde : ne jamais lancer rm -rf sur une variable potentiellement vide
    [ -n "${tmp_dir:-}" ] && rm -rf "$tmp_dir"
    log_ok "VPN $name configuré"
}

# ====================== EXÉCUTION ======================
configure_vpn "Suisse"
configure_vpn "USA"
configure_vpn "France"
configure_vpn "Russie"

log_ok "Configuration OpenVPN terminée (Suisse + USA + France + Russie)"
log_info "Aliases disponibles : vpnsuisse, vpnusa, vpnfr, vpnru, vpnoff, vpnstat"

}

################################################################################
# 29. NETTOYAGE FINAL AVANT REBOOT
################################################################################

section_29() {


apt autoremove -y && apt autoclean -y
log_ok "Système nettoyé"

}

################################################################################
# 29b. VALIDATION POST-REBOOT AUTOMATIQUE (TIMER + MAIL)
################################################################################

section_29b() {

# Au premier boot suivant la post-installation, validate_postinstall.sh est
# exécuté automatiquement (en ${UTILISATEUR}, comme prévu par son en-tête) et
# son rapport est envoyé par mail, puis le timer se désactive (one-shot).
VALIDATE_SRC="$(dirname "$(realpath "$0")")/validate_postinstall.sh"
if [ ! -f "$VALIDATE_SRC" ]; then
    log_warn "validate_postinstall.sh introuvable à côté du script — validation post-reboot non planifiée"
    return 0
fi

install -m 0755 -o root -g root "$VALIDATE_SRC" /usr/local/bin/validate-postinstall.sh
log_ok "validate-postinstall.sh installé dans /usr/local/bin"

# Wrapper exécuté en root par le service : lance la validation via runuser
# (le script de validation est conçu pour ${UTILISATEUR} + sudo NOPASSWD),
# retire les codes couleur ANSI pour le mail, puis désactive le timer.
# Heredoc non quoté : VERSION/CONF_FILE/UTILISATEUR/GMAIL_USER figés à la
# génération ; les \$ sont évalués à l'exécution post-reboot.
cat > /usr/local/bin/postinstall-validation-mail.sh << EOF
#!/bin/bash
# Validation post-reboot — généré par postinstall.26.04_v2.sh v${VERSION}
# Lancé une seule fois par postinstall-validation.timer au boot suivant
set -uo pipefail

RAPPORT_BRUT=\$(mktemp)
RAPPORT="/var/log/postinstall/validation-mail-\$(date +%Y%m%d_%H%M%S).txt"

# POSTINSTALL_CONF : le .conf est resté là où la post-installation a été lancée
export POSTINSTALL_CONF="${CONF_FILE}"
runuser -u ${UTILISATEUR} -- /usr/local/bin/validate-postinstall.sh > "\$RAPPORT_BRUT" 2>&1
rc=\$?

# Suppression des séquences ANSI (couleurs) pour un mail lisible
sed -e 's/\x1b\[[0-9;]*m//g' "\$RAPPORT_BRUT" > "\$RAPPORT"
rm -f "\$RAPPORT_BRUT"

if [ "\$rc" -eq 0 ]; then statut="OK"; else statut="ECHEC (FAIL detectes)"; fi
mail -s "[postinstall] Validation post-reboot \$statut - \$(hostname)" "${GMAIL_USER}" < "\$RAPPORT" \\
    || echo "Envoi du mail de validation échoué — rapport conservé : \$RAPPORT"

# Auto-désactivation : la validation ne tourne qu'au premier boot suivant
systemctl disable --now postinstall-validation.timer 2>/dev/null || true
exit 0
EOF
chmod 755 /usr/local/bin/postinstall-validation-mail.sh
log_ok "Wrapper postinstall-validation-mail.sh créé"

cat > /etc/systemd/system/postinstall-validation.service << 'EOF'
[Unit]
Description=Validation post-installation (rapport par mail)
Wants=network-online.target
After=network-online.target postfix.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/postinstall-validation-mail.sh
EOF

cat > /etc/systemd/system/postinstall-validation.timer << 'EOF'
[Unit]
Description=Validation post-installation au premier boot suivant

[Timer]
OnBootSec=4min

[Install]
WantedBy=timers.target
EOF

# enable SANS --now : OnBootSec=4min du boot COURANT est déjà dépassé, --now
# déclencherait la validation immédiatement (avant le reboot) — on veut
# qu'elle parte 4 min après le PROCHAIN démarrage.
systemctl daemon-reload
systemctl enable postinstall-validation.timer >> "$LOG_FILE" 2>&1 \
    && log_ok "Validation post-reboot planifiée (4 min après le prochain démarrage, rapport → ${GMAIL_USER})" \
    || log_warn "postinstall-validation.timer : activation échouée"

}

################################################################################
# 30. RÉSUMÉ FINAL + EMPLACEMENT DU LOG
################################################################################

section_30() {


# ---- Calcul de la durée totale d'exécution (U1 v8.8.24) ----
DUREE=$(( $(date +%s) - START ))
DUREE_FMT=$(printf '%dh %02dm %02ds' $((DUREE/3600)) $((DUREE%3600/60)) $((DUREE%60)))

echo ""
echo "=========================================="
echo "   POST-INSTALLATION TERMINÉE (v${VERSION}) !"
echo "     Ubuntu 26.04 LTS — Resolute Raccoon"
echo "     ⏱  Durée totale : $DUREE_FMT"
echo "=========================================="
echo ""
echo "📋 ACTIONS RÉALISÉES (par catégorie — U2 v8.8.24) :"
echo ""
echo "  🖥  SYSTÈME"
echo "    • /home monté sur LVM (UUID) + noatime SSD sur / et /home"
echo "    • GRUB dual-boot configuré (patch_grub idempotent)"
echo "    • Sudo NOPASSWD pour $UTILISATEUR"
echo "    • Dossiers XDG locaux (Documents, Musique, Images, Vidéos dans ~/)"
echo "    • Historique Bash optimisé + prompt Git dynamique"
echo "    • Journal systemd limité à 500 Mo"
echo "    • Mises à jour quotidiennes auto + nettoyage hebdomadaire"
echo ""
echo "  🌐  RÉSEAU & NAS"
echo "    • DNS Google (8.8.8.8 + 8.8.4.4)"
echo "    • ${#NAS_PARTAGES[@]} partages NAS montés (CIFS) + signets file:///mnt/nas/"
echo "    • VPN OpenVPN : Suisse + USA + France + Russie (certs depuis .conf)"
echo "    • SSH activé"
echo ""
echo "  🔐  SÉCURITÉ"
echo "    • UFW désactivé (choix utilisateur)"
echo "    • ClamAV installé + scan hebdomadaire (samedi 14h30)"
echo "    • VeraCrypt + scripts ~/.mount_1984.sh / ~/.umount_1984.sh"
echo "    • Postfix relay Gmail configuré (→ $GMAIL_USER)"
echo "    • Mises à jour sécurité automatiques (unattended-upgrades)"
echo ""
echo "  🛠  OUTILS DEV"
echo "    • Docker Engine (dernière version)"
echo "    • KVM/libvirt + virt-manager"
echo "    • Ansible, Vim, Git, nodejs, npm, jq, tree, ncdu, pv"
echo "    • Claude Code (npm global) + ccstatusline + caveman plugin"
echo ""
echo "  🎬  MULTIMÉDIA & BUREAU"
echo "    • Navigateurs : Chrome, Chromium (snap), Brave, Opera, Firefox snap"
echo "    • Flatpak : Discord, VLC, Bambu Studio, Bottles, ClamUI, Telegram, MuseScore"
echo "    • yt-dlp (binaire GitHub) + alias mp3dl"
echo "    • GNOME : Nautilus, extensions (gsconnect, clipboard-indicator — dash-to-dock : manuel)"
echo "    • Ptyxis thème Solarized Dark, horloge (secondes + jour)"
echo "    • fortune fr | cowsay | lolcat à chaque terminal"
echo ""
echo "  📡  SUPERVISION (Centreon)"
echo "    • NRPE actif sur port 5666 (allowed: 192.168.1.50, 192.168.122.10)"
echo "    • SNMP configuré"
echo "    • Centreon Monitoring Agent (CMA) installé"
echo "    • Groupe monitoring + sudoers restreints (smartctl, nft, iptables, ...)"
echo ""
echo "🎵 yt-dlp — télécharger une vidéo en MP3 :"
echo "   mp3dl https://www.youtube.com/watch?v=..."
echo ""
echo "🤖 Claude Code — lancer avec : claude  (authentif. au 1er lancement : claude auth)"
echo ""
echo "🖥  NOTES UBUNTU 26.04 (Resolute Raccoon) :"
echo "   • Terminal par défaut : Ptyxis (thème Solarized si Ptyxis lancé avant ce script)"
echo "   • Chromium via snap, MuseScore via Flatpak"
echo "   • Wayland-only — gnome-screensaver remplacé par loginctl lock-session"
echo ""
echo "📄 LOG COMPLET : $LOG_FILE"
echo "   Erreurs/warnings : grep -E 'WARN|ERREUR' $LOG_FILE"
echo ""
echo "🔧 APRÈS REBOOT :"
echo "  • claude auth → authentifier Claude Code"
echo "  • Steam → Paramètres → Stockage → ajouter bibliothèque (~/snap/steam/common/.local/share/Steam/steamapps)"
echo "  • GSConnect → appairer avec KDE Connect Android"
echo "  • VirtualBox (Oracle 7.2) → Additions Invité incluses (menu Périphériques) ;"
echo "    se reconnecter pour activer le groupe vboxusers (USB)"
echo "  • DWService → lancer 'dwagent' et entrer le code"
echo "  • Dash to Dock → installer via Extension Manager (absent des dépôts 26.04)"
echo ""

log_ok "Post-installation v${VERSION} terminée en $DUREE_FMT — log : $LOG_FILE"

# ---- Récapitulatif des anomalies (fonction partagée avec le trap INT/TERM) ----
afficher_anomalies

# Envoi du log complet par email
log_section "Envoi du rapport par email"
# Construire un résumé court (pas le log brut complet — fuite de données)
# Inclut uniquement : statut global, anomalies, et les lignes [OK/WARN/ERREUR]
RAPPORT="/tmp/postinstall-rapport-$(date +%Y%m%d_%H%M%S).txt"
{
    echo "Post-installation v${VERSION} — $(hostname) — $(date '+%F %T') — Durée : $DUREE_FMT"
    echo ""
    if [ ${#ANOMALIES[@]} -eq 0 ]; then
        echo "Statut : ✅ AUCUNE ANOMALIE"
    else
        echo "Statut : ❌ ${#ANOMALIES[@]} ANOMALIE(S)"
        echo ""
        echo "--- Anomalies ---"
        printf '  %s\n' "${ANOMALIES[@]}"
    fi
    echo ""
    echo "--- Dernières 50 entrées OK/WARN/ERREUR ---"
    grep -E '\[(OK|WARN|ERREUR)\]' "$LOG_FILE" | tail -50 || true
    echo ""
    echo "Log complet sur $(hostname) : $LOG_FILE"
} > "$RAPPORT"

mail -s "[postinstall] Rapport v${VERSION} - $(hostname)" "$GMAIL_USER" < "$RAPPORT" \
    && log_ok "Rapport envoyé à $GMAIL_USER" \
    || log_warn "Envoi du rapport échoué — vérifier Postfix"

rm -f "$RAPPORT"

}

################################################################################
# 31. REBOOT
################################################################################

section_31() {


# Désarmement du trap INT/TERM (dispatcher) : pendant le compte à rebours,
# Ctrl+C doit ANNULER le reboot — pas déclencher le récapitulatif d'interruption
trap - INT TERM

if [[ "${REBOOT_AUTO:-true}" == "true" ]]; then
    echo "Redémarrage dans 20 secondes (Ctrl+C pour annuler)..."
    for i in {20..1}; do
        printf "\rRedémarrage dans %2d secondes..." "$i"
        sleep 1
    done
    echo ""
    sync
    reboot
else
    log_info "REBOOT_AUTO != true — redémarrage automatique désactivé"
    log_info "Pensez à redémarrer manuellement : sudo reboot"
fi
}

################################################################################
# DISPATCHER — EXÉCUTION ORDONNÉE DES SECTIONS
################################################################################

# Interruption (Ctrl+C, kill) : loguer la section en cours + récapitulatif
# des anomalies avant de sortir. Désarmé par la section 31 (compte à rebours).
SECTION_COURANTE="initialisation"
trap 'echo ""; log_error "Exécution interrompue par signal pendant la section : ${SECTION_COURANTE}"; afficher_anomalies; exit 130' INT TERM

TOTAL_SECTIONS=${#SECTIONS[@]}
INDEX_SECTION=0
DEMARRAGE_ATTEINT=true
[[ -n "$FROM_ID" ]] && DEMARRAGE_ATTEINT=false

[[ -n "$FROM_ID" ]] && log_info "Mode reprise : exécution à partir de la section $FROM_ID"
[[ -n "$ONLY_ID" ]] && log_info "Mode section unique : exécution de la seule section $ONLY_ID"

for entree_section in "${SECTIONS[@]}"; do
    id_section="${entree_section%%|*}"
    titre_section="${entree_section#*|}"
    INDEX_SECTION=$((INDEX_SECTION + 1))

    # Filtre --only : tout sauf la section demandée est ignoré
    if [[ -n "$ONLY_ID" && "$id_section" != "$ONLY_ID" ]]; then
        continue
    fi

    # Filtre --from : ignorer jusqu'à la section de reprise (incluse ensuite)
    if [[ "$DEMARRAGE_ATTEINT" == false ]]; then
        if [[ "$id_section" == "$FROM_ID" ]]; then
            DEMARRAGE_ATTEINT=true
        else
            continue
        fi
    fi

    SECTION_COURANTE="$id_section — $titre_section"
    log_section "[$INDEX_SECTION/$TOTAL_SECTIONS] $titre_section"
    debut_section=$(date +%s)
    "section_${id_section}"
    log_info "⏱ Section $id_section terminée en $(( $(date +%s) - debut_section ))s"
done

Le modèle de configuration — postinstall.26.04_v2.conf.example (complet)

À copier en postinstall.26.04_v2.conf puis à remplir. C’est le seul fichier qui contiendra vos secrets — il est exclu du versionnage et verrouillé root:600 par le script.

################################################################################
# postinstall.26.04_v2.conf — Configuration du script de post-installation
# Ubuntu 26.04 LTS "Resolute Raccoon"
#
# Fait avec amour par Romain J.
#
# ⚠️  CE FICHIER CONTIENT DES MOTS DE PASSE EN CLAIR
#     Permissions recommandées : chmod 600 postinstall.26.04_v2.conf
#     Ne jamais versionner dans un dépôt public (Git, GitHub, etc.)
#
# Utilisation :
#   sudo bash postinstall.26.04_v2.sh
#   (le script cherche postinstall.26.04_v2.conf dans son propre répertoire)
#
#   Ou avec un chemin personnalisé :
#   POSTINSTALL_CONF=/chemin/vers/mon.conf sudo bash postinstall.26.04_v2.sh
#
# Checklist avant exécution :
#   1. ⚠️  NAS_PASSWORD        → mot de passe du NAS
#   2. ⚠️  GMAIL_APP_PASSWORD  → App Password Gmail (16 caractères, espaces inclus)
#   3. ⚠️  VERACRYPT_PASS      → mot de passe du conteneur VeraCrypt
#   4. ⚠️  VPN_*_USER / PASS   → identifiants CyberGhost par connexion
#   5.     UTILISATEUR         → nom du compte principal (actuellement : utilisateur)
#   6.     LVM_HOME_DEVICE     → vérifier avec : sudo lvdisplay
################################################################################


# ==============================================================================
# UTILISATEUR PRINCIPAL
# ==============================================================================

UTILISATEUR="utilisateur"

# Nom de machine appliqué via hostnamectl (postfix myhostname, /etc/hosts,
# supervision Centreon) — quel que soit le nom saisi pendant l'installation
HOSTNAME_MACHINE="ma-machine"


# ==============================================================================
# COMPORTEMENT DU SCRIPT
# ==============================================================================

# Redémarrage automatique en fin de post-installation (true/false)
# Mettre à false pour terminer sans reboot (redémarrage manuel ensuite)
REBOOT_AUTO=true


# ==============================================================================
# NAS
# ==============================================================================

NAS_IP="192.168.1.100"

# ⚠️ À MODIFIER — mot de passe du partage CIFS
NAS_USER="ma-machine"
NAS_PASSWORD='***PURGE***'  # ⚠️ À REMPLACER AVANT EXÉCUTION

# Liste des partages NAS (ordre affiché dans les signets Nautilus)
NAS_PARTAGES=(
    animation
    documents
    downloads
    films
    Imprimante
    jeux
    livres
    musique
    perso
    photographie
    sauvegarde
    seriestv
    surveillance
    Technique
)

# Emplacement du fichier credentials CIFS (créé automatiquement par le script)
NAS_CREDENTIALS="/root/.smbcredentials"


# ==============================================================================
# MONTAGE /home SUR LVM
# ==============================================================================

# Vérifier l'existence du device : sudo lvdisplay /dev/home-vg/home_lv
LVM_HOME_DEVICE="/dev/home-vg/home_lv"


# ==============================================================================
# GMAIL — RELAY POSTFIX
# ==============================================================================

GMAIL_USER="prenom.nom@gmail.com"

# ⚠️ À MODIFIER — App Password Gmail (16 chars, généré sur myaccount.google.com)
# Format avec espaces : "xxxx xxxx xxxx xxxx"
GMAIL_APP_PASSWORD="***PURGE***"  # ⚠️ À REMPLACER (App Password Gmail)


# ==============================================================================
# VERACRYPT
# ==============================================================================

# Fichier qui stockera le mot de passe VeraCrypt sur le système (chmod 600)
VERACRYPT_PASS_FILE="/home/$UTILISATEUR/.veracrypt_pass"

# ⚠️ À MODIFIER — mot de passe du conteneur VeraCrypt
VERACRYPT_PASS="***PURGE***"  # ⚠️ À REMPLACER

# Chemin du conteneur chiffré sur le NAS
VERACRYPT_CONTAINER="/mnt/nas/chemin/vers/conteneur.vc"  # ⚠️ À ADAPTER

# Point de montage VeraCrypt
VERACRYPT_MOUNTPOINT="/media/veracrypt1"


# ==============================================================================
# CENTREON MONITORING AGENT (CMA)
# ==============================================================================
# ⚠️ Token JWT du broker Centreon — secret long-lived (exp: null)
#    À régénérer via l'interface Centreon en cas de compromission

CMA_TOKEN="***PURGE***"  # ⚠️ À REMPLACER (généré dans Centreon)

# Endpoint du serveur Centreon (host:port)
CMA_ENDPOINT="centreon:4317"

# Nom d'hôte affiché dans Centreon
CMA_HOST="MaMachine_avec_CMA"


# ==============================================================================
# UBUNTU PRO
# ==============================================================================
# ⚠️ Token d'attachement Ubuntu Pro — récupérable sur https://ubuntu.com/pro/dashboard
#    À régénérer via le dashboard en cas de compromission.
#    Laisser vide pour ne PAS attacher la machine (section 2g sautée avec warn).

PRO_TOKEN="***PURGE***"  # ⚠️ À REMPLACER (token Ubuntu Pro) ou laisser vide


# ==============================================================================
# VPN — IDENTIFIANTS OPENVPN (CyberGhost)
# ==============================================================================
# ⚠️ À MODIFIER — identifiants spécifiques à chaque connexion CyberGhost

VPN_SUISSE_USER="***PURGE***"  # ⚠️ À REMPLACER
VPN_SUISSE_PASS="***PURGE***"  # ⚠️ À REMPLACER

VPN_USA_USER="***PURGE***"  # ⚠️ À REMPLACER
VPN_USA_PASS="***PURGE***"  # ⚠️ À REMPLACER

VPN_FRANCE_USER="***PURGE***"  # ⚠️ À REMPLACER
VPN_FRANCE_PASS="***PURGE***"  # ⚠️ À REMPLACER

VPN_RUSSIE_USER="***PURGE***"  # ⚠️ À REMPLACER
VPN_RUSSIE_PASS="***PURGE***"  # ⚠️ À REMPLACER

# ==============================================================================
# VPN — CERTIFICATS ET CLÉS OPENVPN (CyberGhost)
# ==============================================================================
# ⚠️ SECRETS — déplacés ici en v8.8.24 (avant : embarqués dans le .sh)
# Le .conf doit rester en chmod 600 (cf. checklist en tête du script)

# Configuration OVPN commune aux 4 serveurs
read -r -d '' VPN_COMMON_OVPN << 'OVPN_EOF' || true
client
dev tun
proto udp
auth-user-pass
resolv-retry infinite
redirect-gateway def1
persist-key
persist-tun
nobind
data-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
auth SHA256
ping 5
explicit-exit-notify 2
script-security 2
remote-cert-tls server
route-delay 5
verb 4
ca ca.crt
cert client.crt
key client.key
OVPN_EOF

# CA CyberGhost (commune aux 4 serveurs)
read -r -d '' VPN_COMMON_CA << 'CA_EOF' || true
-----BEGIN CERTIFICATE-----
MIIGWjCCBEKgAwIBAgIJAJxUG61mxDS7MA0GCSqGSIb3DQEBDQUAMHsxCzAJBgNV
BAYTAlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3Qg
Uy5BLjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkB
FhJpbmZvQGN5YmVyZ2hvc3Qucm8wHhcNMTcwNjE5MDgxNzI1WhcNMzcwNjE0MDgx
NzI1WjB7MQswCQYDVQQGEwJSTzESMBAGA1UEBxMJQnVjaGFyZXN0MRgwFgYDVQQK
Ew9DeWJlckdob3N0IFMuQS4xGzAZBgNVBAMTEkN5YmVyR2hvc3QgUm9vdCBDQTEh
MB8GCSqGSIb3DQEJARYSaW5mb0BjeWJlcmdob3N0LnJvMIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEA7O8+mji2FlQhJXn/G4VLrKPjGtxgQBAdjo0dZEQz
KX08q14dLkslmOLgShStWKrOiLXGAvB1rPvvk613jtA0KjQLpgyLy9lIWohQKYjj
5jrJYXMZMkbSHBYI9L8L7iezBEFYrjYKdDo51nq99wRFhKdbyKKjDh3e2L2SVEZL
T1ogkK5gWzjvH+mjjtjUUicK+YjGwWOz6I+KKaG4Ve/D/cE6nCLbhHIMMnargZEu
7sqA6BFeS4kEP/ZdCZoTSX2n43XV1q63nJt/v0KDetbZDciFVW9h9SVPG4qT44p0
550N+Mom7zTX7S/ID5T9dplgU8sRGtIMrG0cIMD9zmpFgUnMusCrR7jJFr0sMAve
TbgZg95LmstV6R6WKZkSFdUrE0DHl4dHoZvTFX+1LhwhHgjgDLaosX0vhG/C/7Lp
oVWimd6RRQT3M9o4Fa1TuhfvBzQ20QHrmRV/yKvGNK0xckZ6EZ/QY7Z55ORU15Tg
ab4ebnblYPWoEmn0mIYP3LFFeoR5OS1EX7+j4kPv+bwPGsmpHjxmZyq2Y7sJBpbO
CJgbkn52WZdPBIRDpPdIHQ8pAJC4T0iMK9xvAwWNl/V6EYYNpR97osyEDXn+BTdA
HlhJ5fck9KlwI9mb1Kg1bhbvbmaIAiOLenSULYf3j6rI1ygo3R2cCyybtuAq8M7z
0OECAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6tdK1g/He5qzjeAoM5eHt4in9iUwga0G
A1UdIwSBpTCBooAU6tdK1g/He5qzjeAoM5eHt4in9iWhf6R9MHsxCzAJBgNVBAYT
AlJPMRIwEAYDVQQHEwlCdWNoYXJlc3QxGDAWBgNVBAoTD0N5YmVyR2hvc3QgUy5B
LjEbMBkGA1UEAxMSQ3liZXJHaG9zdCBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJp
bmZvQGN5YmVyZ2hvc3Qucm+CCQCcVButZsQ0uzAMBgNVHRMEBTADAQH/MA0GCSqG
SIb3DQEBDQUAA4ICAQDNyQ92kj4qiNjnHk99qvnFw9qGfwB9ofaPL74zh0G5hEe3
Wgb2o4fqUGnvUNgOu53gJksz3DcPQ8t40wfmm9I1Z8tiM9qrqvkuQ+nKcLgdooXt
EsTybPIYDZ2cWR/5E0TKRvC7RFzKgQ4D77Vbi4TdaHiDV7ZNfU1iLCoBGcYm80hc
UHEs5KIVLwUmcSOTmbZBySJxcSD0yUpS7nlZGwLY6VQrU+JFwDSisbXT4DXf3iSz
p7FzW0/u/SFvWsPHrjE0hkPoZPalYvouaJEHKAhip0ZwSmitlxbBnmm8+K/3c9mL
A5/uXrirfpuhhs8V3lyV2mczVtSiTl6gpi88gc//JY80JeHdupjO25T3XEzY9cpx
ecmkWaUEjLMx4wVoXQuUiPonfILM6OLwi+zUS8gQErdFeGvcQXbncPa4SdJuHkF8
lgiX2i8S8fPGdXvU37E9bdAXwP5nZriYq1s0D59Qfvz+vLXVkmyZp6ztxjKjKole
mPMak0Y5c1Q4RjNF6tmQoFuy/ACSkWy14Tzu2dFp7UiVbGg1FOvKhfs48zC2/IUQ
v1arqmPT/9LVq3B2DVT9UKXRUXX/f/jSSsVjkz4uUe2jUyL+XHX1nSmROTPHSAJ+
oKf0BLnfqUxFkEUTwLnayssP2nwGgq35b7wEbTFIXdrjHGFUVQIDeERz8UThew==
-----END CERTIFICATE-----
CA_EOF

# Certificats et clés par serveur — chaque utilisateur CyberGhost a sa paire
# ⚠️ À REMPLACER par les certificats/clés téléchargés depuis l'espace CyberGhost

read -r -d '' VPN_SUISSE_CERT << 'SUISSE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_SUISSE
-----END CERTIFICATE-----
SUISSE_CERT_EOF

read -r -d '' VPN_SUISSE_KEY << 'SUISSE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_SUISSE
-----END PRIVATE KEY-----
SUISSE_KEY_EOF

read -r -d '' VPN_USA_CERT << 'USA_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_USA
-----END CERTIFICATE-----
USA_CERT_EOF

read -r -d '' VPN_USA_KEY << 'USA_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_USA
-----END PRIVATE KEY-----
USA_KEY_EOF

read -r -d '' VPN_FRANCE_CERT << 'FRANCE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_FRANCE
-----END CERTIFICATE-----
FRANCE_CERT_EOF

read -r -d '' VPN_FRANCE_KEY << 'FRANCE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_FRANCE
-----END PRIVATE KEY-----
FRANCE_KEY_EOF

read -r -d '' VPN_RUSSIE_CERT << 'RUSSIE_CERT_EOF' || true
-----BEGIN CERTIFICATE-----
COLLER_ICI_LE_CERTIFICAT_CLIENT_RUSSIE
-----END CERTIFICATE-----
RUSSIE_CERT_EOF

read -r -d '' VPN_RUSSIE_KEY << 'RUSSIE_KEY_EOF' || true
-----BEGIN PRIVATE KEY-----
COLLER_ICI_LA_CLE_PRIVEE_RUSSIE
-----END PRIVATE KEY-----
RUSSIE_KEY_EOF

# ============================================================
# GNOME Remote Desktop (RDP) — section 26b (v8.9.16)
# Identifiants de connexion RDP (headless + partage de session)
# ============================================================
RDP_USER="VOTRE_UTILISATEUR_RDP"
RDP_PASS="VOTRE_MOT_DE_PASSE_RDP"

La validation post-reboot — validate_postinstall.sh (complet)

Exécuté automatiquement 4 minutes après le premier redémarrage par un timer systemd à usage unique ; le rapport arrive par mail.

#!/bin/bash
# =============================================================================
# Fait avec amour par Romain J.
# -----------------------------------------------------------------------------
# Nom         : validate_postinstall.sh
# Description : Validation post-installation Ubuntu 26.04 — contrôle automatique
#               de tout ce qu'a fait postinstall.26.04_v2.sh
# Date        : 2026-06-09
# Version     : 1.2
# Cible       : postinstall.26.04_v2.sh v8.8.29+ (CMA en mode TLS)
# Usage       : ./validate_postinstall.sh
# Contexte    : exécuté par utilisateur (pas root) après postinstall + reboot
# =============================================================================
set -uo pipefail
IFS=$'\n\t'

# ── Localisation du .conf (source unique de vérité, avec repli) ─────────────────
readonly CONF_FILE="${POSTINSTALL_CONF:-$(dirname "$(realpath "$0")")/postinstall.26.04_v2.conf}"

# Lit une variable scalaire du .conf : lecture directe si lisible, sinon via sudo -n.
# Renvoie une chaîne vide si le .conf est inaccessible (600 root + pas de sudo en cache).
# Limite : ne gère que les valeurs sans quotes ou entre doubles quotes — une valeur
# entre quotes simples (ex. NAS_PASSWORD) renverrait une chaîne vide.
lire_conf() {
    local cle="$1" val=""
    if [[ -r "${CONF_FILE}" ]]; then
        val=$(grep -oP "^${cle}=\"?\K[^\"]+" "${CONF_FILE}" 2>/dev/null | head -n1)
    elif sudo -n true 2>/dev/null; then
        val=$(sudo grep -oP "^${cle}=\"?\K[^\"]+" "${CONF_FILE}" 2>/dev/null | head -n1)
    fi
    printf '%s' "${val}"
}

# ── Variables globales ────────────────────────────────────────────────────────
# Déclaration séparée du readonly (SC2155 : un readonly combiné masquerait
# le code retour de la substitution de commande)
DATE_EXECUTION=$(date +%Y%m%d_%H%M%S)
readonly DATE_EXECUTION
readonly REPERTOIRE_LOG="/var/log/postinstall"
readonly JOURNAL="${REPERTOIRE_LOG}/validation-${DATE_EXECUTION}.log"
# Dérivées du .conf quand il est lisible, sinon valeurs de repli (cohérence avec le .sh)
_user=$(lire_conf UTILISATEUR);  readonly UTILISATEUR_ATTENDU="${_user:-utilisateur}"
_nasip=$(lire_conf NAS_IP);      readonly NAS_IP="${_nasip:-192.168.1.100}"
unset _user _nasip

# Couleurs ANSI
readonly C_VERT='\033[0;32m'
readonly C_ORANGE='\033[0;33m'
readonly C_ROUGE='\033[0;31m'
readonly C_BLEU='\033[0;34m'
readonly C_GRAS='\033[1m'
readonly C_RESET='\033[0m'

# Compteurs et listes
NB_OK=0
NB_WARN=0
NB_FAIL=0
NB_SKIP=0
POINTS_MANUELS=()

# ── Préparation du journal ────────────────────────────────────────────────────
preparer_journal() {
    if [[ ! -d "${REPERTOIRE_LOG}" ]]; then
        sudo mkdir -p "${REPERTOIRE_LOG}" 2>/dev/null || {
            # Fallback : log dans /tmp si /var/log/postinstall inaccessible
            echo "AVERTISSEMENT : impossible de créer ${REPERTOIRE_LOG}, fallback /tmp" >&2
            local nouveau_journal="/tmp/validation-${DATE_EXECUTION}.log"
            # Réassigner via variable globale non readonly de fallback
            JOURNAL_EFFECTIF="${nouveau_journal}"
            return
        }
        sudo chown "${UTILISATEUR_ATTENDU}:${UTILISATEUR_ATTENDU}" "${REPERTOIRE_LOG}" 2>/dev/null || true
    fi
    sudo touch "${JOURNAL}" 2>/dev/null || true
    sudo chown "${UTILISATEUR_ATTENDU}:${UTILISATEUR_ATTENDU}" "${JOURNAL}" 2>/dev/null || true
    JOURNAL_EFFECTIF="${JOURNAL}"
}

# ── Fonctions de journalisation ───────────────────────────────────────────────

# Écrit dans le journal (sans couleur)
ecrire_journal() {
    local message="$1"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${message}" >> "${JOURNAL_EFFECTIF}" 2>/dev/null || true
}

# Affiche un titre de section
val_section() {
    local titre="$1"
    echo ""
    echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
    echo -e "${C_BLEU}${C_GRAS}  ${titre}${C_RESET}"
    echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
    ecrire_journal "===== ${titre} ====="
}

# Affiche un test OK (vert)
val_ok() {
    local message="$1"
    echo -e "  ${C_VERT}[ OK ]${C_RESET}  ${message}"
    ecrire_journal "[OK]   ${message}"
    NB_OK=$((NB_OK + 1))
}

# Affiche un avertissement (orange) — non bloquant
val_warn() {
    local message="$1"
    echo -e "  ${C_ORANGE}[WARN]${C_RESET}  ${message}"
    ecrire_journal "[WARN] ${message}"
    NB_WARN=$((NB_WARN + 1))
}

# Affiche un échec (rouge)
val_fail() {
    local message="$1"
    echo -e "  ${C_ROUGE}[FAIL]${C_RESET}  ${message}"
    ecrire_journal "[FAIL] ${message}"
    NB_FAIL=$((NB_FAIL + 1))
}

# Affiche un test ignoré (sudo indisponible, etc.)
val_skip() {
    local message="$1"
    echo -e "  ${C_ORANGE}[SKIP]${C_RESET}  ${message}"
    ecrire_journal "[SKIP] ${message}"
    NB_SKIP=$((NB_SKIP + 1))
}

# Ajoute un point à vérifier manuellement
ajouter_point_manuel() {
    POINTS_MANUELS+=("$1")
}

# ── Helpers ───────────────────────────────────────────────────────────────────

# Vérifie si sudo est utilisable sans mot de passe (cache valide)
sudo_dispo() {
    sudo -n true 2>/dev/null
}

# Vérifie si un paquet apt est installé
paquet_installe() {
    dpkg -s "$1" 2>/dev/null | grep -q "^Status: install ok installed"
}

# Vérifie si un service systemd est actif
service_actif() {
    systemctl is-active --quiet "$1" 2>/dev/null
}

# ── Section 1 : SYSTÈME DE BASE ───────────────────────────────────────────────
valider_systeme_base() {
    val_section "[1] SYSTÈME DE BASE"

    # Utilisateur utilisateur
    if id "${UTILISATEUR_ATTENDU}" &>/dev/null; then
        val_ok "Utilisateur ${UTILISATEUR_ATTENDU} existe"

        # Groupes
        local groupes_attendus=(docker kvm libvirt sudo)
        local groupes_utilisateur
        groupes_utilisateur=$(id -nG "${UTILISATEUR_ATTENDU}" 2>/dev/null || echo "")
        for groupe in "${groupes_attendus[@]}"; do
            if echo " ${groupes_utilisateur} " | grep -q " ${groupe} "; then
                val_ok "Groupe '${groupe}' présent pour ${UTILISATEUR_ATTENDU}"
            else
                val_fail "Groupe '${groupe}' MANQUANT pour ${UTILISATEUR_ATTENDU}"
            fi
        done
    else
        val_fail "Utilisateur ${UTILISATEUR_ATTENDU} introuvable"
    fi

    # /home sur LVM
    if mount | grep -E "on /home " | grep -q "mapper" 2>/dev/null; then
        val_ok "/home monté sur LVM"
    elif mount | grep -qE "on /home "; then
        val_warn "/home monté mais peut-être pas sur LVM (vérifier : mount | grep /home)"
    else
        val_fail "/home n'est pas un point de montage séparé"
    fi

    # noatime sur / et /home
    local options_root
    options_root=$(findmnt -no OPTIONS / 2>/dev/null || echo "")
    if echo "${options_root}" | grep -q "noatime"; then
        val_ok "noatime actif sur /"
    else
        val_warn "noatime absent sur / (options : ${options_root})"
    fi

    if findmnt /home &>/dev/null; then
        local options_home
        options_home=$(findmnt -no OPTIONS /home 2>/dev/null || echo "")
        if echo "${options_home}" | grep -q "noatime"; then
            val_ok "noatime actif sur /home"
        else
            val_warn "noatime absent sur /home (options : ${options_home})"
        fi
    else
        val_warn "/home pas un montage séparé — noatime non vérifiable"
    fi

    # Espace disque /
    local espace_libre_gib
    espace_libre_gib=$(df -BG / 2>/dev/null | awk 'NR==2 {gsub("G",""); print $4}' || echo "0")
    if [[ "${espace_libre_gib}" -gt 5 ]]; then
        val_ok "Espace libre sur / : ${espace_libre_gib} GiB (> 5 GiB)"
    else
        val_fail "Espace libre sur / : ${espace_libre_gib} GiB (≤ 5 GiB)"
    fi

    # Attachement Ubuntu Pro (section 2g) — SKIP si aucun token dans le .conf
    local pro_token
    pro_token=$(lire_conf PRO_TOKEN)
    if [[ -z "${pro_token}" || "${pro_token}" == *PURGE* ]]; then
        val_skip "Ubuntu Pro : PRO_TOKEN non renseigné dans le .conf — attachement non attendu"
    elif ! command -v pro &>/dev/null; then
        val_fail "Ubuntu Pro : client 'pro' introuvable"
    elif pro status --format json 2>/dev/null | grep -qE '"attached":[[:space:]]*true'; then
        val_ok "Ubuntu Pro : machine attachée au contrat"
    else
        val_fail "Ubuntu Pro : machine NON attachée (relancer : sudo bash postinstall.26.04_v2.sh --only 2g)"
    fi
}

# ── Section 2 : PAQUETS ESSENTIELS ────────────────────────────────────────────
valider_paquets() {
    val_section "[2] PAQUETS ESSENTIELS"

    local paquets=(
        git vim htop curl jq tree ncdu rsync ansible
        docker-ce qemu-system-x86 virt-manager flatpak
        postfix clamav nagios-nrpe-server snmpd smartmontools
        veracrypt-console nodejs npm pwgen
    )
    # Note : yt-dlp n'est PAS un paquet apt (binaire GitHub dans /usr/local/bin) —
    # il est validé via `command -v yt-dlp` en section [10].
    # wget et snapd retirés : préinstallés Ubuntu, jamais installés par le .sh
    # (évite un faux négatif). snapd est couvert via `snap list` en section [6].

    for paquet in "${paquets[@]}"; do
        if paquet_installe "${paquet}"; then
            val_ok "Paquet installé : ${paquet}"
        else
            val_fail "Paquet MANQUANT : ${paquet}"
        fi
    done
}

# ── Section 3 : SERVICES SYSTEMD ──────────────────────────────────────────────
valider_services() {
    val_section "[3] SERVICES SYSTEMD"

    local services=(
        ssh docker postfix
        nagios-nrpe-server snmpd
        clamav-freshclam smartmontools
    )

    for service in "${services[@]}"; do
        if service_actif "${service}"; then
            val_ok "Service actif : ${service}"
        else
            val_fail "Service INACTIF : ${service}"
        fi
    done

    # Services tolérés inactifs (dépendances externes) → WARN, pas FAIL :
    #  - centagent      : inactif si le serveur Centreon est injoignable
    #  - clamav-daemon  : peut ne pas démarrer tant que la base virale n'est pas prête
    for service in centagent clamav-daemon; do
        if service_actif "${service}"; then
            val_ok "Service actif : ${service}"
        else
            val_warn "Service inactif : ${service} (toléré — dépendance externe)"
        fi
    done

    # libvirtd ou virtqemud (selon version)
    if service_actif "libvirtd"; then
        val_ok "Service actif : libvirtd"
    elif service_actif "virtqemud"; then
        val_ok "Service actif : virtqemud (remplace libvirtd)"
    else
        val_fail "Service INACTIF : libvirtd ET virtqemud"
    fi

    # Timer update-system
    if systemctl is-active --quiet "update-system.timer" 2>/dev/null; then
        val_ok "Timer actif : update-system.timer"
    else
        val_fail "Timer INACTIF : update-system.timer"
    fi
}

# ── Section 4 : RÉSEAU ET NAS ─────────────────────────────────────────────────
valider_reseau() {
    val_section "[4] RÉSEAU ET NAS"

    # IP statique (v8.9.15) : le postinstall fige l'IP DHCP en manuel
    local conn_active methode_ip ip_actuelle
    conn_active=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | grep -E 'enp|eth|eno|wl' | cut -d: -f1 | head -n1)
    if [[ -n "${conn_active}" ]]; then
        methode_ip=$(nmcli -g ipv4.method connection show "${conn_active}" 2>/dev/null)
        ip_actuelle=$(nmcli -g IP4.ADDRESS connection show "${conn_active}" 2>/dev/null | head -1)
        if [[ "${methode_ip}" == "manual" ]]; then
            val_ok "IP statique configurée : ${ip_actuelle} (${conn_active})"
        else
            val_fail "IP en DHCP (${methode_ip:-inconnue}) sur ${conn_active} — le postinstall doit la figer"
        fi
    else
        val_fail "Aucune connexion réseau active détectée (nmcli)"
    fi

    # DNS
    if ping -c 1 -W 3 google.com &>/dev/null; then
        val_ok "Résolution DNS Google opérationnelle"
    else
        val_fail "Résolution DNS Google échoue (ping google.com)"
    fi

    # GNOME Remote Desktop (v8.9.16) : service système + port RDP
    if systemctl is-active --quiet gnome-remote-desktop.service 2>/dev/null; then
        if ss -tln 2>/dev/null | grep -q ':3389'; then
            val_ok "GNOME Remote Desktop actif (port 3389 en écoute)"
        else
            val_fail "GNOME Remote Desktop actif mais port 3389 fermé (certificat TLS ?)"
        fi
    else
        val_warn "GNOME Remote Desktop inactif (RDP non configuré ou service arrêté)"
    fi

    # Montages NAS (x-systemd.automount → montage différé : on déclenche l'accès
    # avant le test pour éviter un faux négatif juste après reboot)
    for partage in sauvegarde Technique; do
        ls "/mnt/nas/${partage}" >/dev/null 2>&1 || true
        if mountpoint -q "/mnt/nas/${partage}" 2>/dev/null; then
            val_ok "NAS monté : /mnt/nas/${partage}"
        else
            val_fail "NAS NON monté : /mnt/nas/${partage}"
        fi
    done

    # SSH NAS
    if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no \
           "${UTILISATEUR_ATTENDU}@${NAS_IP}" -p 22 "true" &>/dev/null; then
        val_ok "Connexion SSH NAS (${NAS_IP}:22) opérationnelle"
    else
        val_warn "Connexion SSH NAS (${NAS_IP}:22) échoue (vérifier clé SSH)"
    fi

    # Port NRPE 5666
    if ss -tln 2>/dev/null | grep -q ":5666 "; then
        val_ok "Port NRPE 5666 en écoute"
    else
        val_fail "Port NRPE 5666 absent (NRPE inopérant)"
    fi

    # Port SNMP 161
    if ss -uln 2>/dev/null | grep -q ":161 "; then
        val_ok "Port SNMP 161 en écoute"
    else
        val_fail "Port SNMP 161 absent (snmpd inopérant)"
    fi

    # Port CMA 4317 sur centreon (nc si présent, sinon repli /dev/tcp de bash)
    local centreon_ok=1
    if command -v nc &>/dev/null; then
        nc -z -w 3 centreon 4317 &>/dev/null || centreon_ok=0
    else
        timeout 3 bash -c 'exec 3<>/dev/tcp/centreon/4317' &>/dev/null || centreon_ok=0
    fi
    if [[ "${centreon_ok}" -eq 1 ]]; then
        val_ok "Serveur Centreon joignable (centreon:4317)"
    else
        val_fail "Serveur Centreon INJOIGNABLE (centreon:4317)"
    fi

    # Entrée /etc/hosts pour 'centreon' (ajoutée par le .sh, section 2d)
    if grep -qE '^[^#]\S*\s+centreon(\s|$)' /etc/hosts 2>/dev/null; then
        val_ok "/etc/hosts : entrée 'centreon' présente"
    else
        val_fail "/etc/hosts : entrée 'centreon' MANQUANTE"
    fi
}

# ── Section 5 : MONITORING CENTREON ───────────────────────────────────────────
valider_centreon() {
    val_section "[5] MONITORING CENTREON"

    # Fichier centagent.json
    local centagent_json="/etc/centreon-monitoring-agent/centagent.json"
    if [[ -f "${centagent_json}" ]] || sudo test -f "${centagent_json}" 2>/dev/null; then
        if sudo_dispo; then
            if sudo jq empty "${centagent_json}" 2>/dev/null; then
                val_ok "centagent.json présent et JSON valide"
                # Mode de chiffrement attendu : full (TLS)
                local enc_mode
                enc_mode=$(sudo jq -r '.encryption // "absent"' "${centagent_json}" 2>/dev/null || echo "absent")
                if [[ "${enc_mode}" == "full" ]]; then
                    val_ok "centagent.json : encryption=full (TLS actif)"
                else
                    val_fail "centagent.json : encryption=${enc_mode} (attendu : full)"
                fi
            else
                val_fail "centagent.json présent mais JSON INVALIDE"
            fi
        else
            val_skip "centagent.json présent (sudo indisponible pour valider JSON/encryption)"
        fi
    else
        val_fail "centagent.json INTROUVABLE (${centagent_json})"
    fi

    # Service centagent (déjà testé en [3] mais utile ici)
    if service_actif "centagent"; then
        val_ok "Service centagent actif"
    else
        val_fail "Service centagent INACTIF"
    fi

    # Certificat TLS centreon-agent.crt (mode encryption: full)
    local centreon_crt="/etc/centreon-monitoring-agent/centreon-agent.crt"
    if [[ -f "${centreon_crt}" ]] || sudo test -f "${centreon_crt}" 2>/dev/null; then
        if sudo_dispo; then
            if sudo openssl x509 -checkend 86400 -noout -in "${centreon_crt}" &>/dev/null; then
                val_ok "centreon-agent.crt valide (non expiré dans les 24h)"
            else
                val_fail "centreon-agent.crt EXPIRÉ ou expire sous 24h"
            fi
        else
            val_skip "centreon-agent.crt présent (sudo indisponible pour vérifier expiration)"
        fi
    else
        val_fail "centreon-agent.crt INTROUVABLE (${centreon_crt})"
    fi

    # nrpe.cfg
    if [[ -f "/etc/nagios/nrpe.cfg" ]] || sudo test -f "/etc/nagios/nrpe.cfg" 2>/dev/null; then
        val_ok "nrpe.cfg présent (/etc/nagios/nrpe.cfg)"
    else
        val_fail "nrpe.cfg INTROUVABLE"
    fi

    # Sondes
    local sondes=(
        /usr/lib/nagios/plugins/check_mem.sh
        /usr/lib/nagios/plugins/check_mem.pl
        /usr/lib/nagios/plugins/check_service.sh
        /usr/local/bin/cma_test_ok.sh
    )
    for sonde in "${sondes[@]}"; do
        if [[ -x "${sonde}" ]]; then
            val_ok "Sonde présente et exécutable : ${sonde}"
        else
            val_fail "Sonde MANQUANTE ou non exécutable : ${sonde}"
        fi
    done

    # Test check_mem.sh
    if [[ -x "/usr/lib/nagios/plugins/check_mem.sh" ]]; then
        local sortie_mem
        sortie_mem=$(/usr/lib/nagios/plugins/check_mem.sh 2>&1 || true)
        if echo "${sortie_mem}" | grep -qE "^(OK|WARNING|CRITICAL)"; then
            val_ok "check_mem.sh retourne un état Nagios valide"
        else
            val_warn "check_mem.sh : sortie inattendue (${sortie_mem:0:60})"
        fi
    fi

    # Test check_service.sh ssh
    if [[ -x "/usr/lib/nagios/plugins/check_service.sh" ]]; then
        local sortie_svc
        sortie_svc=$(/usr/lib/nagios/plugins/check_service.sh ssh 2>&1 || true)
        if echo "${sortie_svc}" | grep -qE "^OK"; then
            val_ok "check_service.sh ssh : OK"
        else
            val_warn "check_service.sh ssh : non OK (${sortie_svc:0:60})"
        fi
    fi
}

# ── Section 6 : NAVIGATEURS ET APPLICATIONS ───────────────────────────────────
valider_applications() {
    val_section "[6] NAVIGATEURS ET APPLICATIONS"

    # Navigateurs (binaires)
    local navigateurs=(google-chrome brave-browser opera)
    for nav in "${navigateurs[@]}"; do
        if command -v "${nav}" &>/dev/null; then
            val_ok "Navigateur installé : ${nav}"
        else
            val_fail "Navigateur MANQUANT : ${nav}"
        fi
    done

    # Flatpak apps
    if command -v flatpak &>/dev/null; then
        local flatpak_apps=(
            "com.discordapp.Discord:Discord"
            "org.videolan.VLC:VLC"
            "org.telegram.desktop:Telegram"
        )
        local flatpak_liste
        flatpak_liste=$(flatpak list --app --columns=application 2>/dev/null || echo "")
        for entree in "${flatpak_apps[@]}"; do
            local app_id="${entree%%:*}"
            local nom="${entree##*:}"
            if echo "${flatpak_liste}" | grep -q "^${app_id}$"; then
                val_ok "Flatpak installé : ${nom} (${app_id})"
            else
                val_fail "Flatpak MANQUANT : ${nom} (${app_id})"
            fi
        done
    else
        val_fail "flatpak introuvable — impossible de vérifier les apps Flatpak"
    fi

    # Snap apps
    if command -v snap &>/dev/null; then
        local snap_liste
        snap_liste=$(snap list 2>/dev/null | awk 'NR>1 {print $1}' || echo "")
        local snaps=(codium signal-desktop chromium)
        for s in "${snaps[@]}"; do
            if echo "${snap_liste}" | grep -q "^${s}$"; then
                val_ok "Snap installé : ${s}"
            else
                val_fail "Snap MANQUANT : ${s}"
            fi
        done
    else
        val_fail "snap introuvable — impossible de vérifier les snaps"
    fi
}

# ── Section 7 : BASH ET ALIASES ───────────────────────────────────────────────
valider_bash() {
    val_section "[7] BASH ET ALIASES"

    local bashrc="/home/${UTILISATEUR_ATTENDU}/.bashrc"
    local aliases="/home/${UTILISATEUR_ATTENDU}/.bash_aliases"

    # .bashrc
    if [[ -s "${bashrc}" ]]; then
        val_ok "~/.bashrc présent et non vide"
        if grep -q "bash_aliases" "${bashrc}" 2>/dev/null; then
            val_ok "~/.bashrc source bien ~/.bash_aliases"
        else
            val_warn "~/.bashrc ne semble pas sourcer ~/.bash_aliases"
        fi
    else
        val_fail "~/.bashrc absent ou vide"
    fi

    # .bash_aliases
    if [[ -s "${aliases}" ]]; then
        val_ok "~/.bash_aliases présent"

        local aliases_critiques=(lok pw mp3dl ytdlpup veracryptup update vpnsuisse cvscan)
        for a in "${aliases_critiques[@]}"; do
            if grep -qE "^\s*alias\s+${a}=" "${aliases}" 2>/dev/null \
               || grep -qE "^\s*${a}\(\)" "${aliases}" 2>/dev/null; then
                val_ok "Alias/fonction définie : ${a}"
            else
                val_fail "Alias/fonction MANQUANTE : ${a}"
            fi
        done
    else
        val_fail "~/.bash_aliases absent"
    fi

    # fortune + cowsay + lolcat — PATH étendu à /usr/games : lancé par le timer
    # systemd, le PATH ne contient pas /usr/games → faux négatifs (test réel)
    for binaire in fortune cowsay lolcat; do
        if PATH="${PATH}:/usr/games" command -v "${binaire}" &>/dev/null; then
            val_ok "Binaire présent : ${binaire}"
        else
            val_fail "Binaire MANQUANT : ${binaire}"
        fi
    done
}

# ── Section 8 : SÉCURITÉ ──────────────────────────────────────────────────────
valider_securite() {
    val_section "[8] SÉCURITÉ"

    # /root/.smbcredentials
    if sudo_dispo; then
        if sudo test -f /root/.smbcredentials; then
            local perms_smb
            perms_smb=$(sudo stat -c "%a" /root/.smbcredentials 2>/dev/null || echo "?")
            if [[ "${perms_smb}" == "600" ]]; then
                val_ok "/root/.smbcredentials présent et chmod 600"
            else
                val_fail "/root/.smbcredentials présent mais chmod ${perms_smb} (attendu : 600)"
            fi
        else
            val_fail "/root/.smbcredentials INTROUVABLE"
        fi
    else
        val_skip "/root/.smbcredentials (sudo indisponible)"
    fi

    # ~/.veracrypt_pass
    local veracrypt_pass="/home/${UTILISATEUR_ATTENDU}/.veracrypt_pass"
    if [[ -f "${veracrypt_pass}" ]]; then
        local perms_vc
        perms_vc=$(stat -c "%a" "${veracrypt_pass}" 2>/dev/null || echo "?")
        if [[ "${perms_vc}" == "600" ]]; then
            val_ok "~/.veracrypt_pass présent et chmod 600"
        else
            val_fail "~/.veracrypt_pass présent mais chmod ${perms_vc} (attendu : 600)"
        fi
    else
        val_fail "~/.veracrypt_pass INTROUVABLE"
    fi

    # /etc/sudoers.d/monitoring-plugins-custom
    local sudoers_mon="/etc/sudoers.d/monitoring-plugins-custom"
    if sudo_dispo; then
        if sudo test -f "${sudoers_mon}"; then
            local perms_sud
            perms_sud=$(sudo stat -c "%a" "${sudoers_mon}" 2>/dev/null || echo "?")
            if [[ "${perms_sud}" == "440" ]]; then
                val_ok "${sudoers_mon} présent et chmod 440"
            else
                val_fail "${sudoers_mon} présent mais chmod ${perms_sud} (attendu : 440)"
            fi
        else
            val_fail "${sudoers_mon} INTROUVABLE"
        fi
    else
        val_skip "${sudoers_mon} (sudo indisponible)"
    fi

    # /etc/sudoers.d/90-<utilisateur> (NOPASSWD)
    local sudoers_user="/etc/sudoers.d/90-${UTILISATEUR_ATTENDU}"
    if sudo_dispo; then
        if sudo test -f "${sudoers_user}"; then
            local perms_u
            perms_u=$(sudo stat -c "%a" "${sudoers_user}" 2>/dev/null || echo "?")
            if [[ "${perms_u}" == "440" ]]; then
                val_ok "${sudoers_user} présent et chmod 440"
            else
                val_warn "${sudoers_user} présent mais chmod ${perms_u} (attendu : 440)"
            fi
        else
            val_fail "${sudoers_user} INTROUVABLE"
        fi
    else
        val_skip "${sudoers_user} (sudo indisponible)"
    fi

    # VeraCrypt installé
    if command -v veracrypt &>/dev/null; then
        val_ok "VeraCrypt installé"
    else
        val_fail "VeraCrypt MANQUANT"
    fi

    # UFW désactivé
    if command -v ufw &>/dev/null; then
        if sudo_dispo; then
            # LANG=C : en locale française « État : inactif » ne matche pas
            # « inactive » → faux positif « UFW actif » (test réel)
            if sudo LANG=C ufw status 2>/dev/null | grep -qi "inactive"; then
                val_ok "UFW désactivé (conforme)"
            else
                val_warn "UFW actif (attendu : inactif)"
            fi
        else
            val_skip "Statut UFW (sudo indisponible)"
        fi
    else
        val_ok "UFW non installé (équivalent à désactivé)"
    fi
}

# ── Section 9 : DÉVELOPPEMENT ─────────────────────────────────────────────────
valider_developpement() {
    val_section "[9] DÉVELOPPEMENT"

    # Claude Code — installé dans ~/.npm-global/bin, souvent hors PATH non interactif
    local claude_bin=""
    if command -v claude &>/dev/null; then
        claude_bin="$(command -v claude)"
    elif [[ -x "/home/${UTILISATEUR_ATTENDU}/.npm-global/bin/claude" ]]; then
        claude_bin="/home/${UTILISATEUR_ATTENDU}/.npm-global/bin/claude"
    fi
    if [[ -n "${claude_bin}" ]]; then
        val_ok "Claude Code installé (${claude_bin})"
    else
        val_fail "Claude Code (claude) introuvable (PATH et ~/.npm-global/bin)"
    fi

    # Node.js >= 18
    if command -v node &>/dev/null; then
        local node_ver
        node_ver=$(node --version 2>/dev/null | sed 's/^v//' || echo "0")
        local node_major="${node_ver%%.*}"
        if [[ "${node_major}" =~ ^[0-9]+$ ]] && [[ "${node_major}" -ge 18 ]]; then
            val_ok "Node.js v${node_ver} (>= 18)"
        else
            val_fail "Node.js v${node_ver} (< 18 requis)"
        fi
    else
        val_fail "Node.js introuvable"
    fi

    # npm
    if command -v npm &>/dev/null; then
        local npm_ver
        npm_ver=$(npm --version 2>/dev/null || echo "?")
        val_ok "npm installé (v${npm_ver})"
    else
        val_fail "npm introuvable"
    fi

    # Docker hello-world
    if command -v docker &>/dev/null; then
        if docker run --rm hello-world &>/dev/null; then
            val_ok "Docker fonctionne (hello-world OK)"
        else
            val_fail "Docker présent mais 'hello-world' échoue (utilisateur dans le groupe docker ?)"
        fi
    else
        val_fail "Docker introuvable"
    fi
}

# ── Section 10 : MISE À JOUR AUTOMATIQUE ──────────────────────────────────────
valider_maj_auto() {
    val_section "[10] MISE À JOUR AUTOMATIQUE"

    local script_maj="/usr/local/bin/update-system.sh"
    if [[ -x "${script_maj}" ]]; then
        val_ok "${script_maj} présent et exécutable"
    else
        val_fail "${script_maj} MANQUANT ou non exécutable"
    fi

    if systemctl is-active --quiet "update-system.timer" 2>/dev/null; then
        val_ok "update-system.timer actif"
    else
        val_fail "update-system.timer INACTIF"
    fi

    # Timer de nettoyage hebdomadaire (section 17 du .sh)
    if systemctl is-active --quiet "cleanup-system.timer" 2>/dev/null; then
        val_ok "cleanup-system.timer actif"
    else
        val_fail "cleanup-system.timer INACTIF"
    fi

    # Crons postinstall (rappels quotidiens + scan ClamAV hebdomadaire)
    for cron in /etc/cron.d/postinstall-rappels /etc/cron.d/postinstall-clamav; do
        if [[ -f "${cron}" ]] || sudo test -f "${cron}" 2>/dev/null; then
            val_ok "Cron présent : ${cron}"
        else
            val_fail "Cron MANQUANT : ${cron}"
        fi
    done

    if command -v yt-dlp &>/dev/null; then
        local ytdlp_ver
        ytdlp_ver=$(yt-dlp --version 2>/dev/null || echo "?")
        val_ok "yt-dlp installé (version : ${ytdlp_ver})"
    else
        val_fail "yt-dlp introuvable"
    fi
}

# ── Section 11 : VPN ──────────────────────────────────────────────────────────
valider_vpn() {
    val_section "[11] VPN"

    local profils=(Suisse USA France Russie)
    local liste_nm
    liste_nm=$(nmcli -t -f NAME connection show 2>/dev/null || echo "")

    for profil in "${profils[@]}"; do
        if echo "${liste_nm}" | grep -Fxq "${profil}"; then
            val_ok "Profil VPN NetworkManager présent : ${profil}"
        else
            val_fail "Profil VPN NetworkManager MANQUANT : ${profil}"
        fi
    done
}

# ── Section 12 : VÉRIFICATIONS MANUELLES REQUISES ─────────────────────────────
afficher_verifications_manuelles() {
    val_section "[12] VÉRIFICATIONS MANUELLES REQUISES"

    ajouter_point_manuel "Connexion VPN fonctionnelle    → tester : vpnsuisse  puis  vpnoff"
    ajouter_point_manuel "Envoi de mail Gmail            → echo test | mail -s \"test postinstall\" prenom.nom@gmail.com"
    # Nom d'hôte CMA lu depuis centagent.json (plus de valeur en dur : le nom
    # varie selon la machine — MaMachine_avec_CMA n'était vrai que sur le poste principal)
    local hote_cma
    hote_cma=$(sudo -n grep -oP '"host"\s*:\s*"\K[^"]+' /etc/centreon-monitoring-agent/centagent.json 2>/dev/null || echo "voir centagent.json")
    ajouter_point_manuel "CMA visible dans Centreon      → vérifier l'interface web (hôte : ${hote_cma})"
    ajouter_point_manuel "Montage VeraCrypt              → tester : 1984  puis  1984q"
    ajouter_point_manuel "Extensions GNOME chargées      → vérifier visuellement (gnome-extensions list)"
    ajouter_point_manuel "GSConnect apparié              → vérifier appariement avec téléphone Android"
    ajouter_point_manuel "DWService opérationnel         → lancer : dwagent et tester accès distant"

    echo ""
    echo -e "${C_ORANGE}  Les points suivants ne sont pas testables automatiquement.${C_RESET}"
    echo -e "${C_ORANGE}  Ils sont listés à nouveau dans le résumé final ci-dessous.${C_RESET}"
}

# ── Résumé final ──────────────────────────────────────────────────────────────
afficher_resume() {
    local total=$((NB_OK + NB_WARN + NB_FAIL))
    echo ""
    echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
    echo -e "${C_BLEU}${C_GRAS}  RÉSUMÉ DE LA VALIDATION POST-INSTALLATION${C_RESET}"
    echo -e "${C_BLEU}${C_GRAS}═══════════════════════════════════════════════════════════════════════════════${C_RESET}"
    echo ""
    echo -e "  Total de tests automatiques : ${C_GRAS}${total}${C_RESET}"
    echo -e "    ${C_VERT}[ OK ]${C_RESET}  : ${C_VERT}${C_GRAS}${NB_OK}${C_RESET}"
    echo -e "    ${C_ORANGE}[WARN]${C_RESET}  : ${C_ORANGE}${C_GRAS}${NB_WARN}${C_RESET}"
    echo -e "    ${C_ROUGE}[FAIL]${C_RESET}  : ${C_ROUGE}${C_GRAS}${NB_FAIL}${C_RESET}"
    echo -e "    ${C_ORANGE}[SKIP]${C_RESET}  : ${C_ORANGE}${NB_SKIP}${C_RESET}  (sudo indisponible ou non testable)"
    echo ""

    if [[ "${NB_FAIL}" -eq 0 && "${NB_WARN}" -eq 0 ]]; then
        echo -e "  ${C_VERT}${C_GRAS}✔ INSTALLATION VALIDÉE — aucun point bloquant détecté.${C_RESET}"
    elif [[ "${NB_FAIL}" -eq 0 ]]; then
        echo -e "  ${C_ORANGE}${C_GRAS}⚠ INSTALLATION VALIDÉE AVEC ${NB_WARN} AVERTISSEMENT(S) — à examiner.${C_RESET}"
    else
        echo -e "  ${C_ROUGE}${C_GRAS}✘ ${NB_FAIL} POINT(S) À CORRIGER — installation incomplète.${C_RESET}"
    fi

    echo ""
    echo -e "${C_BLEU}${C_GRAS}─── Vérifications manuelles à effectuer ──────────────────────────────────────${C_RESET}"
    local i=1
    for point in "${POINTS_MANUELS[@]}"; do
        echo -e "  ${C_ORANGE}${i}.${C_RESET} ${point}"
        i=$((i + 1))
    done

    echo ""
    echo -e "  Journal complet : ${C_GRAS}${JOURNAL_EFFECTIF}${C_RESET}"
    echo ""
    echo -e "${C_BLEU}  Fait avec amour par Romain J.${C_RESET}"
    echo ""

    # Trace dans le journal
    ecrire_journal "===== RÉSUMÉ ====="
    ecrire_journal "OK=${NB_OK} WARN=${NB_WARN} FAIL=${NB_FAIL} SKIP=${NB_SKIP}"
}

# ── Code principal ────────────────────────────────────────────────────────────
main() {
    # Avertissement si lancé en root
    if [[ "${EUID}" -eq 0 ]]; then
        echo -e "${C_ORANGE}AVERTISSEMENT : ce script est conçu pour être lancé par ${UTILISATEUR_ATTENDU}, pas root.${C_RESET}"
    fi

    # Pré-chauffe sudo AVANT toute opération sudo (préparation du journal incluse)
    if sudo -n true 2>/dev/null; then
        :  # sudo déjà en cache
    else
        echo ""
        echo -e "${C_ORANGE}Certains tests nécessitent sudo. Si refus, ils seront marqués [SKIP].${C_RESET}"
        sudo -v 2>/dev/null || true
    fi

    preparer_journal

    echo -e "${C_BLEU}${C_GRAS}Validation post-installation Ubuntu 26.04${C_RESET}"
    echo -e "Date          : $(date '+%Y-%m-%d %H:%M:%S')"
    echo -e "Utilisateur   : $(id -un)"
    echo -e "Hôte          : $(hostname)"
    echo -e "Journal       : ${JOURNAL_EFFECTIF}"
    ecrire_journal "Démarrage validation — utilisateur $(id -un) sur $(hostname)"

    valider_systeme_base       || true
    valider_paquets            || true
    valider_services           || true
    valider_reseau             || true
    valider_centreon           || true
    valider_applications       || true
    valider_bash               || true
    valider_securite           || true
    valider_developpement      || true
    valider_maj_auto           || true
    valider_vpn                || true
    afficher_verifications_manuelles || true

    afficher_resume

    # Code de sortie : 1 si au moins un FAIL (utile en automatisation/CI), 0 sinon
    [[ "${NB_FAIL}" -eq 0 ]]
}

main "$@"

Conclusion

Ce script n’est pas un produit : c’est un outil personnel, publié tel quel parce que son squelette — configuration séparée et verrouillée, sections rejouables pilotées par un tableau unique, journalisation exhaustive, validation automatique après reboot — se transpose à n’importe quel environnement. Supprimez les sections qui ne vous concernent pas, remplissez le .conf, et faites-en le vôtre. Et si vous ne repartez qu’avec une idée, prenez celle-ci : un /home sur son propre LVM et un script idempotent transforment la réinstallation, corvée redoutée, en opération de routine d’une heure, café compris.

3 mai 2018 /

Voici comment faire un petit dock à partir de la barre de lanceurs (Unity Launcher sous 16.04) qui ressemble à ça:

1. Installation de dconf-tools (nécessaire pour gsettings)
sudo apt install -y dconf-tools
Note :
Sur les versions récentes d’Ubuntu (22.04+), le paquet s’appelle plutôt dconf-cli ou est déjà installé avec GNOME.

Alternative plus précise :
sudo apt install -y dconf-cli

 

2. Personnalisation du Dock (Dash-to-Dock)
gsettings set org.gnome.shell.extensions.dash-to-dock extend-height false
gsettings set org.gnome.shell.extensions.dash-to-dock dock-position BOTTOM
gsettings set org.gnome.shell.extensions.dash-to-dock transparency-mode FIXED
gsettings set org.gnome.shell.extensions.dash-to-dock dash-max-icon-size 30
gsettings set org.gnome.shell.extensions.dash-to-dock unity-backlit-items true
Explication des options :

  • extend-height false : le dock n’occupe pas toute la hauteur de l’écran (plus compact).
  • dock-position BOTTOM : place le dock en bas (comme sous Windows/macOS).
  • transparency-mode FIXED : transparence fixe (pas dynamique selon le fond).
  • dash-max-icon-size 30 : taille des icônes réduite à 30 px (plus discret).
  • unity-backlit-items true : effet de rétro-éclairage sur les applications actives (style Unity).

Pour que les changements prennent effet immédiatement, redémarre GNOME Shell : Alt+F2, taper r, Entrée (ou déconnexion/reconnexion).

On peut également en profiter pour masquer automatiquement le dock et ainsi profiter pleinement de son écran

3. Revenir aux valeurs par défaut
gsettings reset org.gnome.shell.extensions.dash-to-dock extend-height
gsettings reset org.gnome.shell.extensions.dash-to-dock dock-position
gsettings reset org.gnome.shell.extensions.dash-to-dock transparency-mode
gsettings reset org.gnome.shell.extensions.dash-to-dock dash-max-icon-size
gsettings reset org.gnome.shell.extensions.dash-to-dock unity-backlit-items

Alternative plus rapide pour mettre Dash-to-Dock aux valeurs par défaut:

gsettings reset-recursively org.gnome.shell.extensions.dash-to-dock

 

 

3 mai 2018 /
1. Mise à jour complète d’Ubuntu 16.04
sudo apt update && sudo apt dist-upgrade && sudo apt autoremove
2. Installation de update-manager-core (si pas déjà présent)
sudo apt-get install update-manager-core

On vérifie dans le fichier du gestionnaire de mise à jour (/etc/update-manager/release-upgrades) que le paramètre prompt est bien positionné sur « lts » (Prompt=lts)
Si ce n’est pas le cas, changez-le en lts et enregistrez le fichier.

4. Lancement de la mise à niveau vers 18.04 LTS
sudo do-release-upgrade -d

 

 

31 mai 2017 /

Coté serveur Centreon CES 3.3 (192.168.122.58)

On commence par installer le script check_nrpe:

yum install -y nrpe-plugin

 

Par défaut, la variable $USER1$ qui correspond au répertoire où sont stockés les scripts Centreon, pointe sur /usr/lib/nagios/plugins/, alors que check_nrpe a été installé dans /usr/lib64/nagios/plugins/ . Nous devons donc créer un lien symbolique pour que check_nrpe y soit présent.

ln -s /usr/lib64/nagios/plugins/check_nrpe /usr/lib/nagios/plugins/check_nrpe

 

Sur l’interface web de Centreon, nous allons maintenant configurer la commande check_nrpe.
Pour cela nous allons dans Configuration / Commandes puis on clique sur ajouter:

Dans ligne de commande on met ceci:

$USER1$/check_nrpe -H $HOSTADDRESS$ -p $_SERVICEPORT$ $_SERVICECOMMAND$

Puis on clique sur « Décrire les macro » puis sauvegarder sur le pop-up qui apparaît, pour générer le texte suivant:

MACRO (SERVICE) PORT :
MACRO (SERVICE) COMMAND :

Nous pouvons maintenant créer des services NRPE. Pour cela nous allons dans Configuration / Services / Modèles puis on clique sur Ajouter:

On rempli le formulaire comme ci dessus pour la création d’un service surveillance de la charge système d’un hôte.

On met la « Commande de vérification » sur la commande « check_nrpe« .

La variable PORT contient le port de service de NRPE, 5666.
La variable COMMAND contient la valeur « check_load« .
Cela correspond au nom de la commande sur le serveur cible (dans nrpe.cfg).

L’hôte que l’on souhaite monitorer doit donc avoir cette commande déclarer dans son fichier nrpe.cfg, comme on peut le voir ici:

vi /etc/nagios/nrpe.cfg

.....
command[check_load]=/usr/lib/nagios/plugins/check_load -w 15,10,5 -c 30,25,20
......

 

Puis on va dans l’onglet « Relations » pour lié le modèle à des modèles d’hôtes:

On associe le modèle de service au modèle d’hôte qui fonctionneront avec NRPE comme ci-dessus. Il faudra reproduire la même chose pour chaque service NRPE que l’on désire configurer.

On peut maintenant créer dans Centreon notre première machine à monitorer avec notre service NRPE, « check_load« .

 

Coté client Ubuntu (192.168.0.20)

On installe sur le client Ubuntu le deamon NRPE ainsi que des scipts pour le monitorer:

apt-get install nagios-nrpe-server nagios-plugins

On va autoriser le serveur Centreon dans le fichier de configuration NRPE:

vi /etc/nagios/nrpe.cfg

allowed_hosts=127.0.0.1,192.168.122.58

Puis on démarre le service:

/etc/init.d/nagios-nrpe-server start

ou

systemctl start nagios-nrpe-server

On peut vérifier que le client Ubuntu répond au serveur Centreon avec cette commande (à faire depuis Centreon):

/usr/lib/nagios/plugins/check_nrpe -H 192.168.0.20 -c check_load

OK - load average: 0.06, 0.06, 0.01|load1=0.060;15.000;30.000;0; load5=0.060;10.000;25.000;0; load15=0.010;5.000;20.000;0;

On va maintenant déclarer la machine dans Centreon.
Pour cela on va dans Configuration / Hôtes et on clique sur le bouton ajouter

On rempli le formulaire en ayant spécifié le modèle d’hôte « Serveurs-Ubuntu« .
L’hôte va ainsi hérité du service NRPE que nous avons créer tout à l’heure.

On peut vérifié que le service pour la charge système est bien présent pour notre hôte Ubuntu à monitorer.

Pour cela on va dans Configuration / Services et on clique sur le service de notre hôte:

On vérifie que le « Modèle de service » que l’on souhaite, ici « NRPE_Charge_Système » que l’on a configurer précédemment est bien sélectionné.
On met la « Commande de vérification » sur « check_nrpe« .

On peut vérifier que cela fonctionne depuis le serveur Centreon avec cette commande:

/usr/lib/nagios/plugins/check_nrpe -H 192.168.0.20 -c check_load

Si tous est bon, il ne reste plus qu’a recharger la configuration du Collecteur:

 

Si il n’y à pas d’erreur, on coche également « Déplacer les fichier« , et « Redémarrer l’ordonnanceur »

Puis on clic sur exporter.

 

Coté Client CentOS (192.168.122.172)

Pour installer le deamon NRPE, nous devons passer par les dépôts EPEL:

yum install epel-release -y

Puis

yum -y install nrpe nagios-plugin*

On va autoriser le serveur Centreon dans le fichier de configuration NRPE:

vi /etc/nagios/nrpe.cfg

allowed_hosts=127.0.0.1,192.168.122.58

Puis on démarre le service:

service nrpe start
chkconfig nrpe on

Particularité CentOS 7

systemctl restart nrpe
systemctl enable nrpe
firewall-cmd --permanent --add-port=5666/tcp

 

On va maintenant déclarer la machine CentOS dans Centreon.

En ayant spécifié le modèle d’hôte « Serveurs-CentOS« , l’hôte va ainsi hérité du service NRPE que nous avons créer tout à l’heure.

On peut vérifié que le service pour la charge système est bien présent pour notre hôte CentOS à monitorer.

Pour cela on va dans Configuration / Services et on clique sur le service de notre hôte:

On vérifie que le « Modèle de service » que l’on souhaite, ici « NRPE_Charge_Système » que l’on a configurer précédemment est bien sélectionné.
On vérifie que la « Commande de vérification » est bien « check_nrpe« .

On peut vérifier que cela fonctionne depuis le serveur Centreon avec cette commande:

/usr/lib/nagios/plugins/check_nrpe -H 192.168.122.172 -c check_load

Si tous est bon, il ne reste plus qu’a recharger la configuration du Collecteur:

Si il n’y à pas d’erreur, on coche également « Déplacer les fichier », et « Redémarrer l’ordonnanceur »

Puis on clic sur exporter.

 

10 février 2017 /

Voici comment configurer une agrégation de liens sur les ports Ethernet dans Ubuntu 16.04.
Dans mon cas, j’ai deux cartes (ens160 & ens192) qui formeront une interface d’agrégat (bond0).

L’agrégation peut fonctionner suivant plusieurs modes qui détermineront son fonctionnement:

Mode 0 : Round Robin , équilibrage de charge

La transmission des paquets se fait de façon séquentielle sur chacune des cartes actives dans l’agrégat. Ce mode augmente la bande passante et gère la tolérance de panne.

Mode 1 : Active – passive

Ce mode ne gère que la tolérance de panne. Si une des interfaces est désactivée, une autre du bond prend le relais.

Mode 2 : Balance xor

Une interface est affectée à l’envoi vers une même adresse MAC. Ainsi les transferts sont parallélisés et le choix de l’interface suit la règle : (Adresse MAC de la source XOR Adresse MAC de la destination) modulo nombre d’interfaces.

Mode 3 : Broadcast

Tout le trafic est envoyé par toutes les interfaces

mode 4 : 802.3ad ou LCAP

Ce mode s’appuie sur la norme IEEE 802.3ad Dynamic link aggregation. Toutes les interfaces du groupe sont agrégées de façon dynamique, ce qui augmente la bande passante et gère la tolérance de panne. Cela implique que le switch gère le 802.ad et les interfaces soient compatibles mii-tool et/ou ethtool.

mode 5 : balance-tlb

Adaptive transmit load balancing : seule la bande passante en sortie est load balancée selon la charge calculée en fonction de la vitesse, ceci pour chaque interface. Le flux entrant est affecté à l’interface courante. Si celle-ci devient inactive, une autre prend alors l’adresse MAC et devient l’interface courante.

mode 6 : balance-alb

Adaptive load balancing : ce mode inclut en plus du tlb un load balancing sur le flux entrant et seulement pour un trafic IPV4. L’équilibrage est réalisé au niveau ARP. Le module intercepte les réponses pour y réécrire l’adresse MAC de l’une des interfaces du bond tout en tenant compte des spécificités du protocole ARP. La répartition entre les différentes interfaces, se fait de façon séquentielle ( round robin ).

 

Configuration du noyau via le fichier bonding.conf:

 

vi /etc/modprobe.d/bonding.conf

 

et on y ajoute cela:

alias bond0 bonding
options bonding mode=0 miimon=100 lacp_rate=1

 

Pour afficher les informations du module d’agrégation:

 

modinfo bonding

 

Création de l’interface bond0 :

 

Installation du paquet qui va gérer l’agrégation ifenslave :

apt-get -y install ifenslave

 

Édition du fichier interfaces :

vi /etc/network/interfaces

 

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto ens160
iface ens160 inet dhcp

devient

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto ens160
iface ens160 inet manual
bond-master bond0

# The secondary network interface
auto ens192
iface ens192 inet manual
bond-master bond0

auto bond0
iface bond0 inet static
address 10.148.14.244
gateway 10.148.14.1
netmask 255.255.255.0
dns-nameservers 8.8.8.8 8.8.4.4
up /sbin/ifenslave bond0 ens160 ens192
down /sbin/ifenslave -d bond0 ens160 ens192

 

Pour ne pas se retrouver au prochain redémarrage avec ce genre de message:

« A start job is running for raise network interfaces (1 minutes of 5 mins 4 sec) »


il faut éditer ce fichier et modifier la valeur du paramètre « TimeoutStartSec » :

vi /etc/systemd/system/network-online.target.wants/networking.service

[Unit]
Description=Raise network interfaces
Documentation=man:interfaces(5)
DefaultDependencies=no
Wants=network.target
After=local-fs.target network-pre.target apparmor.service systemd-sysctl.service systemd-modules-load.service
Before=network.target shutdown.target network-online.target
Conflicts=shutdown.target

[Install]
WantedBy=multi-user.target
WantedBy=network-online.target

[Service]
Type=oneshot
EnvironmentFile=-/etc/default/networking
ExecStartPre=-/bin/sh -c '[ "$CONFIGURE_INTERFACES" != "no" ] && [ -n "$(ifquery --read-environment --list --exclude=lo)" ] && udevadm settle'
ExecStart=/sbin/ifup -a --read-environment
ExecStop=/sbin/ifdown -a --read-environment
RemainAfterExit=true
TimeoutStartSec=30sec

 

« Failed to start Raise network interfaces – networking.service: Failed with result ‘timeout' »:

Il faut désactiver la configuration par default:

vi /etc/default/networking

On active le paramètre suivant et on le met sur « no »:

CONFIGURE_INTERFACES=no

Puis on redémarre:

reboot

 

Une fois redémarré, Il est possible de visualiser l’état de l’agrégat « bond0 » en affichant le contenu de ce fichier :

cat /proc/net/bonding/bond0

Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)

Bonding Mode: load balancing (round-robin)
MII Status: up
MII Polling Interval (ms): 100
Up Delay (ms): 0
Down Delay (ms): 0

Slave Interface: ens160
MII Status: up
Speed: 10000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 00:0c:29:ae:79:ef
Slave queue ID: 0

Slave Interface: ens192
MII Status: up
Speed: 10000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 00:0c:29:ae:79:f9
Slave queue ID: 0

 

Vérification de l’agrégat:

ifconfig

bond0     Link encap:Ethernet  HWaddr 00:0c:29:ae:79:ef
inet addr:10.148.14.244  Bcast:10.148.14.255  Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:feae:79ef/64 Scope:Link
UP BROADCAST RUNNING MASTER MULTICAST  MTU:1500  Metric:1
RX packets:2467 errors:0 dropped:19 overruns:0 frame:0
TX packets:553 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:231406 (231.4 KB)  TX bytes:45953 (45.9 KB)

ens160    Link encap:Ethernet  HWaddr 00:0c:29:ae:79:ef
UP BROADCAST RUNNING SLAVE MULTICAST  MTU:1500  Metric:1
RX packets:1235 errors:0 dropped:0 overruns:0 frame:0
TX packets:277 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:115802 (115.8 KB)  TX bytes:22197 (22.1 KB)

ens192    Link encap:Ethernet  HWaddr 00:0c:29:ae:79:ef
UP BROADCAST RUNNING SLAVE MULTICAST  MTU:1500  Metric:1
RX packets:1232 errors:0 dropped:0 overruns:0 frame:0
TX packets:276 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:115604 (115.6 KB)  TX bytes:23756 (23.7 KB)

lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING  MTU:65536  Metric:1
RX packets:1920 errors:0 dropped:0 overruns:0 frame:0
TX packets:1920 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:142080 (142.0 KB)  TX bytes:142080 (142.0 KB)

 

On peut également voir son bon fonctionnement avec la commande ip:

ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master bond0 state UP group default qlen 1000
link/ether 00:0c:29:ae:79:ef brd ff:ff:ff:ff:ff:ff
3: ens192: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master bond0 state UP group default qlen 1000
link/ether 00:0c:29:ae:79:ef brd ff:ff:ff:ff:ff:ff
4: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 00:0c:29:ae:79:ef brd ff:ff:ff:ff:ff:ff
inet 10.148.14.244/24 brd 10.148.14.255 scope global bond0
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:feae:79ef/64 scope link tentative dadfailed
valid_lft forever preferred_lft forever