1050 lines
56 KiB
Bash
Executable File
1050 lines
56 KiB
Bash
Executable File
#!/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" <<ENV_EOF
|
|
# Secrets du stack ADS-B — NE PAS VERSIONNER
|
|
PG_PASSWORD=${NEW_PASSWORD}
|
|
ENV_EOF
|
|
chmod 600 "$ADSB_DIR/.env"
|
|
umask 022
|
|
ok "Secrets écrits dans $ADSB_DIR/.env (chmod 600)"
|
|
|
|
# Le compose est copié tel quel : plus aucun numéro de série à substituer, les
|
|
# serials sont injectés au démarrage par gen_env.sh via .env.receivers.
|
|
cp "$BUNDLE_DIR/docker-compose.yml" "$ADSB_DIR/docker-compose.yml"
|
|
ok "docker-compose.yml installé (profiles rx1/rx2/rx3, serials injectés à l'exécution)"
|
|
|
|
# Gains par slot : autogain par défaut, laisse converger le tuner.
|
|
if [ ! -f "$ADSB_DIR/.env.gains" ]; then
|
|
cat > "$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
|