#!/bin/bash # ═══════════════════════════════════════════════════════════════════════════════ # install_adsb_stack.sh — Installation automatique du stack ADS-B complet # Version 3.0 — JCA 2026 # # À LANCER SUR LA MACHINE NEUVE (root ou sudo), depuis le dossier extrait de # l'archive produite par export_adsb_bundle.sh sur Linux-25. # # Comportement : automatique de bout en bout, MAIS demande confirmation avant # toute étape destructive ou modifiant l'état système global (sysctl, udev, # crontab, chown récursif, exécution du schéma SQL, activation systemd). # Idempotent : peut être relancé sans casser un état déjà en place. # # v3.0 (05/08/2026) : aligné sur l'architecture v4.2 (multi-récepteurs). # - détection matérielle via detect_dongles.sh : 1 à 3 dongles, aucun numéro # de série écrit en dur nulle part # - profiles Docker Compose rx1/rx2/rx3 pilotés par gen_env.sh # - secrets sortis du docker-compose.yml vers /data/adsb/.env (chmod 600) # - bridge-nf-call-iptables=0 posé dans /etc/sysctl.d/99-docker-bridge.conf # (et non plus /etc/sysctl.conf, qui avait été écrasé sur Linux-25) # - règle udev déléguant à systemctl restart adsb-stack.service # - watchdog_readsb.sh v4 : seuils prudents, budget de redémarrages, sonde SBS # - paquet rtl-sdr (rtl_test) installé : dépendance de detect_dongles.sh # - index GiST posés d'avance sur les partitions futures # - vérification post-install de la carte historique (fuseau horaire) # # v2.0 (05/07/2026) : reconstruit avec tous les correctifs découverts sur # Linux-25 le 03-05/07/2026 — voir docs/CHANGELOG_BUNDLE.md pour le détail. # Différences clés vs v1.0 : # - schéma SQL généré SANS partitions datées : amorce create_week_partition() # / create_year_partition() sur la date réelle d'installation # - watchdog_readsb.sh v2 (seuil adaptatif jour/nuit) installé par défaut # - cron d'automatisation des partitions (dimanche 20h, +14 jours) installé # - vérification post-install via check_adsb.sh v3.4 en fin de script # ═══════════════════════════════════════════════════════════════════════════════ set -uo pipefail # pas de -e : on gère nous-mêmes les échecs étape par étape BUNDLE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ADSB_DIR="/data/adsb" LOG_FILE="/tmp/install_adsb_$(date +%Y%m%d_%H%M%S).log" GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' ok() { echo -e " ${GREEN}✓${NC} $1" | tee -a "$LOG_FILE"; } fail() { echo -e " ${RED}✗${NC} $1" | tee -a "$LOG_FILE"; } warn() { echo -e " ${YELLOW}⚠${NC} $1" | tee -a "$LOG_FILE"; } info() { echo -e " ${CYAN}→${NC} $1" | tee -a "$LOG_FILE"; } step() { echo -e "\n${BOLD}▸ $1${NC}" | tee -a "$LOG_FILE"; } STEPS_OK=0 STEPS_FAILED=0 STEPS_SKIPPED=0 confirm() { local msg="$1" echo -e "\n ${YELLOW}⚠ CONFIRMATION REQUISE${NC}" echo -e " $msg" read -rp " Continuer ? [o/N] " reply case "$reply" in o|O|oui|OUI) return 0 ;; *) return 1 ;; esac } require_root() { if [ "$(id -u)" -ne 0 ]; then fail "Ce script doit être lancé en root (sudo ./install_adsb_stack.sh)" exit 1 fi } echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ Installation stack ADS-B — machine : $(hostname)" echo -e "${BOLD}║ Bundle source : $BUNDLE_DIR" echo -e "${BOLD}║ Log complet : $LOG_FILE" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}" require_root # ═══════════════════════════════════════════════════════════════════════════════ # 0. PRÉ-VOL — vérifications de base # ═══════════════════════════════════════════════════════════════════════════════ step "0. Pré-vol — vérifications de base" MISSING_TOOLS="" for tool in docker curl python3 lsusb udevadm openssl; do if ! command -v "$tool" &>/dev/null; then MISSING_TOOLS="$MISSING_TOOLS $tool" fi done if [ -n "$MISSING_TOOLS" ]; then fail "Outils manquants :$MISSING_TOOLS — installe-les avant de continuer (apt install docker.io curl python3 usbutils udev openssl)" exit 1 else ok "Tous les outils requis sont présents (docker, curl, python3, lsusb, udevadm, openssl)" fi if ! docker compose version &>/dev/null && ! command -v docker-compose &>/dev/null; then fail "docker compose (plugin ou binaire) introuvable" exit 1 else ok "docker compose disponible" fi for f in docker-compose.yml sql/schema_adsb_base.sql adsb2pg webapp; do if [ ! -e "$BUNDLE_DIR/$f" ]; then fail "Élément manquant dans le bundle : $f" STEPS_FAILED=$((STEPS_FAILED+1)) else ok "Bundle : $f présent" fi done if [ "$STEPS_FAILED" -gt 0 ]; then fail "Bundle incomplet — abandon. Régénère-le avec export_adsb_bundle.sh" exit 1 fi # ── Détection du dongle RTL-SDR ──────────────────────────────────────────────── RTLSDR_USB_LINE=$(lsusb | grep -i "0bda" || true) if [ -z "$RTLSDR_USB_LINE" ]; then fail "Aucun dongle RTL-SDR détecté (idVendor 0bda) — branche-le avant de continuer" exit 1 fi ok "Dongle RTL-SDR détecté : $RTLSDR_USB_LINE" # ── Dépendance rtl-sdr (rtl_test) ───────────────────────────────────────────── # detect_dongles.sh énumère les dongles via rtl_test. Sans ce paquet, il # retombe sur un repli lsusb qui ne donne PAS les numéros de série : les slots # seraient alors affectés à des serials « unknown1/2/3 » et readsb ne pourrait # pas sélectionner son dongle. if ! command -v rtl_test &>/dev/null; then if confirm "Installer le paquet rtl-sdr (fournit rtl_test, requis pour la détection des dongles) ?"; then apt-get update -qq && apt-get install -y rtl-sdr 2>&1 | tee -a "$LOG_FILE" command -v rtl_test &>/dev/null \ && ok "rtl-sdr installé ($(command -v rtl_test))" \ || { fail "rtl_test toujours absent après installation"; exit 1; } else fail "rtl-sdr est requis pour la détection multi-dongles — abandon" exit 1 fi else ok "rtl_test présent : $(command -v rtl_test)" fi # ── Détection des dongles présents (1 à 3) ──────────────────────────────────── # v3.0 : plus aucun numéro de série écrit en dur. detect_dongles.sh est la # source de vérité unique, partagée par gen_env.sh, check_adsb.sh et le # watchdog. Les dongles sont affectés aux slots rx1..rx3 dans leur ordre # d'énumération USB. DETECT_SRC="$BUNDLE_DIR/scripts/detect_dongles.sh" if [ ! -f "$DETECT_SRC" ]; then fail "scripts/detect_dongles.sh absent du bundle — indispensable en v3.0" exit 1 fi # shellcheck source=/dev/null source "$DETECT_SRC" mapfile -t RTLSDR_SERIALS < <(detect_serials) RX_COUNT=${#RTLSDR_SERIALS[@]} if [ "$RX_COUNT" -eq 0 ]; then warn "Aucun numéro de série lisible via rtl_test" warn "Certains dongles ne l'exposent pas, ou un processus détient déjà le device" read -rp " Entre le numéro de série manuellement (ex: 00000011), ou vide pour abandonner : " MANUAL_SN [ -z "$MANUAL_SN" ] && { fail "Au moins un dongle est requis — abandon"; exit 1; } RTLSDR_SERIALS=("$MANUAL_SN") RX_COUNT=1 fi if [ "$RX_COUNT" -gt 3 ]; then warn "$RX_COUNT dongles détectés — seuls les 3 premiers seront configurés" RX_COUNT=3 fi ok "$RX_COUNT dongle(s) RTL-SDR détecté(s) :" for _i in $(seq 0 $((RX_COUNT - 1))); do info " slot rx$((_i + 1)) → SN ${RTLSDR_SERIALS[$_i]}" done # Conservé pour compatibilité avec les étapes suivantes (table receivers, etc.) RTLSDR_SERIAL="${RTLSDR_SERIALS[0]}" # ═══════════════════════════════════════════════════════════════════════════════ # 0b. VOLUMES LVM DÉDIÉS À DOCKER (optionnel, recommandé) # ═══════════════════════════════════════════════════════════════════════════════ # Sur Linux-25, Docker vit sur des LV dédiés (lvdocker/lvdockvol/lvdockoverlay) # plutôt que sur la partition racine — évite qu'une base qui grossit ou des # images qui s'accumulent ne saturent /. On reproduit cette convention ici # si aucun stockage Docker dédié n'est déjà en place. step "0b. Volumes LVM pour Docker" if ! command -v vgs &>/dev/null || ! command -v lvcreate &>/dev/null; then warn "Outils LVM (vgs/lvcreate) absents — étape ignorée, Docker utilisera le stockage par défaut" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) else info "Groupes de volumes (VG) détectés :" vgs --noheadings -o vg_name,vg_size,vg_free --units g 2>/dev/null | tee -a "$LOG_FILE" # Où Docker stocke-t-il déjà ses données (si installé/démarré) ? DOCKER_ROOT="" if command -v docker &>/dev/null && docker info &>/dev/null; then DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null) fi DOCKER_ROOT="${DOCKER_ROOT:-/var/lib/docker}" # Ce chemin est-il déjà un point de montage dédié (donc probablement déjà # sur un LV/disque séparé) ou fait-il juste partie de la racine ("/") ? DOCKER_ROOT_SRC=$(findmnt -no SOURCE --target "$DOCKER_ROOT" 2>/dev/null) ROOT_SRC=$(findmnt -no SOURCE --target "/" 2>/dev/null) if [ -n "$DOCKER_ROOT_SRC" ] && [ "$DOCKER_ROOT_SRC" != "$ROOT_SRC" ]; then ok "$DOCKER_ROOT est déjà sur un point de montage dédié ($DOCKER_ROOT_SRC) — rien à faire" else warn "$DOCKER_ROOT est sur la partition racine ($ROOT_SRC) — pas de LV dédié à Docker" if confirm "Créer des volumes LVM dédiés à Docker (comme sur Linux-25 : lvdocker + lvdockvol + lvdockoverlay) et y déplacer le stockage Docker ?"; then mapfile -t VG_NAMES < <(vgs --noheadings -o vg_name 2>/dev/null | awk '{print $1}') if [ "${#VG_NAMES[@]}" -eq 0 ]; then fail "Aucun VG détecté — impossible de créer les LV. Étape ignorée." STEPS_SKIPPED=$((STEPS_SKIPPED+1)) elif [ "${#VG_NAMES[@]}" -eq 1 ]; then SELECTED_VG="${VG_NAMES[0]}" FREE=$(vgs --noheadings -o vg_free --units g "$SELECTED_VG" 2>/dev/null | tr -d ' ') ok "Un seul VG détecté : $SELECTED_VG (libre : ${FREE}) — sélectionné automatiquement" else echo " VG disponibles :" for i in "${!VG_NAMES[@]}"; do FREE=$(vgs --noheadings -o vg_free --units g "${VG_NAMES[$i]}" 2>/dev/null | tr -d ' ') echo " [$i] ${VG_NAMES[$i]} (libre : ${FREE})" done SELECTED_VG="" while [ -z "$SELECTED_VG" ]; do read -rp " Choisis le numéro du VG à utiliser : " VG_IDX if [[ "$VG_IDX" =~ ^[0-9]+$ ]] && [ -n "${VG_NAMES[$VG_IDX]:-}" ]; then SELECTED_VG="${VG_NAMES[$VG_IDX]}" else warn "Choix invalide ($VG_IDX) — réessaie avec un numéro entre 0 et $((${#VG_NAMES[@]}-1))" fi done fi if [ -z "${SELECTED_VG:-}" ]; then fail "Aucun VG sélectionné — étape ignorée" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) else VG_FREE_G=$(vgs --noheadings -o vg_free --units g "$SELECTED_VG" 2>/dev/null | tr -d ' g') VG_FREE_G=${VG_FREE_G%.*} ok "VG sélectionné : $SELECTED_VG (${VG_FREE_G}G libres)" read -rp " Taille lvdocker (données Docker /var/lib/docker) [20G] : " SZ_DOCKER read -rp " Taille lvdockvol (volumes nommés) [20G] : " SZ_DOCKVOL read -rp " Taille lvdockoverlay (overlay2, images/layers) [40G] : " SZ_OVERLAY SZ_DOCKER="${SZ_DOCKER:-20G}" SZ_DOCKVOL="${SZ_DOCKVOL:-20G}" SZ_OVERLAY="${SZ_OVERLAY:-40G}" # Vérification grossière de l'espace disponible (retire le 'G' final pour comparer) NEED_G=$(( ${SZ_DOCKER%G} + ${SZ_DOCKVOL%G} + ${SZ_OVERLAY%G} )) if [ "${VG_FREE_G:-0}" -lt "$NEED_G" ] 2>/dev/null; then fail "Espace insuffisant dans $SELECTED_VG (${VG_FREE_G}G libres, ${NEED_G}G demandés) — étape ignorée" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) else if confirm "Va créer : lvdocker (${SZ_DOCKER}), lvdockvol (${SZ_DOCKVOL}), lvdockoverlay (${SZ_OVERLAY}) dans $SELECTED_VG, les formater en ext4 (machine neuve — aucune donnée Docker existante à préserver), et arrêter momentanément Docker le temps du montage. Continuer ?"; then systemctl stop docker 2>/dev/null systemctl stop docker.socket 2>/dev/null ok "Service Docker arrêté (montage des volumes en cours)" declare -A LV_MOUNT=( ["lvdocker"]="/var/lib/docker" ["lvdockvol"]="/var/lib/docker/volumes" ["lvdockoverlay"]="/var/lib/docker/overlay2" ) declare -A LV_SIZE=( ["lvdocker"]="$SZ_DOCKER" ["lvdockvol"]="$SZ_DOCKVOL" ["lvdockoverlay"]="$SZ_OVERLAY" ) mkdir -p /var/lib/docker/volumes /var/lib/docker/overlay2 LV_FAIL=0 for lv in lvdocker lvdockvol lvdockoverlay; do if lvs "/dev/$SELECTED_VG/$lv" &>/dev/null; then ok "$lv existe déjà dans $SELECTED_VG — non recréé" continue fi if lvcreate -L "${LV_SIZE[$lv]}" -n "$lv" "$SELECTED_VG" 2>&1 | tee -a "$LOG_FILE"; then mkfs.ext4 -q "/dev/$SELECTED_VG/$lv" 2>&1 | tee -a "$LOG_FILE" ok "$lv créé et formaté (ext4, ${LV_SIZE[$lv]})" else fail "Échec de création de $lv" LV_FAIL=1 fi done if [ "$LV_FAIL" -eq 0 ]; then # Sauvegarde fstab avant modification cp /etc/fstab "/etc/fstab.bak.$(date +%Y%m%d_%H%M%S)" ok "Sauvegarde de /etc/fstab effectuée" for lv in lvdocker lvdockvol lvdockoverlay; do UUID=$(blkid -s UUID -o value "/dev/$SELECTED_VG/$lv") MP="${LV_MOUNT[$lv]}" mkdir -p "$MP" if ! grep -q "$UUID" /etc/fstab; then echo "UUID=$UUID $MP ext4 defaults 0 2" >> /etc/fstab ok "Entrée fstab ajoutée pour $lv → $MP" fi mount "$MP" 2>&1 | tee -a "$LOG_FILE" done ok "Volumes montés — Docker utilisera désormais $SELECTED_VG pour son stockage" else warn "Un ou plusieurs LV n'ont pas pu être créés — Docker redémarrera sur le stockage par défaut" fi systemctl start docker 2>/dev/null systemctl start docker.socket 2>/dev/null sleep 2 if docker info &>/dev/null; then ok "Docker redémarré avec succès" else fail "Docker ne redémarre pas correctement — vérifier 'journalctl -u docker' avant de continuer" exit 1 fi else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Création des LV annulée — Docker reste sur le stockage par défaut" fi fi fi else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "LV dédiés non créés — Docker utilisera le stockage par défaut (partition racine)" fi fi fi # ═══════════════════════════════════════════════════════════════════════════════ # 1. COPIE DES FICHIERS VERS /data/adsb # ═══════════════════════════════════════════════════════════════════════════════ step "1. Copie des fichiers vers $ADSB_DIR" if [ -d "$ADSB_DIR" ] && [ -n "$(ls -A "$ADSB_DIR" 2>/dev/null)" ]; then if ! confirm "$ADSB_DIR existe déjà et n'est pas vide. Les fichiers seront FUSIONNÉS (pas d'écrasement des données existantes, mais les scripts/configs seront mis à jour)."; then fail "Installation annulée par l'utilisateur" exit 1 fi fi mkdir -p "$ADSB_DIR"/{adsb2pg,webapp,scripts,sql,logs} cp -r "$BUNDLE_DIR/adsb2pg/." "$ADSB_DIR/adsb2pg/" && ok "adsb2pg copié" cp -r "$BUNDLE_DIR/webapp/." "$ADSB_DIR/webapp/" && ok "webapp copié" cp "$BUNDLE_DIR/sql/schema_adsb_base.sql" "$ADSB_DIR/sql/" && ok "schema_adsb_base.sql copié" cp "$BUNDLE_DIR/scripts/watchdog_readsb.sh" "$ADSB_DIR/scripts/" 2>/dev/null && ok "watchdog_readsb.sh copié" cp "$BUNDLE_DIR/check_adsb.sh" "$ADSB_DIR/" 2>/dev/null || cp "$BUNDLE_DIR/scripts/check_adsb.sh" "$ADSB_DIR/" 2>/dev/null chmod +x "$ADSB_DIR/scripts/"*.sh "$ADSB_DIR/check_adsb.sh" 2>/dev/null ok "Scripts rendus exécutables" # ── Règle udev ───────────────────────────────────────────────────────────────── # v3.0 : la règle ne redémarre plus un conteneur nommé en dur. Elle délègue à # adsb-stack.service, dont l'ExecStartPre appelle gen_env.sh : brancher un 2e # dongle démarre donc le profile rx2 tout seul, et en retirer un fait # disparaître ses conteneurs (--remove-orphans). UDEV_SRC="$BUNDLE_DIR/scripts/99-adsb-docker.rules" if [ -f "$UDEV_SRC" ]; then if confirm "Installer la règle udev 99-adsb-docker.rules (recalcul auto des profiles au branchement/retrait d'un dongle) ?"; then cp "$UDEV_SRC" /etc/udev/rules.d/ udevadm control --reload-rules udevadm trigger ok "Règle udev copiée et rechargée" # Vérification réelle : la règle doit matcher le dongle de CETTE machine. # On ne teste plus la présence de /dev/adsb_dongle (le symlink n'est plus # utilisé par le compose depuis la v4.2, qui monte /dev/bus/usb en # entier) mais la correspondance idVendor/idProduct. USB_ID=$(lsusb | grep -i "0bda" | head -1) RULE_MATCH=$(grep -c 'idVendor}=="0bda"' /etc/udev/rules.d/99-adsb-docker.rules 2>/dev/null || echo 0) if [ "$RULE_MATCH" -gt 0 ] && [ -n "$USB_ID" ]; then ok "Règle udev cohérente avec le dongle présent : $USB_ID" else warn "La règle ne semble pas matcher ce dongle — vérifier idVendor/idProduct :" lsusb | grep -i "0bda" | tee -a "$LOG_FILE" grep -i "idVendor\|idProduct" /etc/udev/rules.d/99-adsb-docker.rules 2>/dev/null | tee -a "$LOG_FILE" warn "Le stack fonctionnera (readsb accède au bus USB en --privileged), mais sans recalcul auto des profiles" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Règle udev non installée (à faire manuellement si besoin)" fi else warn "Pas de règle udev dans le bundle — à créer manuellement si besoin" fi # ── Paramètres noyau : trafic inter-conteneurs Docker ───────────────────────── # bridge-nf-call-iptables=1 fait remonter le trafic bridgé dans iptables, où il # est jeté SILENCIEUSEMENT (timeout, pas refus de connexion). Sur Linux-25 cela # a coupé le flux Beast readsb→tar1090 pendant 24 h. Le réglage est posé dans # /etc/sysctl.d/ et non dans /etc/sysctl.conf, qui avait été écrasé lors d'un # upgrade et avait fait réapparaître la panne. if confirm "Poser bridge-nf-call-iptables=0 dans /etc/sysctl.d/99-docker-bridge.conf (requis pour la communication inter-conteneurs) ?"; then modprobe br_netfilter 2>/dev/null || true cat > /etc/sysctl.d/99-docker-bridge.conf <<'SYSCTL_EOF' # Le trafic inter-conteneurs sur bridge Docker ne doit pas traverser iptables : # les trames Beast readsb→tar1090 seraient jetées silencieusement (timeout). net.bridge.bridge-nf-call-iptables = 0 net.bridge.bridge-nf-call-ip6tables = 0 SYSCTL_EOF sysctl --system >/dev/null 2>&1 || true BNF=$(sysctl -n net.bridge.bridge-nf-call-iptables 2>/dev/null || echo "?") if [ "$BNF" = "0" ]; then ok "bridge-nf-call-iptables=0 actif et permanent" else warn "bridge-nf-call-iptables=$BNF — le module br_netfilter se charge peut-être après sysctl.d" warn "À revérifier après le prochain redémarrage : sysctl net.bridge.bridge-nf-call-iptables" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Paramètre noyau non posé — risque de blocage silencieux du flux Beast" fi # ═══════════════════════════════════════════════════════════════════════════════ # 2. GÉNÉRATION DU MOT DE PASSE ET CONFIGURATION docker-compose.yml # ═══════════════════════════════════════════════════════════════════════════════ step "2. Configuration docker-compose.yml" NEW_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 25) ok "Nouveau mot de passe PostgreSQL généré (25 caractères, jamais réutilisé d'une machine à l'autre)" # v3.0 : le mot de passe ne figure plus dans docker-compose.yml. Il vit dans # /data/adsb/.env (chmod 600, non versionné), lu par Compose via --env-file. umask 077 cat > "$ADSB_DIR/.env" < "$ADSB_DIR/.env.gains" <<'GAINS_EOF' # Gain par slot : valeur en dB (ex. 49.6) ou « autogain ». # Un gain trop bas rend le récepteur muet SANS erreur : readsb accepte toute # valeur de la table du tuner. Constaté sur Linux-25 avec 7.7 dB — 0 message # décodé pendant 24 h. En cas de doute, laisser autogain. RX1_GAIN=autogain RX2_GAIN=autogain RX3_GAIN=autogain GAINS_EOF ok "Gains par défaut écrits dans $ADSB_DIR/.env.gains (autogain)" fi # Génération initiale de .env.receivers (serials + COMPOSE_PROFILES) if [ -x "$ADSB_DIR/scripts/gen_env.sh" ]; then "$ADSB_DIR/scripts/gen_env.sh" >/dev/null 2>&1 || true PROFILES=$(sed -n 's/^COMPOSE_PROFILES=//p' "$ADSB_DIR/.env.receivers" 2>/dev/null) ok "Environnement récepteurs généré — profiles=${PROFILES:-aucun}" else fail "scripts/gen_env.sh absent ou non exécutable — le stack ne pourra pas démarrer ses récepteurs" exit 1 fi # Coordonnées de la station : proposées par défaut, modifiables read -rp " Latitude de la station [48.8236] : " STATION_LAT read -rp " Longitude de la station [2.2770] : " STATION_LON STATION_LAT="${STATION_LAT:-48.8236}" STATION_LON="${STATION_LON:-2.2770}" sed -i -e "s/READSB_LAT=[^[:space:]]*/READSB_LAT=${STATION_LAT}/" \ -e "s/READSB_LON=[^[:space:]]*/READSB_LON=${STATION_LON}/" \ -e "s/LAT=[^[:space:]]*/LAT=${STATION_LAT}/" \ -e "s/LONG=[^[:space:]]*/LONG=${STATION_LON}/" \ -e "s/STATION_LAT=[^[:space:]]*/STATION_LAT=${STATION_LAT}/" \ -e "s/STATION_LON=[^[:space:]]*/STATION_LON=${STATION_LON}/" \ -e "s/TAR1090_DEFAULTCENTERLAT=[^[:space:]]*/TAR1090_DEFAULTCENTERLAT=${STATION_LAT}/" \ -e "s/TAR1090_DEFAULTCENTERLON=[^[:space:]]*/TAR1090_DEFAULTCENTERLON=${STATION_LON}/" \ "$ADSB_DIR/docker-compose.yml" ok "Coordonnées station : $STATION_LAT, $STATION_LON" # ═══════════════════════════════════════════════════════════════════════════════ # 3. LANCEMENT DES CONTENEURS # ═══════════════════════════════════════════════════════════════════════════════ step "3. Lancement des conteneurs Docker" # v3.0 : les DEUX --env-file sont obligatoires. Dès qu'on en passe un, Compose # cesse de charger .env automatiquement ; et sans .env.receivers, COMPOSE_PROFILES # n'est pas défini — aucun récepteur n'est alors éligible et seule la webapp # démarre, sans le moindre message d'erreur. if confirm "Lancer 'docker compose up -d' dans $ADSB_DIR (démarre les récepteurs détectés, webapp, postgres) ?"; then (cd "$ADSB_DIR" && docker compose \ --env-file "$ADSB_DIR/.env" \ --env-file "$ADSB_DIR/.env.receivers" \ up -d --build --remove-orphans) 2>&1 | tee -a "$LOG_FILE" if [ "${PIPESTATUS[0]}" -eq 0 ]; then ok "Conteneurs démarrés" else fail "Échec du démarrage des conteneurs — voir $LOG_FILE" exit 1 fi else fail "Installation interrompue — les conteneurs doivent tourner pour la suite" exit 1 fi info "Attente du démarrage de PostgreSQL (jusqu'à 60s)..." PG_READY=false for i in $(seq 1 30); do if docker exec wordpress-postgres pg_isready -U pgz_admin &>/dev/null; then PG_READY=true break fi sleep 2 done if $PG_READY; then ok "PostgreSQL prêt après ~$((i*2))s" else fail "PostgreSQL ne répond pas après 60s — vérifier 'docker logs wordpress-postgres'" exit 1 fi # ═══════════════════════════════════════════════════════════════════════════════ # 3b. TUNING POSTGRESQL SELON LA RAM DE LA MACHINE # ═══════════════════════════════════════════════════════════════════════════════ # Les valeurs codées en dur sur Linux-25 (shared_buffers=3200MB, etc.) ont été # calibrées pour SES 16 Go de RAM — les réutiliser telles quelles sur une # machine avec moins (ou plus) de mémoire serait au mieux sous-optimal, au pire # risquerait un échec de démarrage PostgreSQL (shared_buffers > RAM dispo). # On calcule donc les valeurs à partir de la RAM réellement détectée. step "3b. Tuning PostgreSQL selon la RAM de la machine" TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') ok "RAM totale détectée : ${TOTAL_RAM_MB} MB" if [ "${TOTAL_RAM_MB:-0}" -lt 2048 ]; then warn "RAM très limitée (<2GB) — tuning conservateur appliqué, les performances seront limitées" fi # Formules calibrées pour reproduire les ratios observés sur Linux-25 (16384 MB # → shared_buffers 3200MB ≈ 20%, effective_cache_size 9216MB ≈ 56%, # work_mem 32MB ≈ 0.2%, max_wal_size 4096MB ≈ 25%) tout en gardant des planchers # raisonnables pour les petites machines. SHARED_BUFFERS_MB=$(( TOTAL_RAM_MB * 20 / 100 )) EFFECTIVE_CACHE_MB=$(( TOTAL_RAM_MB * 56 / 100 )) WORK_MEM_MB=$(( TOTAL_RAM_MB * 2 / 1000 )) MAINTENANCE_WORK_MEM_MB=$(( SHARED_BUFFERS_MB / 6 )) MAX_WAL_SIZE_MB=$(( TOTAL_RAM_MB * 25 / 100 )) MIN_WAL_SIZE_MB=$(( MAX_WAL_SIZE_MB / 4 )) # Planchers pour éviter des valeurs absurdes sur une machine à faible RAM [ "$SHARED_BUFFERS_MB" -lt 128 ] && SHARED_BUFFERS_MB=128 [ "$WORK_MEM_MB" -lt 4 ] && WORK_MEM_MB=4 [ "$MAINTENANCE_WORK_MEM_MB" -lt 64 ] && MAINTENANCE_WORK_MEM_MB=64 [ "$MAX_WAL_SIZE_MB" -lt 512 ] && MAX_WAL_SIZE_MB=512 [ "$MIN_WAL_SIZE_MB" -lt 128 ] && MIN_WAL_SIZE_MB=128 # Détection SSD vs HDD sur le disque hébergeant /var/lib/docker (best-effort) — # influence random_page_cost et effective_io_concurrency. DOCKER_DISK_DEV=$(findmnt -no SOURCE --target /var/lib/docker 2>/dev/null | sed -E 's/[0-9]+$//; s/.*\///') ROTATIONAL=1 if [ -n "$DOCKER_DISK_DEV" ] && [ -f "/sys/block/$DOCKER_DISK_DEV/queue/rotational" ]; then ROTATIONAL=$(cat "/sys/block/$DOCKER_DISK_DEV/queue/rotational" 2>/dev/null) fi if [ "$ROTATIONAL" = "0" ]; then RANDOM_PAGE_COST="1.1" EFFECTIVE_IO_CONCURRENCY="150" ok "Disque détecté comme SSD ($DOCKER_DISK_DEV) — random_page_cost=1.1, effective_io_concurrency=150" else RANDOM_PAGE_COST="4" EFFECTIVE_IO_CONCURRENCY="2" warn "Disque détecté comme rotatif/HDD ou indéterminé ($DOCKER_DISK_DEV) — random_page_cost=4, effective_io_concurrency=2 (confirme manuellement si c'est bien un SSD)" fi echo "" echo " Valeurs calculées pour ${TOTAL_RAM_MB} MB de RAM :" echo " shared_buffers = ${SHARED_BUFFERS_MB}MB" echo " effective_cache_size = ${EFFECTIVE_CACHE_MB}MB" echo " work_mem = ${WORK_MEM_MB}MB" echo " maintenance_work_mem = ${MAINTENANCE_WORK_MEM_MB}MB" echo " max_wal_size = ${MAX_WAL_SIZE_MB}MB" echo " min_wal_size = ${MIN_WAL_SIZE_MB}MB" echo " random_page_cost = ${RANDOM_PAGE_COST}" echo " effective_io_concurrency = ${EFFECTIVE_IO_CONCURRENCY}" if confirm "Appliquer ce tuning PostgreSQL via ALTER SYSTEM SET (redémarre le conteneur postgres pour prendre effet) ?"; then docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c " ALTER SYSTEM SET shared_buffers = '${SHARED_BUFFERS_MB}MB'; ALTER SYSTEM SET effective_cache_size = '${EFFECTIVE_CACHE_MB}MB'; ALTER SYSTEM SET work_mem = '${WORK_MEM_MB}MB'; ALTER SYSTEM SET maintenance_work_mem = '${MAINTENANCE_WORK_MEM_MB}MB'; ALTER SYSTEM SET max_wal_size = '${MAX_WAL_SIZE_MB}MB'; ALTER SYSTEM SET min_wal_size = '${MIN_WAL_SIZE_MB}MB'; ALTER SYSTEM SET random_page_cost = ${RANDOM_PAGE_COST}; ALTER SYSTEM SET effective_io_concurrency = ${EFFECTIVE_IO_CONCURRENCY}; ALTER SYSTEM SET max_locks_per_transaction = 2048; " 2>&1 | tee -a "$LOG_FILE" docker restart wordpress-postgres 2>&1 | tee -a "$LOG_FILE" info "Attente du redémarrage de PostgreSQL..." sleep 3 PG_READY2=false for i in $(seq 1 20); do if docker exec wordpress-postgres pg_isready -U pgz_admin &>/dev/null; then PG_READY2=true break fi sleep 2 done if $PG_READY2; then ok "PostgreSQL redémarré avec le nouveau tuning" # Vérification que shared_buffers a bien été pris en compte (pas resté # sur une valeur d'auto.conf pré-existante — piège rencontré sur Linux-25) ACTUAL_SB=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc "SHOW shared_buffers;" 2>/dev/null | tr -d ' ') info "shared_buffers effectif (runtime) : $ACTUAL_SB (attendu ~${SHARED_BUFFERS_MB}MB)" else fail "PostgreSQL ne redémarre pas après le tuning — vérifier 'docker logs wordpress-postgres' (valeur shared_buffers trop élevée pour la RAM/shmmax du noyau ?)" exit 1 fi else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Tuning PostgreSQL non appliqué — valeurs par défaut de l'image conservées (sous-optimal mais fonctionnel)" fi # ═══════════════════════════════════════════════════════════════════════════════ # 4. SCHÉMA POSTGRESQL # ═══════════════════════════════════════════════════════════════════════════════ step "4. Application du schéma PostgreSQL" SCHEMA_EXISTS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc \ "SELECT 1 FROM information_schema.schemata WHERE schema_name='adsb';" 2>/dev/null) if [ "$SCHEMA_EXISTS" = "1" ]; then warn "Le schéma 'adsb' existe déjà dans cette base — schéma NON réappliqué (évite d'écraser des données)" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) else if confirm "Appliquer schema_adsb_base.sql (crée le schéma adsb : tables, fonctions, partitions courantes) ?"; then docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \ --set ON_ERROR_STOP=1 < "$ADSB_DIR/sql/schema_adsb_base.sql" 2>&1 | tee -a "$LOG_FILE" if [ "${PIPESTATUS[0]}" -eq 0 ]; then ok "Schéma adsb créé, partitions courantes amorcées" else fail "Échec de l'application du schéma — voir $LOG_FILE (ON_ERROR_STOP a stoppé au premier problème, pas d'état partiel dangereux)" exit 1 fi else fail "Installation interrompue — le schéma est requis pour la suite" exit 1 fi fi # ── Correctif ISOYEAR sur create_week_partition ─────────────────────────────── # v3.0 : la fonction d'origine mélangeait EXTRACT(YEAR) (année civile) et # EXTRACT(WEEK) (semaine ISO). Aux charnières d'année les deux divergent : une # partition pouvait être nommée d'après une semaine déjà prise, le test # d'existence renvoyait « existe déjà », la partition n'était PAS créée et les # insertions de cette semaine échouaient silencieusement. if [ -f "$ADSB_DIR/sql/create_week_partition_fix.sql" ]; then if confirm "Appliquer le correctif ISOYEAR sur adsb.create_week_partition ?"; then docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \ --set ON_ERROR_STOP=1 < "$ADSB_DIR/sql/create_week_partition_fix.sql" 2>&1 | tee -a "$LOG_FILE" [ "${PIPESTATUS[0]}" -eq 0 ] \ && ok "create_week_partition corrigée (ISOYEAR + garde sur les bornes)" \ || { warn "Échec du correctif ISOYEAR — à appliquer manuellement"; STEPS_SKIPPED=$((STEPS_SKIPPED+1)); } else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Correctif ISOYEAR non appliqué — risque de partition manquante aux changements d'année" fi else warn "sql/create_week_partition_fix.sql absent du bundle" fi # ── Zones d'exclusion pour l'analyse des départs ────────────────────────────── # Alimente les onglets « Départs hors aérodromes » et « Zones de départ » : # tout départ détecté dans le rayon d'une zone active est écarté des résultats. # Le référentiel couvre l'Île-de-France ; sur une autre implantation, il faudra # l'adapter depuis l'interface (bouton « Gérer les exclusions »). for SQLZ in 04_airports.sql 05_airports_zones.sql; do if [ -f "$ADSB_DIR/sql/$SQLZ" ]; then docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \ --set ON_ERROR_STOP=1 < "$ADSB_DIR/sql/$SQLZ" >> "$LOG_FILE" 2>&1 if [ $? -eq 0 ]; then ok "$SQLZ appliqué" else warn "Échec de $SQLZ — voir $LOG_FILE" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi else warn "sql/$SQLZ absent du bundle — l'analyse des départs sera indisponible" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi done AERO_N=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc \ "SELECT count(*) FROM adsb.airports WHERE actif;" 2>/dev/null) if [ "${AERO_N:-0}" -gt 0 ]; then ok "$AERO_N zone(s) d'exclusion active(s)" warn "Référentiel calibré pour l'Île-de-France : vérifier les aérodromes" warn " proches de CETTE station via l'onglet « Zones de départ »" else warn "Aucune zone d'exclusion active — l'analyse des départs remontera tout le trafic" fi # ── Partitions et index GiST d'avance ───────────────────────────────────────── # Les requêtes cartographiques filtrent sur geom : sans index GiST sur la # partition en cours d'écriture, la carte historique part en timeout et revient # vide. Le symptôme apparaît au rollover hebdomadaire, pas à l'installation — # d'où la création anticipée sur 8 semaines. if confirm "Créer les partitions des 8 prochaines semaines avec leurs index GiST ?"; then for W in $(seq 0 7); do docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc \ "SELECT adsb.create_week_partition((CURRENT_DATE + INTERVAL '$((W * 7)) days')::date);" \ >> "$LOG_FILE" 2>&1 || true done # Filet de sécurité : certaines partitions peuvent préexister sans index. docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c " DO \$\$ DECLARE r record; BEGIN FOR r IN SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid WHERE i.inhparent = 'adsb.positions'::regclass AND NOT EXISTS (SELECT 1 FROM pg_index x JOIN pg_class ic ON ic.oid = x.indexrelid WHERE x.indrelid = c.oid AND ic.relname LIKE '%geom_gix') LOOP EXECUTE format('CREATE INDEX %I ON adsb.%I USING GIST (geom)', r.relname || '_geom_gix', r.relname); END LOOP; END \$\$;" 2>&1 | tee -a "$LOG_FILE" PART_COUNT=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc \ "SELECT count(*) FROM pg_inherits WHERE inhparent='adsb.positions'::regclass;" 2>/dev/null) ok "Partitions amorcées (${PART_COUNT:-?} au total) et index GiST posés" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Partitions non amorcées — la carte historique sera vide au prochain rollover" fi # ── Enregistrement des récepteurs dans adsb.receivers ───────────────────────── # v3.0 : boucle sur tous les dongles détectés, un enregistrement par slot. step "4b. Enregistrement des récepteurs" for _i in $(seq 0 $((RX_COUNT - 1))); do _SN="${RTLSDR_SERIALS[$_i]}" _SLOT=$((_i + 1)) RECEIVER_EXISTS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -tAc \ "SELECT 1 FROM adsb.receivers WHERE serial='${_SN}';" 2>/dev/null) if [ "$RECEIVER_EXISTS" = "1" ]; then ok "Récepteur serial=$_SN déjà enregistré" else docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c " INSERT INTO adsb.receivers (serial, name, manufacturer, model, chipset, lat, lon, location_desc, active) VALUES ('${_SN}', '$(hostname) rx${_SLOT}', 'Generic', 'RTL2832U', 'R820T', ${STATION_LAT}, ${STATION_LON}, '$(hostname) — slot rx${_SLOT}, installation automatique', true) ON CONFLICT (serial) DO NOTHING; " 2>&1 | tee -a "$LOG_FILE" ok "Récepteur enregistré dans adsb.receivers (serial=$_SN, slot rx${_SLOT})" fi done # ═══════════════════════════════════════════════════════════════════════════════ # 5. CRONTAB — watchdog + automatisation des partitions # ═══════════════════════════════════════════════════════════════════════════════ step "5. Tâches planifiées (crontab)" CURRENT_CRON=$(crontab -l 2>/dev/null || echo "") NEW_CRON="$CURRENT_CRON" if echo "$CURRENT_CRON" | grep -q "watchdog_readsb.sh"; then ok "Entrée crontab watchdog déjà présente" else if confirm "Ajouter au crontab : watchdog readsb toutes les 5 min (*/5 * * * * $ADSB_DIR/scripts/watchdog_readsb.sh) ?"; then NEW_CRON="${NEW_CRON} */5 * * * * $ADSB_DIR/scripts/watchdog_readsb.sh" ok "Ligne watchdog ajoutée" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Watchdog non planifié — le dongle ne sera pas relancé automatiquement en cas de gel" fi fi # Le watchdog v4 exige detect_dongles.sh : sans lui il refuse de tourner plutôt # que de redémarrer un slot au hasard. if [ -f "$ADSB_DIR/scripts/watchdog_readsb.sh" ]; then if grep -q "detect_dongles.sh" "$ADSB_DIR/scripts/watchdog_readsb.sh"; then ok "Watchdog v4 installé (récepteurs dérivés de detect_dongles.sh)" else warn "Watchdog antérieur à la v4 : seuil trop agressif, aucun budget de redémarrages" warn "Il empêcherait l'autogain de converger — mettre à jour scripts/watchdog_readsb.sh" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi fi if echo "$CURRENT_CRON" | grep -q "create_week_partition"; then ok "Entrée crontab automatisation partitions déjà présente" else if confirm "Ajouter au crontab : création automatique de la partition +14 jours chaque dimanche 20h ?"; then NEW_CRON="${NEW_CRON} 0 20 * * 0 docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \"SELECT adsb.create_week_partition((CURRENT_DATE + INTERVAL '14 days')::date);\" >> $ADSB_DIR/logs/partition_cron.log 2>&1" ok "Ligne d'automatisation des partitions ajoutée" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Automatisation des partitions non planifiée — pense à créer les partitions manuellement chaque semaine" fi fi if [ "$NEW_CRON" != "$CURRENT_CRON" ]; then echo "$NEW_CRON" | crontab - ok "Crontab mis à jour" fi # ── Vérification RÉELLE de la crontab (pas juste "la commande n'a pas échoué") ── # Sur une machine neuve minimaliste, le service cron peut être absent/désactivé # — dans ce cas les lignes crontab sont enregistrées mais ne s'exécuteront # jamais, ce qui serait un échec silencieux découvert seulement des jours plus # tard (exactement le genre d'incident qu'on a eu sur Linux-25). info "Vérification de la crontab installée..." INSTALLED_CRON=$(crontab -l 2>/dev/null || echo "") CRON_TESTS_OK=true if echo "$INSTALLED_CRON" | grep -q "watchdog_readsb.sh"; then ok "Ligne watchdog confirmée dans la crontab" else warn "Ligne watchdog ABSENTE de la crontab (ignorée précédemment ou échec d'écriture)" CRON_TESTS_OK=false fi if echo "$INSTALLED_CRON" | grep -q "create_week_partition"; then ok "Ligne automatisation partitions confirmée dans la crontab" else warn "Ligne automatisation partitions ABSENTE de la crontab" CRON_TESTS_OK=false fi CRON_SERVICE="" for svc in cron crond; do if systemctl list-unit-files 2>/dev/null | grep -q "^${svc}\.service"; then CRON_SERVICE="$svc" break fi done if [ -z "$CRON_SERVICE" ]; then fail "Aucun service cron/crond installé sur cette machine — les tâches planifiées ne s'exécuteront JAMAIS" if confirm "Installer le paquet cron maintenant (apt install -y cron) ?"; then apt install -y cron 2>&1 | tee -a "$LOG_FILE" systemctl enable --now cron 2>&1 | tee -a "$LOG_FILE" ok "Paquet cron installé et activé" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "cron non installé — watchdog et automatisation des partitions resteront inactifs" fi else if systemctl is-active --quiet "$CRON_SERVICE"; then ok "Service $CRON_SERVICE actif" else warn "Service $CRON_SERVICE installé mais INACTIF" if confirm "Activer et démarrer $CRON_SERVICE maintenant ?"; then systemctl enable --now "$CRON_SERVICE" 2>&1 | tee -a "$LOG_FILE" ok "$CRON_SERVICE activé et démarré" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "$CRON_SERVICE reste inactif — les tâches planifiées ne s'exécuteront pas" fi fi fi if ! $CRON_TESTS_OK; then info "Rappel : relance ce script pour proposer à nouveau les entrées crontab manquantes (idempotent)" fi # ═══════════════════════════════════════════════════════════════════════════════ # 5b. PARE-FEU # ═══════════════════════════════════════════════════════════════════════════════ # Le stack expose 8080 (webapp), 8090 (tar1090) et 30005 (Beast TCP) sur toutes # les interfaces (0.0.0.0) selon docker-compose.yml — un pare-feu actif sur la # machine neuve pourrait bloquer l'accès LAN à ces services même si les # conteneurs tournent parfaitement. step "5c. Pare-feu" FIREWALL_TOOL="" if command -v ufw &>/dev/null; then FIREWALL_TOOL="ufw" elif command -v firewall-cmd &>/dev/null; then FIREWALL_TOOL="firewalld" fi if [ -z "$FIREWALL_TOOL" ]; then info "Aucun pare-feu (ufw/firewalld) détecté sur cette machine — aucune restriction à gérer" else case "$FIREWALL_TOOL" in ufw) UFW_STATUS=$(ufw status 2>/dev/null | head -1) info "ufw détecté — $UFW_STATUS" if echo "$UFW_STATUS" | grep -qi "inactive"; then ok "ufw inactif — aucune règle à ajouter, tous les ports sont déjà accessibles" else echo " Règles ufw actuelles :" ufw status numbered 2>/dev/null | tee -a "$LOG_FILE" for PORT_DESC in "8080:webapp" "8090:tar1090" "30005:Beast TCP"; do PORT="${PORT_DESC%%:*}"; DESC="${PORT_DESC##*:}" if ufw status 2>/dev/null | grep -qE "^${PORT}(/tcp)?\s+ALLOW"; then ok "Port $PORT ($DESC) déjà autorisé" else if confirm "ufw est actif et bloque probablement le port $PORT ($DESC) — l'autoriser (ufw allow $PORT/tcp) ?"; then ufw allow "${PORT}/tcp" 2>&1 | tee -a "$LOG_FILE" ok "Port $PORT ($DESC) autorisé" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Port $PORT ($DESC) non autorisé — inaccessible depuis le LAN tant que la règle n'est pas ajoutée" fi fi done fi ;; firewalld) FW_STATE=$(firewall-cmd --state 2>/dev/null) info "firewalld détecté — état : $FW_STATE" if [ "$FW_STATE" != "running" ]; then ok "firewalld non actif — aucune règle à ajouter" else for PORT_DESC in "8080:webapp" "8090:tar1090" "30005:Beast TCP"; do PORT="${PORT_DESC%%:*}"; DESC="${PORT_DESC##*:}" if firewall-cmd --list-ports 2>/dev/null | grep -q "${PORT}/tcp"; then ok "Port $PORT ($DESC) déjà autorisé" else if confirm "firewalld est actif et bloque probablement le port $PORT ($DESC) — l'autoriser en permanence ?"; then firewall-cmd --permanent --add-port="${PORT}/tcp" 2>&1 | tee -a "$LOG_FILE" ok "Port $PORT ($DESC) autorisé (permanent)" else STEPS_SKIPPED=$((STEPS_SKIPPED+1)) warn "Port $PORT ($DESC) non autorisé" fi fi done firewall-cmd --reload 2>&1 | tee -a "$LOG_FILE" ok "firewalld rechargé" fi ;; esac fi # Test RÉEL de portée réseau (indépendant du pare-feu — confirme l'accès effectif) info "Test d'accès réel aux ports exposés (depuis localhost)..." for PORT_DESC in "8080:webapp" "8090:tar1090"; do PORT="${PORT_DESC%%:*}"; DESC="${PORT_DESC##*:}" if curl -s -o /dev/null -w "%{http_code}" --max-time 3 "http://127.0.0.1:${PORT}/" 2>/dev/null | grep -q "200"; then ok "Port $PORT ($DESC) répond HTTP 200" else warn "Port $PORT ($DESC) ne répond pas encore — normal si les conteneurs finissent tout juste de démarrer" fi done # ═══════════════════════════════════════════════════════════════════════════════ # 6. VÉRIFICATION FINALE # ═══════════════════════════════════════════════════════════════════════════════ step "6. Vérification finale" sleep 5 # laisser le temps à adsb2pg de faire au moins un cycle # ── Contrôles fonctionnels v3.0 ─────────────────────────────────────────────── # Ces trois vérifications correspondent aux pannes réellement rencontrées sur # Linux-25 : chacune produisait un stack « vert » mais inexploitable. # 1. Le flux Beast passe-t-il de readsb à tar1090 ? (bridge-nf-call-iptables) info "Vérification du flux Beast readsb → tar1090 (30s d'attente)..." sleep 30 AC_MSGS=$(curl -s --max-time 10 "http://127.0.0.1:8090/data/aircraft.json" 2>/dev/null \ | grep -oP '"messages"\s*:\s*\K[0-9]+' | head -1) if [ "${AC_MSGS:-0}" -gt 0 ]; then ok "tar1090 agrège ${AC_MSGS} messages — le flux Beast passe" else warn "tar1090 annonce 0 message — vérifier dans l'ordre :" warn " a) sysctl net.bridge.bridge-nf-call-iptables (doit valoir 0)" warn " b) le gain : un gain trop bas rend readsb muet sans erreur" warn " c) docker logs adsb-tar1090 | grep -i beast (timeout = filtrage)" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi # 2. Le récepteur décode-t-il réellement ? (sonde SBS directe, sans tar1090) SBS_BYTES=$(timeout 8 bash -c 'exec 3<>/dev/tcp/127.0.0.1/30003 && head -c 400 <&3' 2>/dev/null | wc -c) if [ "${SBS_BYTES:-0}" -ge 50 ]; then ok "Port SBS 30003 : ${SBS_BYTES} octets reçus — le récepteur décode" else warn "Port SBS 30003 silencieux — readsb ne décode rien (gain, antenne ou connecteur)" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi # 3. La carte historique renvoie-t-elle des données ? (fuseau horaire) # Sans date_default_timezone_set dans api.php, PHP retombe sur UTC et la # fenêtre interrogée est décalée : la carte reste vide sans aucune erreur HTTP. if [ -f "$ADSB_DIR/webapp/src/api.php" ]; then if grep -q "date_default_timezone_set" "$ADSB_DIR/webapp/src/api.php"; then ok "api.php : fuseau horaire ancré" else warn "api.php : date_default_timezone_set absent — carte historique vide (décalage UTC/locale)" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi fi # 5. Les endpoints d'analyse des départs répondent-ils ? for ACT in airports departures_offfield departures_offfield_zones; do BODY=$(curl -s --max-time 25 "http://127.0.0.1:8080/api.php?action=${ACT}&window=1440" 2>/dev/null) if echo "$BODY" | grep -q '"error"'; then warn "api.php?action=${ACT} : $(echo "$BODY" | head -c 120)" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) elif echo "$BODY" | head -c 1 | grep -q '{'; then ok "api.php?action=${ACT} : réponse valide" else warn "api.php?action=${ACT} : réponse inattendue" STEPS_SKIPPED=$((STEPS_SKIPPED+1)) fi done # 6. Les drapeaux sont-ils servis ? (sinon le JS retombe sur le pictogramme « ? ») FLAG_N=$(docker exec adsb-webapp sh -c 'ls /var/www/html/assets/flags/*.png 2>/dev/null | wc -l' 2>/dev/null) if [ "${FLAG_N:-0}" -gt 50 ]; then ok "$FLAG_N drapeau(x) disponible(s) dans l'image" else warn "Seulement ${FLAG_N:-0} drapeau(x) — les pays s'afficheront avec un pictogramme générique" warn " Relancer : /data/adsb/scripts/fetch_flags.sh puis rebuild de la webapp" fi if [ -x "$ADSB_DIR/check_adsb.sh" ]; then info "Lancement de check_adsb.sh pour valider l'installation..." "$ADSB_DIR/check_adsb.sh" 2>&1 | tee -a "$LOG_FILE" else warn "check_adsb.sh absent ou non exécutable — vérification manuelle recommandée :" info " docker ps" info " curl -s http://127.0.0.1:8090/data/aircraft.json | head -c 300" fi echo -e "\n${BOLD}╔══════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ INSTALLATION TERMINÉE ║${NC}" echo -e "${BOLD}╠══════════════════════════════════════════════════════╣${NC}" echo -e "${BOLD}║ Récepteur : serial=$RTLSDR_SERIAL" echo -e "${BOLD}║ Log complet : $LOG_FILE" echo -e "${BOLD}║ Dashboard : http://$(hostname -I | awk '{print $1}'):8080" echo -e "${BOLD}║ tar1090 : http://$(hostname -I | awk '{print $1}'):8090" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}" if [ "$STEPS_SKIPPED" -gt 0 ]; then warn "$STEPS_SKIPPED étape(s) ignorée(s) sur demande — relance ce script pour les proposer à nouveau si besoin (idempotent)" fi