pl4y.store

6 - Shell

addition.sh

.sh 830 lignes - 47KB

addition.sh
#!/bin/bash
# =============================================================================
# addition.sh - LES SUPPLEMENTS QUI NE SONT PAS DANS L ISO
#
# Jumelle d update.sh, et lancee comme elle : une icone GTK sur le bureau
# (osmo-addition-anim), ou a la main.
#
# POURQUOI DOCKER N EST PAS DANS L IMAGE
# L ISO est un noeud NATIF : elle porte le banc complet sans conteneur, et
# n a donc besoin de docker pour rien. L y embarquer couterait ~500 Mo et le
# demon tournerait en permanence sur un banc qui ne s en sert pas - avec son
# bridge, ses regles iptables et sa MASQUERADE, au milieu d une machine dont
# tout l interet est de router du GSM a la main.
# Docker ne sert qu a UN scenario : le multi-operateur (start-multi.sh), ou les
# operateurs 2 et 3 et l inter-STP tournent en conteneurs a cote du natif.
# C est un supplement, il s installe comme tel - ici.
#
# Usage :
#   sudo ./addition.sh            fenetre GTK : "Docker container and SS7
#                                 multioperator", puis une seconde fenetre en
#                                 boutons radio : image TELECHARGEE ou COMPILEE
#                                 (exclusif). osmocom-run est derivee dans la
#                                 foulee : start-multi.sh demarre juste apres.
#   sudo ./addition.sh --multi    le meme, sans la fenetre : image TELECHARGEE
#                                 (docker pull bastienbaranoff/norf_gsm, taguee
#                                 osmocom-nitb)
#   sudo ./addition.sh --multi-build
#                                 le meme, image COMPILEE sur place (build.sh,
#                                 plusieurs dizaines de minutes)
#   sudo ./addition.sh --docker   le moteur de conteneurs seul   (depannage)
#   sudo ./addition.sh --image    l image operateur seule, telechargee (depannage)
#   sudo ./addition.sh --build    l image operateur seule, compilee  (depannage)
#   sudo ./addition.sh --opencl   la pile OpenCL seule (calcul GPU)
#   sudo ./addition.sh --claude   Claude Code (CLI de l assistant) seul
#   sudo ./addition.sh --status   dit seulement ce qui est present
# =============================================================================
set -uo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
MULTI_CONF="${MULTI_CONF:-/etc/osmocom/osmo-multi.conf}"
# NOM DE L IMAGE : osmocom-run, PAS "osmo-operator".
# build.sh produit osmocom-nitb (Dockerfile, ~11 Go) puis Dockerfile.run en
# derive osmocom-run, et c est CELLE-LA que start.sh lance - verifie sur le
# banc : `docker inspect osmo-operator-1 --format {{.Config.Image}}` rend
# osmocom-run. Chercher "osmo-operator" (le nom du DEPOT, pas de l image)
# rendait la sonde toujours negative : "image absente" en permanence, et une
# recompilation Osmocom de 40 minutes relancee pour rien a chaque passage.
MULTI_IMAGE="${MULTI_IMAGE:-osmocom-run}"
# IMAGE DE BASE ET SON ORIGINE. osmocom-nitb est ce que build.sh COMPILE
# (~40 min) ; la meme pile est publiee sur le hub sous bastienbaranoff/norf_gsm.
# Par defaut on la TIRE et on la tague osmocom-nitb : start.sh en derive ensuite
# osmocom-run via Dockerfile.run exactement comme apres un build local.
# Les deux chemins sont EXCLUSIFS et choisis explicitement (fenetre ou
# drapeau) : pas de repli silencieux de l un vers l autre.
BASE_IMAGE="${BASE_IMAGE:-osmocom-nitb}"
HUB_IMAGE="${HUB_IMAGE:-bastienbaranoff/norf_gsm}"

# ── _trust_desktop : rendre un raccourci du bureau VISIBLE par DING ──────────
# DING n affiche un .desktop avec son nom et son icone que s il est executable,
# possede par l utilisateur du bureau, ET porteur de metadata::trusted. Cet
# attribut ne vit PAS dans le fichier : il est ecrit dans les metadonnees gvfs
# de la SESSION, donc gio doit parler au bus de CET utilisateur.
#
# LE PIEGE QUI RENDAIT L ICONE INVISIBLE. addition.sh est lance par
# `pkexec env DISPLAY=... XAUTHORITY=... ` (osmo-addition-anim) : pkexec NETTOIE
# l environnement et ne transmet ni XDG_RUNTIME_DIR ni DBUS_SESSION_BUS_ADDRESS.
# `gio set metadata::trusted` n avait alors aucun bus a joindre, echouait en
# silence (2>/dev/null), et le fichier restait pose mais jamais approuve - donc
# absent du bureau. On reconstruit l env a partir de l UID proprietaire du
# bureau et on lance gio SOUS cet utilisateur.
_trust_desktop() {
    local f="$1" home="$2" pos="${3:-}" owner uid bus
    [ -f "$f" ] || return 1
    owner="$(stat -c '%U' "$home" 2>/dev/null)"; [ -n "$owner" ] || owner=root
    uid="$(id -u "$owner" 2>/dev/null)" || return 1
    chown "$owner" "$f" 2>/dev/null || true
    chmod +x "$f" 2>/dev/null || true
    bus="/run/user/$uid/bus"
    # Session active pour ce proprietaire : on pose les attributs TOUT DE SUITE.
    # Sinon (pas de bus), osmo-trust-desktop (autostart) posera trusted au
    # prochain login - l icone apparait alors sans intervention.
    # pos "x,y" (optionnel) : place l icone sur le bureau via DING.
    if [ -S "$bus" ] && command -v runuser >/dev/null 2>&1; then
        runuser -u "$owner" -- env XDG_RUNTIME_DIR="/run/user/$uid" \
            DBUS_SESSION_BUS_ADDRESS="unix:path=$bus" \
            gio set -t string "$f" metadata::trusted true 2>/dev/null || true
        [ -n "$pos" ] && runuser -u "$owner" -- env XDG_RUNTIME_DIR="/run/user/$uid" \
            DBUS_SESSION_BUS_ADDRESS="unix:path=$bus" \
            gio set -t string "$f" metadata::nautilus-icon-position "$pos" 2>/dev/null || true
    fi
    return 0
}

DO_DOCKER=0; DO_IMAGE=0; DO_MULTI=0; DO_OPENCL=0; DO_CLAUDE=0; STATUS_ONLY=0; ANY_FLAG=0
DO_BUILD=0
for a in "$@"; do
    case "$a" in
        --docker) DO_DOCKER=1; ANY_FLAG=1 ;;
        --image)  DO_IMAGE=1;  ANY_FLAG=1 ;;
        --build)  DO_IMAGE=1;  DO_BUILD=1; ANY_FLAG=1 ;;
        --multi)  DO_MULTI=1;  ANY_FLAG=1 ;;
        --multi-build) DO_MULTI=1; DO_BUILD=1; ANY_FLAG=1 ;;
        --opencl) DO_OPENCL=1; ANY_FLAG=1 ;;
        --claude) DO_CLAUDE=1; ANY_FLAG=1 ;;
        --all)    DO_DOCKER=1; DO_IMAGE=1; DO_MULTI=1; DO_OPENCL=1; DO_CLAUDE=1; ANY_FLAG=1 ;;
        --status) STATUS_ONLY=1; ANY_FLAG=1 ;;
        -h|--help) sed -n '2,35p' "$0"; exit 0 ;;
    esac
done

echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║   osmo-operator - supplements (hors ISO)             ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"

etat() {
    local ok_d=0 ok_r=0 ok_i=0 ok_m=0
    command -v docker >/dev/null 2>&1 && ok_d=1
    [ "$ok_d" = "1" ] && docker info >/dev/null 2>&1 && ok_r=1
    [ "$ok_r" = "1" ] && docker image inspect "$MULTI_IMAGE" >/dev/null 2>&1 && ok_i=1
    [ -f "$MULTI_CONF" ] && ok_m=1
    local ok_cl=0
    # clinfo qui rend au moins une plateforme : c est la seule preuve qu une
    # pile OpenCL est UTILISABLE. Les paquets poses ne prouvent rien - un ICD
    # sans pilote derriere laisse "Number of platforms 0".
    command -v clinfo >/dev/null 2>&1 \
        && [ "$(clinfo -l 2>/dev/null | grep -c Platform)" -gt 0 ] && ok_cl=1
    local ok_cc=0
    # claude qui repond --version : la seule preuve qu il est UTILISABLE. Le
    # binaire pose sans node derriere (installation npm cassee) sort en erreur.
    command -v claude >/dev/null 2>&1 \
        && claude --version >/dev/null 2>&1 && ok_cc=1
    [ "$ok_d" = "1" ] && echo -e "  docker installe      : ${GREEN}oui${NC}"      || echo -e "  docker installe      : ${YELLOW}non${NC}"
    [ "$ok_r" = "1" ] && echo -e "  demon actif          : ${GREEN}oui${NC}"      || echo -e "  demon actif          : ${YELLOW}non${NC}"
    [ "$ok_i" = "1" ] && echo -e "  image operateur      : ${GREEN}presente${NC}" || echo -e "  image operateur      : ${YELLOW}absente${NC}"
    [ "$ok_m" = "1" ] && echo -e "  topologie SS7        : ${GREEN}posee${NC}"    || echo -e "  topologie SS7        : ${YELLOW}absente${NC}"
    [ "$ok_cl" = "1" ] && echo -e "  OpenCL               : ${GREEN}operationnel${NC}" || echo -e "  OpenCL               : ${YELLOW}absent${NC}"
    [ "$ok_cc" = "1" ] && echo -e "  Claude Code          : ${GREEN}operationnel${NC}" || echo -e "  Claude Code          : ${YELLOW}absent${NC}"
}

[ "$STATUS_ONLY" = "1" ] && { etat; exit 0; }

# ── LE CHOIX ────────────────────────────────────────────────────────────────
# Lancee par l icone, cette fenetre est la SEULE interface : sans elle, un
# double-clic partait droit sur un apt-get de plusieurs centaines de Mo et une
# compilation Osmocom, sans rien demander.
#
# [2026-08-31] UN SEUL CHOIX, ET C EST VOULU.
# La liste a cocher en offrait trois - "multi-operateur SS7", "docker", "image
# operateur" - alors qu il n y a jamais eu qu un scenario : le multi-operateur,
# qui ENTRAINE les deux autres (l implication, juste en dessous, remettait
# DO_DOCKER=1 et DO_IMAGE=1 des que la premiere ligne etait cochee). Les deux
# dernieres cases n etaient donc decochables qu en apparence : les decocher ne
# changeait rien tant que la premiere restait cochee, et les cocher seules
# donnait un demi-supplement dont rien ne se sert - docker sans image, ou une
# image de 11 Go sans la topologie qui l emploie.
# Une fenetre qui propose des choix sans effet ment sur ce que fait le script.
#
# [2026-09-02] DEUX CHOIX MAINTENANT - ET LA REGLE N A PAS CHANGE.
# Ce qui etait reproche aux trois cases d avant, ce n est pas leur nombre :
# c est qu elles etaient FAUSSES (cocher "docker" seul ne donnait rien
# d utilisable, et le decocher ne l empechait pas d etre installe). OpenCL,
# lui, est un supplement REELLEMENT independant : il n entraine rien, rien ne
# l entraine, on peut le vouloir sans le multi-operateur et l inverse. Une case
# qui commande quelque chose a le droit d exister ; une case decorative, non.
# Les drapeaux --docker / --image / --opencl restent pour le depannage console.
if [ "$ANY_FLAG" = "0" ]; then
    if command -v zenity >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
        # --print-column=2 N EST PAS FACULTATIF : le defaut de zenity est la
        # colonne 1, qui pour un --checklist est la CASE A COCHER - on
        # recupererait une liste de "TRUE" au lieu des choix. On imprime donc
        # la colonne 2, cachee, qui porte la CLE ("multi", "opencl") : le
        # libelle affiche peut alors changer sans casser les tests.
        # --separator : le defaut "|" traverse mal les tests ci-dessous ; on
        # prend un caractere qui n apparait dans aucun libelle.
        _choix=$(zenity --list --checklist --width=680 --height=340 \
            --title="osmo-operator - supplements" \
            --text="Supplements qui ne sont PAS dans l ISO.\nUne connexion Internet est necessaire." \
            --ok-label="Installer" --cancel-label="Fermer" \
            --separator="~" \
            --column="" --column="cle" --column="Supplement" \
            --hide-column=2 --print-column=2 \
            TRUE  multi  "Docker container and SS7 multioperator - docker.io, l image operateur (telechargee du hub OU compilee sur place : le choix vient ensuite), et la topologie op1 natif + op2/op3 docker + inter-STP" \
            FALSE opencl "OpenCL (calcul GPU) - runtime ICD, clinfo, le pilote de la carte detectee (Intel / Mesa-AMD, pocl en repli), et les outils deka / a51_tools / dst80_reversing / tea1-cracker clones dans /root" \
            FALSE claude "Claude Code (CLI de l assistant IA) - installeur natif claude.ai/install.sh (binaire autonome, sans npm) ; lance ensuite avec la commande claude" \
            2>/dev/null) || { echo "Annule."; exit 0; }
        [ -n "$_choix" ] || { echo "Rien de selectionne."; exit 0; }
        case "$_choix" in *multi*)  DO_MULTI=1  ;; esac
        # L ORIGINE DE L IMAGE : une seconde fenetre, en BOUTONS RADIO.
        # Telecharger et compiler sont exclusifs par construction - un radiolist
        # ne rend qu une valeur, il n y a pas de "les deux" a rattraper apres
        # coup comme avec des cases a cocher.
        if [ "$DO_MULTI" = "1" ]; then
            _orig=$(zenity --list --radiolist --width=680 --height=260 \
                --title="osmo-operator - image operateur" \
                --text="D ou vient l image operateur (osmocom-nitb, ~11 Go) ?" \
                --ok-label="Continuer" --cancel-label="Fermer" \
                --column="" --column="cle" --column="Origine" \
                --hide-column=2 --print-column=2 \
                TRUE  dl    "TELECHARGER - docker pull ${HUB_IMAGE} (quelques minutes selon le debit)" \
                FALSE build "COMPILER sur place - build.sh, compilation Osmocom (plusieurs dizaines de minutes)" \
                2>/dev/null) || { echo "Annule."; exit 0; }
            case "$_orig" in
                build) DO_BUILD=1 ;;
                dl)    DO_BUILD=0 ;;
                *)     echo "Aucune origine choisie."; exit 0 ;;
            esac
        fi
        case "$_choix" in *opencl*) DO_OPENCL=1 ;; esac
        case "$_choix" in *claude*) DO_CLAUDE=1 ;; esac
    else
        # Console sans zenity : le supplement historique, celui de l icone.
        DO_MULTI=1
    fi
fi

# Le multi-operateur ENTRAINE ses dependances : sans moteur ni image, la
# topologie posee ne servirait a rien et start-multi.sh renverrait ici.
if [ "$DO_MULTI" = "1" ]; then DO_DOCKER=1; DO_IMAGE=1; fi

[ "$(id -u)" -eq 0 ] || { echo -e "${RED}Root requis : sudo $0${NC}"; exit 1; }

# ── OPENCL ──────────────────────────────────────────────────────────────────
# Supplement independant : rien ici n entraine docker, et docker n entraine pas
# ceci. Il sert au calcul GPU - les traitements lourds du cote SDR (correlation,
# FFT larges) et tout ce qu un utilisateur voudra faire tourner a cote.
#
# UNE PILE OPENCL, C EST DEUX MOITIES, ET L ERREUR CLASSIQUE EST DE N EN POSER
# QU UNE. Le "runtime" (ocl-icd) n est qu un aiguilleur : il lit
# /etc/OpenCL/vendors/*.icd et charge la bibliotheque du VENDEUR qui y est
# nommee. Sans ICD de vendeur, tout s installe sans une erreur et clinfo rend
#     Number of platforms   0
# ce qui ressemble a un materiel non supporte alors qu il ne manque qu un
# paquet. On pose donc l aiguilleur ET le pilote de la carte reellement
# presente, lue dans lspci.
#
# pocl est pose dans tous les cas : c est une implementation PROCESSEUR. Elle
# ne remplace pas un GPU, mais elle garantit qu un programme OpenCL trouve
# toujours une plateforme - y compris en machine virtuelle, ou dans un banc
# sans carte graphique dediee, ou l absence totale de plateforme fait echouer
# le code avec un CL_PLATFORM_NOT_FOUND_KHR que personne ne sait lire.
#
# NVIDIA est le cas a part : son ICD OpenCL n est pas un paquet separe
# installable seul, il voyage avec le pilote proprietaire (le paquet
# libnvidia-compute-xxx, tire par nvidia-driver-xxx). On l installe donc via
# ubuntu-drivers, qui choisit le pilote recommande pour la carte. C est un
# pilote graphique proprietaire, sur une machine dont l ecran depend : il faut
# le plus souvent REDEMARRER pour qu il prenne - on le dit clairement.
if [ "$DO_OPENCL" = "1" ]; then
    echo -e "  ${CYAN}→${NC} installation de la pile OpenCL ..."
    export DEBIAN_FRONTEND=noninteractive

    # lvm2 : PAS de l OpenCL, mais deka (installe avec ce supplement) monte ses
    # tables sur des volumes LVM - sans vgchange, deka-start.sh ne monte rien.
    # On le pose donc ici, avec deka.
    # swig + python3-dev : compilation des modules natifs de deka (_delta.so,
    # _libvankus.so) plus bas. build-essential est deja tire ailleurs.
    _cl_pkgs="ocl-icd-libopencl1 ocl-icd-opencl-dev opencl-headers clinfo pocl-opencl-icd lvm2 swig python3-dev"
    _vga="$(lspci 2>/dev/null | grep -iE 'vga|3d controller|display' || true)"
    _nvidia=0
    case "$_vga" in
        *[Ii]ntel*)   _cl_pkgs="$_cl_pkgs intel-opencl-icd" ;;
    esac
    case "$_vga" in
        *AMD*|*ATI*|*[Rr]adeon*) _cl_pkgs="$_cl_pkgs mesa-opencl-icd" ;;
    esac
    case "$_vga" in
        *NVIDIA*|*nVidia*) _nvidia=1 ;;
    esac
    echo -e "      paquets : ${BOLD}${_cl_pkgs}${NC}"

    apt-get update || { echo -e "  ${RED}✗ apt-get update a echoue - pas de reseau ?${NC}"
                        echo -e "    Ce supplement a BESOIN d Internet : rien n est pre-telecharge dans l ISO."
                        exit 1; }
    # Paquet par paquet, et non fatal : un ICD de vendeur absent du miroir
    # (cela arrive) ne doit pas emporter le runtime avec lui - la pile
    # processeur reste utilisable, et c est mieux que rien du tout.
    for _p in $_cl_pkgs; do
        apt-get install -y "$_p" >/dev/null 2>&1 \
            && echo -e "      ${GREEN}✓${NC} $_p" \
            || echo -e "      ${YELLOW}!${NC} $_p non installe (absent du miroir ?)"
    done

    # pyopencl : le binding Python d OpenCL, POSE DANS LE VENV /root/.env - le
    # meme que start-clean.sh (qosmo-grgsm) et le profil de root activent, et ou
    # vit deja gr-gsm. On l installe avec le pip DU VENV (/root/.env/bin/python3)
    # et pas le pip systeme : sinon le module ne serait pas visible du code qui
    # tourne sous ce venv. opencl-headers + ocl-icd-opencl-dev (poses au-dessus)
    # fournissent les en-tetes et libOpenCL.so dont pyopencl a besoin. Non fatal.
    if [ -x /root/.env/bin/python3 ]; then
        echo -e "  ${CYAN}→${NC} pyopencl dans le venv ${BOLD}/root/.env${NC} ..."
        if /root/.env/bin/python3 -m pip install --no-cache-dir --disable-pip-version-check pyopencl >/dev/null 2>&1; then
            echo -e "      ${GREEN}✓${NC} pyopencl"
        else
            echo -e "      ${YELLOW}!${NC} pyopencl non installe (voir : /root/.env/bin/python3 -m pip install pyopencl)"
        fi
    else
        echo -e "  ${YELLOW}!${NC} venv /root/.env absent - pyopencl non pose (attendu sur l ISO)"
    fi

    # LA SEULE VERIFICATION QUI VAUT : est-ce qu une plateforme repond ?
    if command -v clinfo >/dev/null 2>&1; then
        _np="$(clinfo -l 2>/dev/null | grep -c Platform || true)"
        if [ "${_np:-0}" -gt 0 ]; then
            echo -e "  ${GREEN}✓${NC} OpenCL operationnel - ${BOLD}${_np}${NC} plateforme(s) :"
            clinfo -l 2>/dev/null | sed 's/^/      /'
        else
            echo -e "  ${YELLOW}!${NC} paquets poses, mais aucune plateforme OpenCL ne repond."
            echo -e "      Voir : ${BOLD}ls /etc/OpenCL/vendors/${NC} puis ${BOLD}clinfo${NC}"
        fi
    fi

    if [ "$_nvidia" = "1" ]; then
        echo -e "  ${CYAN}→${NC} carte ${BOLD}NVIDIA${NC} detectee : installation du pilote (ICD OpenCL inclus) ..."
        # LES HEADERS DU NOYAU D ABORD. Le pilote NVIDIA compile son module par
        # DKMS contre le noyau EN COURS : sans linux-headers-$(uname -r), dkms
        # et build-essential, le build du module echoue et l installation part
        # en erreur (souvent muette : le paquet s installe, mais nvidia.ko
        # n existe pas, et OpenCL reste absent). On les pose avant.
        _kver="$(uname -r)"
        echo -e "      ${CYAN}→${NC} pre-requis DKMS : linux-headers-${_kver}, dkms, build-essential"
        apt-get install -y "linux-headers-${_kver}" dkms build-essential >/dev/null 2>&1 \
            || echo -e "      ${YELLOW}!${NC} headers/dkms : installation partielle (headers absents du miroir ?)"
        # linux-headers-generic en filet : garde les headers alignes sur le
        # meta-noyau pour les mises a jour de noyau suivantes.
        apt-get install -y linux-headers-generic >/dev/null 2>&1 || true
        apt-get install -y ubuntu-drivers-common >/dev/null 2>&1 || true
        if command -v ubuntu-drivers >/dev/null 2>&1; then
            if ubuntu-drivers install; then
                echo -e "  ${GREEN}✓${NC} pilote NVIDIA installe"
            else
                echo -e "  ${YELLOW}!${NC} ubuntu-drivers install a echoue - repli : ${BOLD}nvidia-driver + nvidia-opencl-icd${NC}"
                apt-get install -y  nvidia-driver-610 nvidia-opencl-icd >/dev/null 2>&1 \
                    || echo -e "  ${YELLOW}!${NC} pilote NVIDIA non installe (voir : ubuntu-drivers devices)"
            fi
        else
            echo -e "  ${YELLOW}!${NC} ubuntu-drivers absent - installez a la main : ${BOLD}sudo ubuntu-drivers install${NC}"
        fi
        echo -e "  ${YELLOW}!${NC} un ${BOLD}redemarrage${NC} est generalement necessaire pour que le pilote"
        echo -e "      NVIDIA (et donc son OpenCL) soit actif - ${BOLD}clinfo${NC} le confirmera ensuite."
    fi

    # ── LES OUTILS QUI TOURNENT SUR OPENCL ───────────────────────────────────
    # deka, a51_tools, dst80_reversing, tea1-cracker : quatre depots de calcul
    # qui se servent du GPU. Ils VONT AVEC OpenCL - c est pour eux qu on le
    # pose - donc ils s installent ici, avec lui, et pas dans l ISO : l image
    # reste un noeud GSM, ces outils sont un supplement de calcul.
    # Clones dans /root. Idempotent : si le depot est deja la, on met a jour
    # (git pull) au lieu de recloner. Non fatal - un depot injoignable (prive,
    # reseau) n arrete pas le reste.
    echo -e "  ${CYAN}→${NC} outils OpenCL (deka, a51_tools, dst80_reversing, tea1-cracker) dans /root ..."
    # NON-INTERACTIF, SINON LA FENETRE FIGE. Sur un depot prive ou absent, git
    # reclame un identifiant sur le terminal et attend INDEFINIMENT - lance par
    # l icone, il n y a personne pour repondre. GIT_TERMINAL_PROMPT=0 le fait
    # echouer net (pas de prompt), et le message plus bas dit quoi faire.
    export GIT_TERMINAL_PROMPT=0
    for _rt in "deka=https://github.com/bbaranoff/deka" \
               "a51_tools=https://github.com/bbaranoff/a51_tools" \
               "dst80_reversing=https://github.com/bbaranoff/dst80_reversing" \
               "tea1-cracker=https://github.com/bbaranoff/tea1-cracker"; do
        _name="${_rt%%=*}"; _url="${_rt#*=}"; _dst="/root/$_name"
        # http.version=HTTP/1.1 : "expected flush after ref listing" vient d un
        # flux git corrompu par HTTP/2 (proxy/antivirus qui s intercale) - le
        # symptome depend de la taille, d ou certains depots qui passent et
        # d autres non. Forcer HTTP/1.1 est le remede de cette erreur exacte.
        # On CAPTURE la sortie de git et on la MONTRE en cas d echec.
        GIT="git -c http.version=HTTP/1.1"
        if [ -d "$_dst/.git" ]; then
            if _err=$($GIT -C "$_dst" pull --ff-only 2>&1); then
                echo -e "      ${GREEN}✓${NC} $_name a jour ($(git -C "$_dst" log -1 --format='%h'))"
            else
                echo -e "      ${YELLOW}!${NC} $_name : git pull a echoue - $(echo "$_err" | tail -1)"
            fi
        elif _err=$($GIT clone --depth 1 "$_url" "$_dst" 2>&1); then
            echo -e "      ${GREEN}✓${NC} $_name clone ($(git -C "$_dst" log -1 --format='%h'))"
        else
            echo -e "      ${YELLOW}!${NC} $_name : clone impossible - $_url"
            echo -e "        ${YELLOW}git:${NC} $(echo "$_err" | tail -1)"
        fi
    done

    # ── deka : compilation des modules natifs (make) ─────────────────────────
    # deka importe _delta.so et _libvankus.so, produits par SWIG + gcc depuis
    # delta.c / libvankus.c (voir son Makefile). Sans ce make, les workers
    # importent des modules absents et deka ne calcule rien. Pre-requis : swig
    # et les entetes python3. Non fatal : un echec le dit, sans stopper le reste.
    if [ -f /root/deka/Makefile ]; then
        echo -e "  ${CYAN}→${NC} deka : compilation des modules natifs (make) ..."
        apt-get install -y --no-install-recommends swig python3-dev build-essential >/dev/null 2>&1 || true
        if ( cd /root/deka && make >/dev/null 2>&1 ) \
           && [ -e /root/deka/_delta.so ] && [ -e /root/deka/_libvankus.so ]; then
            echo -e "      ${GREEN}✓${NC} _delta.so + _libvankus.so construits"
        else
            echo -e "      ${YELLOW}!${NC} make deka a echoue (swig / python3-dev ?) - voir : cd /root/deka && make"
        fi
    fi

    # ── deka : une ICONE d appli (pas un service) ────────────────────────────
    # deka monte des tables et lance des workers - ca ne doit PAS partir a
    # chaque boot dans le dos de l utilisateur (le montage suppose le groupe de
    # volumes present). On pose donc une icone : l utilisateur la clique quand
    # il veut demarrer deka. deka-start.sh reste le moteur ; l icone l appelle
    # avec les droits root (pkexec) dans un terminal, pour voir la sortie.
    if [ -d /root/deka ]; then
        echo -e "  ${CYAN}→${NC} deka : pose de l icone d application ..."
        # deka-start.sh vit dans le depot : s il est la (clone a jour), on n y
        # touche pas. On ne l ecrit qu en SECOURS, pour un clone qui ne l aurait
        # pas encore.
        if [ ! -f /root/deka/deka-start.sh ]; then
        cat > /root/deka/deka-start.sh <<'DEKASTART'
#!/bin/bash
# deka-start.sh - montage des tables deka et lancement des workers.
# Pose par addition.sh (supplement OpenCL). Lance par l icone deka (pkexec), ou
# a la main : sudo /root/deka/deka-start.sh
#   1. vgchange -ay : active le groupe de volumes (sinon /dev/tables/* absent).
#   2. les 4 points de montage, puis les 4 volumes (1_10..31_40).
#   3. les 3 workers python, depuis /root/deka (delta.pyc y vit).
set -u
DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
LOG=/var/log/deka.log
# Sortie a l ECRAN + copie dans le log (tee) : on veut VOIR les montages et les
# PID des workers, pas les chercher dans un fichier.
exec > >(tee -a "$LOG") 2>&1
echo "=== $(date -Is) deka-start ==="
[ "$(id -u)" -eq 0 ] || { echo "root requis : sudo $0"; exit 1; }
PY="$(command -v python3.7 || command -v python3)"
[ -n "$PY" ] || { echo "aucun python3 trouve"; exit 1; }
vgchange -ay || echo "ATTENTION: vgchange -ay a echoue (groupe absent ?)"
mkdir -p /mnt1 /mnt2 /mnt3
monter() {
    local dev="$1" pt="$2"
    if mountpoint -q "$pt"; then echo "deja monte: $pt"; return 0; fi
    if [ ! -e "$dev" ]; then echo "ABSENT: $dev - non monte sur $pt"; return 1; fi
    mount "$dev" "$pt" && echo "monte: $dev -> $pt" || echo "ECHEC montage: $dev -> $pt"
}
monter /dev/tables/1_10  /mnt
monter /dev/tables/11_20 /mnt1
monter /dev/tables/21_30 /mnt2
monter /dev/tables/31_40 /mnt3
cd "$DIR" || exit 1
for w in paplon.py oclvankus.py delta_client.py; do
    [ -f "$w" ] || { echo "worker absent: $w"; continue; }
    echo "lancement: $PY $DIR/$w"
    "$PY" "$DIR/$w" >>"/var/log/deka-${w%.py}.log" 2>&1 &
    echo "  pid $!"
done
echo "deka-start termine."
DEKASTART
        chmod 755 /root/deka/deka-start.sh
        fi

        # LE LANCEUR : deka-start.sh a besoin de root (vgchange, mount). Depuis
        # une icone on n est pas root : pkexec eleve, et un terminal reste
        # ouvert pour lire la sortie (montages, PID des workers). Meme facture
        # que osmo-addition-anim.
        cat > /usr/local/bin/osmo-deka-anim <<'DEKAGUI'
#!/bin/bash
set -u
SCRIPT=/root/deka/deka-start.sh
if [ ! -x "$SCRIPT" ]; then
    command -v zenity >/dev/null 2>&1 && \
        zenity --error --text="deka-start.sh introuvable : $SCRIPT" 2>/dev/null
    exit 1
fi
RUNNER="$SCRIPT"
if [ "$(id -u)" -ne 0 ]; then
    if command -v pkexec >/dev/null 2>&1; then
        RUNNER="pkexec env DISPLAY=${DISPLAY:-} XAUTHORITY=${XAUTHORITY:-} $SCRIPT"
    else
        RUNNER="sudo -E $SCRIPT"
    fi
fi
CMD="$RUNNER; echo; read -n1 -rsp 'deka lance - une touche pour fermer...'"
for term in x-terminal-emulator gnome-terminal xterm; do
    command -v "$term" >/dev/null 2>&1 || continue
    case "$term" in
        gnome-terminal) exec "$term" --title="deka" -- bash -c "$CMD" ;;
        *)              exec "$term" -T "deka" -e bash -c "$CMD" ;;
    esac
done
exec bash -c "$RUNNER"
DEKAGUI
        chmod 755 /usr/local/bin/osmo-deka-anim

        # ICONE DEDIEE : la mandala de connecteurs (data/deka.svg, facon BRMLAB),
        # posee en CHEMIN ABSOLU dans Icon= - un nom de theme arrive apres
        # icon-theme.cache s afficherait en page blanche (cf. update.sh).
        install -d /usr/share/osmo-operator/icons /usr/share/icons/hicolor/scalable/apps
        if [ -f "$DIR/data/deka.svg" ]; then
            install -m644 "$DIR/data/deka.svg" /usr/share/osmo-operator/icons/deka.svg
            install -m644 "$DIR/data/deka.svg" /usr/share/icons/hicolor/scalable/apps/deka.svg
        fi
        _deka_desktop=/usr/share/applications/deka.desktop
        cat > "$_deka_desktop" <<'DEKADSK'
[Desktop Entry]
Type=Application
Name=deka
Name[fr]=deka
Comment=Monte les tables et lance les workers deka
Comment[fr]=Monte les tables et lance les workers deka
Exec=/usr/local/bin/osmo-deka-anim
Icon=/usr/share/osmo-operator/icons/deka.svg
Terminal=false
Categories=System;Utility;
Keywords=deka;tables;workers;opencl;
DEKADSK
        chmod 644 "$_deka_desktop"
        command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -f -q /usr/share/icons/hicolor 2>/dev/null || true
        update-desktop-database /usr/share/applications 2>/dev/null || true

        # Sur les bureaux : DING n affiche un .desktop avec son nom et son icone
        # que s il est executable ET porteur de metadata::trusted (voir la meme
        # mecanique dans update.sh). On le pose sous chaque bureau existant.
        _posee=0
        for _h in /root /home/*; do
            for _dir in Bureau Desktop; do
                [ -d "$_h/$_dir" ] || continue
                cp -f "$_deka_desktop" "$_h/$_dir/deka.desktop" 2>/dev/null || continue
                _trust_desktop "$_h/$_dir/deka.desktop" "$_h" "0,0"
                touch "$_h/$_dir" 2>/dev/null || true
                _posee=1
            done
        done
        [ "$_posee" = "1" ] \
            && echo -e "  ${GREEN}✓${NC} icone ${BOLD}deka${NC} posee sur le bureau (clic pour monter + lancer)" \
            || echo -e "  ${GREEN}✓${NC} icone ${BOLD}deka${NC} dans le menu des applications"
    fi
fi

# ── CLAUDE CODE ──────────────────────────────────────────────────────────────
# Supplement independant : Claude Code, la CLI de l assistant. Rien ici
# n entraine docker ni OpenCL, et rien ne l entraine - on peut le vouloir seul.
#
# ON PREND L INSTALLEUR NATIF (claude.ai/install.sh), PAS npm, ET C EST VOULU.
# @anthropic-ai/claude-code par npm exige node >= 18 ; or le node des depots
# Ubuntu 22.04 est une v12, trop vieille - l installation npm casserait, ou
# reclamerait un depot tiers (NodeSource) qu on evite ici comme on evite
# get.docker.com plus bas. L installeur natif pose un binaire autonome (node
# embarque), sans toucher au systeme : c est le chemin le plus sur.
#
# Il pose dans ~/.local/bin (donc /root/.local/bin, lance en sudo). On lie ce
# binaire dans /usr/local/bin pour qu il soit sur le PATH de tout le monde et
# que la sonde `command -v claude` le voie.
if [ "$DO_CLAUDE" = "1" ]; then
    echo -e "  ${CYAN}→${NC} installation de Claude Code ..."
    export DEBIAN_FRONTEND=noninteractive

    # curl + certificats : l installeur les exige. Non fatal en soi, mais on
    # previent tot si le reseau manque - rien n est pre-telecharge dans l ISO.
    command -v curl >/dev/null 2>&1 || apt-get install -y curl ca-certificates >/dev/null 2>&1 || true
    if ! command -v curl >/dev/null 2>&1; then
        echo -e "  ${RED}✗ curl absent et non installable - pas de reseau ?${NC}"
        echo -e "    Ce supplement a BESOIN d Internet : rien n est pre-telecharge dans l ISO."
        exit 1
    fi

    # HOME explicite : lance par pkexec/sudo, $HOME peut manquer ou pointer
    # ailleurs ; l installeur pose dans $HOME/.local/bin, on veut savoir ou.
    _cc_home="${HOME:-/root}"
    if _out=$(HOME="$_cc_home" bash -c 'curl -fsSL https://claude.ai/install.sh | bash' 2>&1); then
        echo -e "      ${GREEN}✓${NC} installeur natif termine"
    else
        echo -e "      ${YELLOW}!${NC} installeur natif en echec - $(echo "$_out" | tail -1)"
    fi

    # Trouver le binaire pose et le lier sur le PATH global. On regarde les
    # emplacements connus de l installeur (~/.local/bin, puis repli /root).
    _cc_bin=""
    for _c in "$_cc_home/.local/bin/claude" /root/.local/bin/claude "$_cc_home/.claude/bin/claude"; do
        [ -x "$_c" ] && { _cc_bin="$_c"; break; }
    done
    if [ -n "$_cc_bin" ] && [ ! -e /usr/local/bin/claude ]; then
        ln -sf "$_cc_bin" /usr/local/bin/claude
        echo -e "      ${GREEN}✓${NC} lien /usr/local/bin/claude -> $_cc_bin"
    fi

    # LA SEULE VERIFICATION QUI VAUT : est-ce que claude repond ?
    if command -v claude >/dev/null 2>&1 && _ver=$(claude --version 2>/dev/null); then
        echo -e "  ${GREEN}✓${NC} Claude Code operationnel - ${BOLD}${_ver}${NC}"
        echo -e "      lancez-le avec la commande ${BOLD}claude${NC}"
    else
        echo -e "  ${YELLOW}!${NC} Claude Code pose, mais ne repond pas encore."
        echo -e "      Ouvrez un nouveau terminal (PATH), ou lancez ${BOLD}$_cc_home/.local/bin/claude${NC}"
    fi

    # ── ICONE CLAUDE : tuile clay + sunburst Anthropic (data/claude.svg) ──────
    # Lanceur osmo-claude-anim : ouvre un terminal qui lance claude ; s il n est
    # pas installe, il enchaine ce meme supplement (--claude) puis le lance.
    install -d /usr/share/osmo-operator/icons /usr/share/icons/hicolor/scalable/apps
    if [ -f "$DIR/data/claude.svg" ]; then
        install -m644 "$DIR/data/claude.svg" /usr/share/osmo-operator/icons/claude.svg
        install -m644 "$DIR/data/claude.svg" /usr/share/icons/hicolor/scalable/apps/claude.svg
    fi
    cat > /usr/local/bin/osmo-claude-anim <<'CLA'
#!/bin/bash
set -u
if ! command -v claude >/dev/null 2>&1; then
    ADD=/opt/GSM/osmo-operator/addition.sh
    if [ -x "$ADD" ]; then
        if [ "$(id -u)" -ne 0 ] && command -v pkexec >/dev/null 2>&1; then
            pkexec env DISPLAY="${DISPLAY:-}" XAUTHORITY="${XAUTHORITY:-}" "$ADD" --claude
        else
            "$ADD" --claude
        fi
    fi
fi
CMD='if command -v claude >/dev/null 2>&1; then claude; else echo "Claude non installe - lancez le supplement (--claude)."; fi; echo; read -n1 -rsp "Une touche pour fermer..."'
for term in x-terminal-emulator gnome-terminal xterm; do
    command -v "$term" >/dev/null 2>&1 || continue
    case "$term" in
        gnome-terminal) exec "$term" --title="Claude" -- bash -lc "$CMD" ;;
        *)              exec "$term" -T "Claude" -e bash -lc "$CMD" ;;
    esac
done
exec bash -lc "$CMD"
CLA
    chmod 755 /usr/local/bin/osmo-claude-anim
    if [ -f "$DIR/data/desktop/claude.desktop" ]; then
        install -m644 "$DIR/data/desktop/claude.desktop" /usr/share/applications/claude.desktop
        sed -i "s|^Icon=.*|Icon=/usr/share/osmo-operator/icons/claude.svg|" /usr/share/applications/claude.desktop
        command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -f -q /usr/share/icons/hicolor 2>/dev/null || true
        update-desktop-database /usr/share/applications 2>/dev/null || true
        _homes=(/root)
        if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
            _uh="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
            [ -n "$_uh" ] && [ -d "$_uh" ] && _homes+=("$_uh")
        fi
        _cposee=0
        for _h in "${_homes[@]}"; do
            for _dir in Bureau Desktop; do
                [ -d "$_h/$_dir" ] || continue
                cp -f /usr/share/applications/claude.desktop "$_h/$_dir/claude.desktop" 2>/dev/null || continue
                _trust_desktop "$_h/$_dir/claude.desktop" "$_h" "110,0"
                touch "$_h/$_dir" 2>/dev/null || true
                _cposee=1
            done
        done
        [ "$_cposee" = "1" ] \
            && echo -e "  ${GREEN}✓${NC} icone ${BOLD}Claude${NC} posee sur le bureau (en haut a gauche)" \
            || echo -e "  ${GREEN}✓${NC} icone ${BOLD}Claude${NC} dans le menu des applications"
    fi
fi

# ── DOCKER ──────────────────────────────────────────────────────────────────
# docker.io des depots Ubuntu, PAS le script get.docker.com : celui-ci ajoute un
# depot tiers et remplace containerd, ce qui sur une image figee se paie au
# premier apt-get upgrade. La version des depots suffit : on ne fait tourner
# que nos propres conteneurs.
if [ "$DO_DOCKER" = "1" ]; then
    if command -v docker >/dev/null 2>&1; then
        echo -e "  ${GREEN}✓${NC} docker deja installe ($(docker --version 2>/dev/null | head -1))"
    else
        echo -e "  ${CYAN}→${NC} installation de docker.io ..."
        export DEBIAN_FRONTEND=noninteractive
        apt-get update || { echo -e "  ${RED}✗ apt-get update a echoue - pas de reseau ?${NC}"
                            echo -e "    Ce supplement a BESOIN d Internet : rien n est pre-telecharge dans l ISO."
                            exit 1; }
        apt-get install -y docker.io || { echo -e "  ${RED}✗ installation de docker.io echouee${NC}"; exit 1; }
        echo -e "  ${GREEN}✓${NC} docker.io installe"
    fi
    systemctl enable --now docker 2>/dev/null || true
    # Le socket met un instant a repondre apres un premier demarrage : sans
    # cette attente, le `docker info` suivant echoue et l on croit
    # l installation ratee.
    for _i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 1; done
    docker info >/dev/null 2>&1 \
        && echo -e "  ${GREEN}✓${NC} demon docker actif" \
        || { echo -e "  ${RED}✗ le demon docker ne repond pas${NC} - voir : systemctl status docker"; exit 1; }
fi

# ── IMAGE OPERATEUR ─────────────────────────────────────────────────────────
# Deux chemins pour obtenir osmocom-nitb :
#   1. docker pull ${HUB_IMAGE} && docker tag ... osmocom-nitb  (defaut)
#      La pile Osmocom deja compilee, publiee sur le hub : le telechargement
#      remplace 40 minutes de compilation. Le tag donne au resultat le nom que
#      start.sh (Dockerfile.run : FROM osmocom-nitb) et start-nitb.sh attendent.
#   2. build.sh --no-cache  (--build / --multi-build / case "compiler")
#      --no-cache : le supplement rebatit l image PROPRE. Le cache docker
#      gardait des couches d une pile Osmocom a moitie compilee (ex. le clone
#      github casse de libosmocore) et les rejouait a l identique - un build
#      deja casse restait casse a chaque passage. On force la reconstruction.
_build_image() {
    [ -x "$DIR/build.sh" ] || { echo -e "  ${RED}✗ build.sh introuvable dans $DIR${NC}"; return 1; }
    echo -e "  ${CYAN}→${NC} construction de l image : ${BOLD}${DIR}/build.sh --no-cache${NC}"
    echo -e "      compilation Osmocom - comptez plusieurs dizaines de minutes."
    "$DIR/build.sh" --no-cache || { echo -e "  ${RED}✗ ${DIR}/build.sh a echoue${NC}"; return 1; }
    echo -e "  ${GREEN}✓${NC} image operateur construite (${BASE_IMAGE})"
}
_pull_image() {
    echo -e "  ${CYAN}→${NC} telechargement de l image : ${BOLD}docker pull ${HUB_IMAGE}${NC}"
    echo -e "      ~11 Go depuis le hub - selon le debit, quelques minutes a une heure."
    docker pull "$HUB_IMAGE" || { echo -e "  ${RED}✗ docker pull ${HUB_IMAGE} a echoue${NC} - reseau, ou image absente du hub"
                                  echo -e "    Alternative : compiler sur place avec ${BOLD}$0 --multi-build${NC}"; return 1; }
    docker tag "$HUB_IMAGE" "$BASE_IMAGE" || { echo -e "  ${RED}✗ docker tag ${HUB_IMAGE} ${BASE_IMAGE} a echoue${NC}"; return 1; }
    echo -e "  ${GREEN}✓${NC} image operateur tiree du hub et taguee ${BASE_IMAGE}"
}
# L IMAGE QUE LE MULTI-OPERATEUR LANCE EST osmocom-run, PAS osmocom-nitb.
# Ni le pull ni build.sh ne la produisent : c est Dockerfile.run (FROM
# osmocom-nitb) qui la derive, en quelques secondes. start-multi.sh sonde
# osmocom-run et renvoie ici si elle manque : sans cette derivation, un
# supplement "reussi" laissait le multi-operateur incapable de demarrer.
# Meme commande que start.sh (build_run_image, mode quick).
_derive_run_image() {
    echo -e "  ${CYAN}→${NC} derivation de l image d execution : ${BOLD}docker build -f Dockerfile.run -t ${MULTI_IMAGE}${NC}"
    ( cd "$DIR" && docker build --build-arg QEMU_CACHE_BUST=$(date +%s) \
                       -f Dockerfile.run -t "$MULTI_IMAGE" . ) \
        || { echo -e "  ${RED}✗ derivation de ${MULTI_IMAGE} echouee${NC}"; return 1; }
    echo -e "  ${GREEN}✓${NC} image d execution prete (${MULTI_IMAGE})"
}
if [ "$DO_IMAGE" = "1" ]; then
    if [ "$DO_BUILD" = "1" ]; then
        _build_image || exit 1
        _derive_run_image || exit 1
    elif docker image inspect "$MULTI_IMAGE" >/dev/null 2>&1; then
        echo -e "  ${GREEN}✓${NC} image operateur deja presente (${MULTI_IMAGE})"
    elif docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then
        echo -e "  ${GREEN}✓${NC} image de base deja presente (${BASE_IMAGE})"
        _derive_run_image || exit 1
    else
        _pull_image || exit 1
        _derive_run_image || exit 1
    fi
fi

# ── TOPOLOGIE MULTI-OPERATEUR ───────────────────────────────────────────────
# Ecrite ICI, une fois, et relue par start-multi.sh. Elle n est pas recalculee
# des deux cotes : deux formules jumelles finissent toujours par diverger, et le
# depot en porte deja la trace (start.sh et lib/gabarits.sh tiennent les MEMES
# fonctions en double).
#
# LE PLAN SS7, tel que start-interstp.sh le definit :
#     PC   = 1.<noeud><op>.<role>      role 1=MSC 2=STP 3=BSC
#     RCTX = noeud*1000 + op*100 + 50
# Sur une seule machine le noeud vaut 1, donc PC = 1.<op>.<role>.
#
#   op 1  NATIF   - PC 1.1.2, celui de start-direct.sh, laisse tel quel
#   op 2  DOCKER  - PC 1.2.2, backbone 172.20.0.12
#   op 3  DOCKER  - PC 1.3.2, backbone 172.20.0.13
#
# ⚠️ LES CONTENEURS COMMENCENT A 2, ET CELA SE PILOTE.
# start.sh numerotait ses conteneurs 1..N en dur : avec --operators 2 il creait
# osmo-operator-1 en point code 1.1.2, celui du natif. Deux equipements a la
# meme adresse SS7 - "pas un conflit de nom, du routage faux"
# (checks/wan_ss7_check.sh). OP_ID_BASE=2 (start.sh) fait demarrer les
# conteneurs au rang 2 et laisse le 1 au natif ; nom, backbone, point code,
# RCTX, segment prive et ports publies en decoulent tous.
# Le natif joint le hub par la passerelle du bridge : l hote atteint
# 172.20.0.10 directement, sans NAT ni route a ajouter.
if [ "$DO_MULTI" = "1" ]; then
    install -d "$(dirname "$MULTI_CONF")"
    cat > "$MULTI_CONF" <<CONF
# osmo-multi.conf - topologie multi-operateur, ecrite par addition.sh.
# Relue par start-multi.sh. Ne pas editer a la main sans relire les deux.
MULTI_NODE=1
MULTI_HUB_NAME=osmo-inter-stp
MULTI_HUB_IP=172.20.0.10
MULTI_HUB_PC=0.0.0
MULTI_M3UA_PORT=2908
MULTI_NET_NAME=osmo-ss7
MULTI_NET_SUBNET=172.20.0.0/24
MULTI_NET_GW=172.20.0.1
MULTI_IMAGE=osmocom-run
# operateurs : "index:mode:backbone_ip:point_code:rctx"
MULTI_OPS="1:native::1.1.2:150 2:docker:172.20.0.12:1.2.2:250 3:docker:172.20.0.13:1.3.2:350"
CONF
    echo -e "  ${GREEN}✓${NC} topologie SS7 ecrite : ${CYAN}${MULTI_CONF}${NC}"
    echo -e "      op1 ${BOLD}natif${NC} (1.1.2) + op2/op3 ${BOLD}docker${NC} (1.2.2 / 1.3.2) → hub 172.20.0.10"

    # ── L ICONE MULTI-OPERATOR : POSEE ICI, A LA FIN, ET PAS AVANT ────────────
    # L antenne bleue lance start-multi.sh, qui suppose docker, l image et la
    # topologie ci-dessus. La poser au build ISO (comme le telephone ou le
    # tutoriel) la faisait apparaitre sur le bureau des le premier boot, AVANT
    # que ce supplement n existe : un clic ne donnait rien. On la pose donc
    # seulement maintenant, une fois l install SS7 reellement faite - build-iso
    # et install_modules/80-bureau.sh ne la posent plus.
    _ico_dir=/usr/share/osmo-operator/icons
    _svg="$DIR/data/osmo-multi.svg"
    _dsk="$DIR/data/desktop/osmo-multi.desktop"
    if [ -f "$_dsk" ]; then
        install -d "$_ico_dir" /usr/share/icons/hicolor/scalable/apps /usr/share/applications
        # SVG en CHEMIN ABSOLU dans Icon= (voir build-iso.sh) : un nom de theme
        # arrive apres icon-theme.cache s affiche en page blanche.
        if [ -f "$_svg" ]; then
            install -m644 "$_svg" "$_ico_dir/osmo-multi.svg"
            install -m644 "$_svg" /usr/share/icons/hicolor/scalable/apps/osmo-multi.svg
        fi
        install -m644 "$_dsk" /usr/share/applications/osmo-multi.desktop
        # Exec= et Icon= suivent l arbre reel ($DIR), pas /opt/GSM en dur.
        sed -i -e "s|^Exec=/opt/GSM/osmo-operator/|Exec=$DIR/|" \
               -e "s|^Icon=.*|Icon=$_ico_dir/osmo-multi.svg|" \
               /usr/share/applications/osmo-multi.desktop

        # Sur les bureaux de root et de l utilisateur sudo, en Bureau/ et
        # Desktop/. metadata::trusted pour que DING l affiche sans pastille ;
        # osmo-trust-desktop (au login) la repositionne et la trust aussi.
        _homes=(/root)
        if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
            _uh="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
            [ -n "$_uh" ] && [ -d "$_uh" ] && _homes+=("$_uh")
        fi
        _posee=0
        for _h in "${_homes[@]}"; do
            for _dir in Bureau Desktop; do
                [ -d "$_h/$_dir" ] || continue
                cp -f /usr/share/applications/osmo-multi.desktop "$_h/$_dir/osmo-multi.desktop" 2>/dev/null || continue
                _trust_desktop "$_h/$_dir/osmo-multi.desktop" "$_h"
                touch "$_h/$_dir" 2>/dev/null || true
                _posee=1
            done
        done
        command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -f -q /usr/share/icons/hicolor 2>/dev/null || true
        command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database /usr/share/applications 2>/dev/null || true
        [ "$_posee" = "1" ] \
            && echo -e "  ${GREEN}✓${NC} icone ${BOLD}multi-operator${NC} (antenne) posee sur le bureau" \
            || echo -e "  ${GREEN}✓${NC} icone ${BOLD}multi-operator${NC} dans le menu des applications"
    fi
fi

echo
etat
echo
[ "$DO_MULTI" = "1" ] && \
    echo -e "  ${CYAN}→${NC} lancer : ${BOLD}sudo $DIR/start-multi.sh${NC}  (ou l antenne bleue du bureau)"
[ "$DO_OPENCL" = "1" ] && \
    echo -e "  ${CYAN}→${NC} verifier OpenCL : ${BOLD}clinfo${NC}"
exit 0

checks/annuaire.sh

.sh 202 lignes - 11KB

checks/annuaire.sh
#!/bin/bash
# =============================================================================
# checks/annuaire.sh -- l'annuaire du banc, confronte a ce que le reseau en sait
#
# Le banc ne rend que des nombres : 001020002000001, 20001, 1.21.2, rctx 2150.
# Chacun est juste, aucun ne se retient, et il faut trois fenetres pour savoir
# si l'abonne 20001 existe, s'il a une cle, s'il est attache, et sur quelle
# machine. Ce script rassemble les quatre sources en une page :
#
#   data/*.txt  -> annuaire.csv    l'etat civil invente au lancement
#   hlr.db                          l'abonne existe-t-il, a-t-il une cle
#   VTY osmo-msc                    est-il ATTACHE, maintenant
#   annuaire-ss7.csv + VTY stp      quelle machine le sert, et sous quel PC
#
# CE QU'IL MONTRE
#   1. les abonnes : IMSI, MSISDN, nom, operateur, pays, cle, attachement
#   2. les incoherences : une fiche sans abonne, un abonne sans fiche, un
#      abonne sans cle - ce dernier etant le plus couteux, parce qu'il n'a
#      l'air de rien : osmo-msc le trouve, reclame ses vecteurs, n'obtient
#      rien, et rejette le mobile en accusant la carte SIM
#   3. l'annuaire SS7 : quel point code, quel routing context, quel hub, et
#      ce que le STP en dit en ce moment
#
# Ne modifie rien. Ne redemarre rien. Lecture seule.
#
# Usage : ./checks/annuaire.sh [--op N] [--ss7] [--no-color]
#   --op N      un seul operateur (defaut : tous ceux qui tournent)
#   --ss7       la section SS7 seule
#   --abo       la section abonnes seule
# =============================================================================
set -uo pipefail

ONLY_OP=""; WANT_ABO=1; WANT_SS7=1; USE_COLOR=1

# Les fiches ne sont pas au meme endroit selon d'ou l'on regarde : start.sh les
# ecrit sur l'HOTE dans /var/tmp, et en pose une copie dans /etc/osmocom de
# chaque conteneur (elle voyage avec la pile). Lance depuis un conteneur, ce
# script ne trouvait donc rien et annoncait "pas de fiches" sur un banc qui en
# avait. On prend la premiere qui existe.
_premier(){ for f in "$@"; do [ -r "$f" ] && { printf '%s' "$f"; return 0; }; done; printf '%s' "$1"; }
ANNU="${OSMO_ANNUAIRE:-$(_premier /var/tmp/osmo-annuaire.csv /etc/osmocom/annuaire.csv)}"
ANNU_SS7="${OSMO_ANNUAIRE_SS7:-$(_premier /var/tmp/osmo-annuaire-ss7.csv /etc/osmocom/annuaire-ss7.csv)}"

while [ $# -gt 0 ]; do
  case "$1" in
    --op)       ONLY_OP="${2:-}"; shift ;;
    --op=*)     ONLY_OP="${1#*=}" ;;
    --ss7)      WANT_ABO=0 ;;
    --abo)      WANT_SS7=0 ;;
    --no-color) USE_COLOR=0 ;;
    -h|--help)  sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
  esac
  shift
done

if [ "$USE_COLOR" = 1 ] && [ -t 1 ]; then
  G=$'\033[0;32m'; R=$'\033[0;31m'; Y=$'\033[1;33m'; C=$'\033[0;36m'; B=$'\033[1m'; D=$'\033[2m'; Z=$'\033[0m'
else G=; R=; Y=; C=; B=; D=; Z=; fi
OK="${G}[ OK ]${Z}"; KO="${R}[FAIL]${Z}"; WARN="${Y}[WARN]${Z}"
head(){ printf '\n%s== %s ==%s\n' "$B" "$1" "$Z"; }

# ── Ou tourne-t-on ? ─────────────────────────────────────────────────────────
# Sur l'hote on interroge les conteneurs ; dans un conteneur, soi-meme. Le
# meme script sert donc des deux cotes, et c'est voulu : un diagnostic qu'on ne
# peut lancer que d'un seul endroit ne sert qu'a la moitie des pannes.
if [ -f /.dockerenv ]; then
  MODE=local
  IN(){ shift; bash -c "$*" 2>/dev/null; }          # $1 ignore
  OPS="${OPERATOR_ID:-1}"
else
  MODE=docker
  IN(){ local c="$1"; shift; docker exec "$c" bash -c "$*" 2>/dev/null; }
  OPS="$(docker ps --format '{{.Names}}' 2>/dev/null | sed -n 's/^osmo-operator-\([0-9]\+\)$/\1/p' | sort -n | tr '\n' ' ')"
fi
[ -n "$ONLY_OP" ] && OPS="$ONLY_OP"
[ -n "${OPS// /}" ] || { printf '%s aucun operateur en service\n' "$KO"; exit 1; }
ct(){ [ "$MODE" = docker ] && echo "osmo-operator-$1" || echo local; }

# ── Les fiches, lues une fois ───────────────────────────────────────────────
# Absent n'est pas une erreur : l'annuaire nait au lancement de start.sh, et un
# banc monte a la main n'en a pas. On le dit et on continue - la partie HLR et
# la partie SS7 restent lisibles sans lui.
if [ -r "$ANNU" ]; then
  printf '%s fiches abonnes : %s (%s lignes)\n' "$OK" "$ANNU" "$(( $(wc -l < "$ANNU") - 1 ))"
else
  printf '%s pas de fiches abonnes (%s) - les noms manqueront\n' "$WARN" "$ANNU"
fi
if [ -r "$ANNU_SS7" ]; then
  printf '%s annuaire SS7    : %s (%s lignes)\n' "$OK" "$ANNU_SS7" "$(( $(wc -l < "$ANNU_SS7") - 1 ))"
else
  printf '%s pas d annuaire SS7 (%s)\n' "$WARN" "$ANNU_SS7"
fi

fiche(){   # $1 = msisdn, $2 = champ (7=nom 3=operateur 4=pays 8=adresse 1=imsi)
  [ -r "$ANNU" ] || return 1
  awk -F';' -v n="$1" -v f="$2" 'NR>1 && $2==n {print $f; exit}' "$ANNU"
}

# ═══════════════════════════ 1. LES ABONNES ═════════════════════════════════
if [ "$WANT_ABO" = 1 ]; then
for op in $OPS; do
  C_NAME="$(ct "$op")"
  head "ABONNES - operateur ${op} (${C_NAME})"

  # Le HLR fait foi sur l'existence et sur la cle. On lit la base plutot que le
  # VTY : "subscriber show" ne parle que d'un abonne a la fois, et il en faut
  # la liste. La colonne cle vient de auc_2g - un abonne sans ligne la-dedans
  # ne peut pas s'authentifier, quoi qu'en dise sa fiche.
  rows="$(IN "$C_NAME" 'command -v sqlite3 >/dev/null 2>&1 || exit 0
      sqlite3 -separator "|" /var/lib/osmocom/hlr.db "
        select s.imsi, coalesce(s.msisdn,\"\"),
               case when a.subscriber_id is null then \"non\" else \"oui\" end
        from subscriber s left join auc_2g a on a.subscriber_id = s.id
        order by s.imsi;"')"
  if [ -z "$rows" ]; then
    printf '  %s HLR illisible (sqlite3 absent, ou base vide)\n' "$KO"
    continue
  fi

  # Qui est ATTACHE, maintenant. C'est le cache d'abonnes du VLR : il ne
  # contient que ce qui s'est presente au reseau. Un abonne provisionne mais
  # jamais vu n'y figure pas - et c'est exactement l'information qui manque
  # quand "tout a l'air bon" dans le HLR.
  # "show subscriber all" n'existe PAS sur osmo-msc (il repond "% Unknown
  # command." et la sortie, non vide, passait pour une liste : tout le monde
  # ressortait non attache). Les formes valides sont : cache, imsi, msisdn, tmsi.
  att="$(IN "$C_NAME" 'exec 9<>/dev/tcp/127.0.0.1/4254 2>/dev/null || exit 0
      { printf "enable\n"; printf "show subscriber cache\n"; sleep 0.5; } >&9
      timeout 3 cat <&9' | tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g')"
  case "$att" in *"Unknown command"*) att="" ;; esac

  # Les codes ANSI comptent dans la largeur d'un %-Ns : on remplit d'abord le
  # texte nu, on colore ensuite. Sans cela chaque cellule coloree decale la
  # ligne d'une dizaine de caracteres et la table devient illisible.
  col(){ local t; printf -v t '%-*s' "$2" "$1"; printf '%s%s%s' "${3:-}" "$t" "${Z}"; }
  printf '  %-16s %-7s %-4s %-8s %-20s %-15s %s\n' \
         IMSI MSISDN CLE ETAT NOM OPERATEUR PAYS
  n_sans_cle=0; n_sans_fiche=0
  while IFS='|' read -r imsi msisdn cle; do
    [ -n "$imsi" ] || continue
    nom="$(fiche "$msisdn" 7)"; ope="$(fiche "$msisdn" 3)"; pays="$(fiche "$msisdn" 4)"
    if [ -z "$nom" ]; then nom="(pas de fiche)"; n_sans_fiche=$((n_sans_fiche+1)); nom_c="$D"; else nom_c=""; fi
    if printf '%s' "$att" | grep -q "$imsi"; then etat="attache"; etat_c="$G"; else etat="-"; etat_c="$D"; fi
    if [ "$cle" = non ]; then cle="NON"; cle_c="$R"; n_sans_cle=$((n_sans_cle+1)); else cle="oui"; cle_c="$G"; fi
    printf '  %-16s %-7s %s %s %s %-15s %s\n' \
           "$imsi" "${msisdn:-(aucun)}" \
           "$(col "$cle" 4 "$cle_c")" "$(col "$etat" 8 "$etat_c")" \
           "$(col "$nom" 20 "$nom_c")" "${ope:--}" "${pays:--}"
  done <<< "$rows"

  if [ "$n_sans_cle" -gt 0 ]; then
    printf '  %s %s abonne(s) SANS CLE : osmo-msc les trouve, reclame leurs vecteurs\n' "$KO" "$n_sans_cle"
    printf '         d authentification, n obtient rien, et rejette le mobile en\n'
    printf '         accusant la carte SIM. Reprovisionner : %ssubscriber imsi X update aud2g comp128v1 ki ...%s\n' "$C" "$Z"
  fi
  [ "$n_sans_fiche" -gt 0 ] && \
    printf '  %s %s abonne(s) sans fiche : le HLR les connait, l annuaire non (banc relance sans start.sh ?)\n' "$WARN" "$n_sans_fiche"

  # Le mobile presente-t-il un IMSI que le HLR connait ? C'est la question que
  # personne ne pose, et la reponse est non plus souvent qu'on ne croit : les
  # deux valeurs sont ecrites par deux scripts differents.
  for f in /root/.osmocom/bb/mobile.cfg /root/.osmocom/bb/mobile_faketrx_bts1.cfg; do
    msi="$(IN "$C_NAME" "awk '\$1==\"imsi\"{print \$2; exit}' $f")"
    [ -n "$msi" ] || continue
    if printf '%s\n' "$rows" | cut -d'|' -f1 | grep -qx "$msi"; then
      printf '  %s %-28s presente %s - connu du HLR\n' "$OK" "$(basename "$f")" "$msi"
    else
      printf '  %s %-28s presente %s - INCONNU du HLR : pas d attachement possible\n' "$KO" "$(basename "$f")" "$msi"
    fi
  done
done
fi

# ═══════════════════════════ 2. L ANNUAIRE SS7 ══════════════════════════════
if [ "$WANT_SS7" = 1 ] && [ -r "$ANNU_SS7" ]; then
  head "ANNUAIRE SS7"
  printf '  %-5s %-16s %-12s %-9s %-9s %-9s %-6s %s\n' \
         NOEUD OPERATEUR PAYS PC_MSC PC_STP PC_BSC RCTX HUB
  awk -F';' 'NR>1 {printf "  %-5s %-16s %-12s %-9s %-9s %-9s %-6s %s\n", $1,$2,$3,$6,$7,$8,$12,$13}' "$ANNU_SS7"

  # Et ce que le STP en dit MAINTENANT. Un point code ecrit dans un fichier ne
  # prouve rien : c'est l'AS actif qui prouve que la machine parle.
  for op in $OPS; do
    C_NAME="$(ct "$op")"
    as_out="$(IN "$C_NAME" 'exec 9<>/dev/tcp/127.0.0.1/4239 2>/dev/null || exit 0
        { printf "enable\n"; printf "show cs7 instance 0 as all\n"; sleep 0.5; } >&9
        timeout 3 cat <&9' | tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g')"
    if [ -z "$as_out" ]; then
      printf '  %s operateur %s : VTY STP (4239) muet\n' "$KO" "$op"
      continue
    fi
    act="$(printf '%s\n' "$as_out" | grep -c 'AS_ACTIVE' || true)"
    tot="$(printf '%s\n' "$as_out" | grep -cE '^as-' || true)"
    if [ "${act:-0}" -gt 0 ]; then
      printf '  %s operateur %s : %s/%s AS actifs\n' "$OK" "$op" "$act" "$tot"
    else
      printf '  %s operateur %s : aucun AS actif (%s declares)\n' "$KO" "$op" "$tot"
    fi
  done
fi

exit 0

checks/check_all.sh

.sh 484 lignes - 24KB

checks/check_all.sh
#!/bin/bash
# =============================================================================
# checks/check_all.sh -- lance TOUS les checks du banc, dans le bon ordre,
#                        et rend UN verdict.
#
# POURQUOI CE FICHIER EXISTE
# --------------------------
# checks/ contient huit scripts qui ne se recouvrent pas : global_check voit
# les demons, ss7_check voit la signalisation, diag-* voit l'interco d'un cote
# ou de l'autre, wan_ss7_check voit ce qui passe entre machines, annuaire voit
# les abonnes. Pris un par un ils sont justes ; pris a la main ils sont
# oublies, lances dans le desordre, ou lances sur le mauvais noeud - et le
# diagnostic finit par dependre de qui tape la commande.
#
# Ce script tape la commande. Il choisit CE QUI A UN SENS ICI (le mode et le
# role decident), execute dans l'ordre du plus general au plus fin, garde tout
# dans un journal, et termine par un tableau : une ligne par check, un verdict,
# un code de retour.
#
# CE QU'IL NE FAIT PAS
#   - il ne modifie rien, ne redemarre rien : tous les checks appeles sont en
#     lecture seule (seul --dump touche au niveau de log des demons, et il est
#     donc optionnel, jamais par defaut) ;
#   - il ne reimplemente aucun test : chaque verdict vient du script d'origine,
#     jamais d'une seconde lecture des VTY faite ici.
#
# CHOIX DES CHECKS - CE QUI EST LANCE, ET QUAND
#   mode           tous les checks respectent --docker/--native ; l'option est
#                  transmise telle quelle, donc un seul monde est regarde.
#   diag-interstp  seulement si un inter-STP est INTERROGEABLE ICI (conteneur
#                  osmo-inter-stp, ou OSMO_ROLE=interstp) : sur un noeud
#                  operateur le hub est une autre machine, son VTY n'ecoute que
#                  sur sa boucle locale, et le lancer produirait un "hub muet"
#                  qui se lit comme une panne.
#   diag-stp-oper. l'exact symetrique : seulement quand ce noeud n'est PAS le
#                  hub et qu'il a un /etc/osmo-role (donc un montage WAN).
#   wan_ss7_check  seulement si /etc/osmo-wan.conf existe : sans plan WAN il
#                  n'y a pas de "entre noeuds" a verifier.
#   annuaire       seulement si les fiches existent (var/tmp ou /etc/osmocom).
#   dump + resume  jamais par defaut : vty-debug-dump.sh passe les demons en
#                  DEBUG et ecrit plusieurs Mo. --dump les active tous les deux,
#                  le resume relisant le dump que le premier vient d'ecrire.
#
# COMMENT EST RENDU LE VERDICT D'UN CHECK
# Les scripts de checks/ n'ont pas tous la meme convention, et on ne la leur
# impose pas apres coup :
#   rc    global_check.sh / ss7_check.sh rendent le NOMBRE d'echecs ;
#   rcb   wan_ss7_check.sh rend 0 ou 1 ;
#   motif diag-*.sh et annuaire.sh rendent toujours 0 - leur verdict est dans
#         leur derniere section ("INTERCO SS7 : DOWN"), on l'y lit, sur la
#         sortie DEBARRASSEE des couleurs.
#
# Usage :
#   sudo ./checks/check_all.sh [options]
#     --quick|-q        transmis a global_check / ss7_check (passage rapide)
#     --verbose|-v      transmis aux checks qui le comprennent
#     --op=N            limiter a un operateur (global_check, annuaire)
#     --docker|--native forcer le monde ; sans option, detection automatique
#     --dump            ajouter vty-debug-dump.sh puis operator_summary.sh
#     --only=a,b        ne lancer que ces checks (voir --list)
#     --skip=a,b        lancer tous les checks sauf ceux-la
#     --list            afficher les checks, leur cle et leur applicabilite
#     --out FICHIER     journal complet (defaut /tmp/osmo-check-all-<date>.txt)
#     --no-log          ne rien ecrire sur disque
#     --timeout N       delai maximum par check, en secondes (defaut 300)
#     --no-color        sortie sans couleur
#
# Code de retour : nombre de checks en echec (0 = tout va bien), 125 au plus.
# =============================================================================

set -u

# ── Bibliotheque commune (detection docker/natif, inventaire, VTY) ────────────
# Chemin RELATIF a CE fichier : le depot vit en /home/.../osmo-operator pendant le
# developpement et en /opt/GSM/osmo-operator sur l'ISO ; un chemin absolu casserait
# l'un des deux.
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
    [ -r "$_c" ] && { . "$_c"; break; }
done
command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }

# ── Options ───────────────────────────────────────────────────────────────────
QUICK=0; VERBOSE=0; SPECIFIC_OP=""; WANT_DUMP=0
ONLY=""; SKIP_LIST=""; LIST_ONLY=0
LOGFILE=""; NO_LOG=0; TIMEOUT=300; USE_COLOR=1
MODE_FORCED=0; MODE_OPT=""

# OSMO_MODE pose dans l'environnement est deja une forme de forcage
# ("OSMO_MODE=native ./checks/check_all.sh") : on le signale comme tel, et on
# le transmet aux fils sous forme d'option pour qu'aucun d'eux ne redetecte.
case "${OSMO_MODE:-}" in docker|native) MODE_FORCED=1 ;; esac

while [ $# -gt 0 ]; do
    case "$1" in
        --quick|-q)   QUICK=1 ;;
        --verbose|-v) VERBOSE=1 ;;
        --op)         SPECIFIC_OP="${2:-}"; shift ;;
        --op=*)       SPECIFIC_OP="${1#*=}" ;;
        --dump)       WANT_DUMP=1 ;;
        --only)       ONLY="${2:-}"; shift ;;
        --only=*)     ONLY="${1#*=}" ;;
        --skip)       SKIP_LIST="${2:-}"; shift ;;
        --skip=*)     SKIP_LIST="${1#*=}" ;;
        --list)       LIST_ONLY=1 ;;
        --out)        LOGFILE="${2:-}"; shift ;;
        --out=*)      LOGFILE="${1#*=}" ;;
        --no-log)     NO_LOG=1 ;;
        --timeout)    TIMEOUT="${2:-}"; shift ;;
        --timeout=*)  TIMEOUT="${1#*=}" ;;
        --no-color)   USE_COLOR=0 ;;
        --docker|--native|--mode=*)
            osmo_mode_force "$1" || { echo "mode inconnu : $1" >&2; exit 1; }
            MODE_FORCED=1 ;;
        -h|--help)    sed -n '2,69p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *)            echo "option inconnue : $1 (voir --help)" >&2; exit 1 ;;
    esac
    shift
done

case "$TIMEOUT" in ''|*[!0-9]*) echo "--timeout attend un entier" >&2; exit 1 ;; esac

# ── Couleurs ──────────────────────────────────────────────────────────────────
if [ "$USE_COLOR" = 1 ] && [ -t 1 ]; then
    GREEN=$'\033[0;32m'; CYAN=$'\033[0;36m'; YELLOW=$'\033[1;33m'
    RED=$'\033[0;31m';   BOLD=$'\033[1m';    DIM=$'\033[2m'; NC=$'\033[0m'
else
    GREEN=; CYAN=; YELLOW=; RED=; BOLD=; DIM=; NC=
fi

banner() {
    echo ""
    echo "${BOLD}$*${NC}"
    printf '%s\n' "$(printf '─%.0s' $(seq 1 74))"
}

# ── Mode, role, inventaire ────────────────────────────────────────────────────
MODE="$(osmo_mode)"
[ "$MODE_FORCED" -eq 1 ] && MODE_SRC="force" || MODE_SRC="detecte"

# L'option de mode transmise aux fils. On la transmet TOUJOURS, meme quand elle
# a ete detectee ici : sans elle chaque fils refait la detection, et deux
# detections successives sur un hote qui a docker installe mais fait tourner le
# lab en natif peuvent ne pas tomber d'accord. Un rapport dont les moities
# regardent deux mondes differents n'est pas un rapport.
MODE_OPT="--$MODE"

HUB_LOCAL=0; HUB_LABEL=""
if HUB_LABEL="$(osmo_hub 2>/dev/null)"; then HUB_LOCAL=1; else HUB_LABEL=""; fi

ROLE=""
[ -r /etc/osmo-role ] && \
    ROLE="$(awk -F= '/^OSMO_ROLE=/ { gsub(/[ \r\t]/, "", $2); print $2 }' /etc/osmo-role 2>/dev/null | tail -1)"

OPS="$(osmo_ops 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')"
N_OPS="$(printf '%s\n' "$OPS" | wc -w | tr -d ' ')"

# Les fiches d'abonnes ne sont pas au meme endroit selon d'ou l'on regarde :
# start.sh les ecrit sur l'HOTE dans /var/tmp et en pose une copie dans
# /etc/osmocom de chaque conteneur. Meme regle que checks/annuaire.sh.
ANNU_FOUND=0
for f in "${OSMO_ANNUAIRE:-}" /var/tmp/osmo-annuaire.csv /etc/osmocom/annuaire.csv; do
    [ -n "$f" ] && [ -r "$f" ] && { ANNU_FOUND=1; break; }
done

# ── Journal ───────────────────────────────────────────────────────────────────
# Le journal recoit la sortie SANS les couleurs : un rapport qu'on envoie par
# mail ou qu'on relit six mois plus tard ne doit pas etre truffe d'echappements.
# Le terminal, lui, garde les siennes.
STRIP_ANSI='s/\x1b\[[0-9;]*[a-zA-Z]//g'
if [ "$NO_LOG" -eq 0 ]; then
    [ -n "$LOGFILE" ] || LOGFILE="/tmp/osmo-check-all-$(date '+%Y%m%d-%H%M%S').txt"
    if : >"$LOGFILE" 2>/dev/null; then :; else
        echo "${YELLOW}journal impossible a ecrire (${LOGFILE}) - on continue sans${NC}" >&2
        NO_LOG=1; LOGFILE=""
    fi
else
    LOGFILE=""
fi
journal() { [ -n "$LOGFILE" ] && printf '%s\n' "$*" | sed "$STRIP_ANSI" >>"$LOGFILE"; return 0; }

# ── Le catalogue ──────────────────────────────────────────────────────────────
# Un enregistrement par check :  cle | script | verdict | description
#   verdict = rc     le script rend le NOMBRE d'echecs (global_check, ss7_check)
#             rcb    le script rend 0 ou non-0 (wan_ss7_check)
#             motif  le script rend toujours 0 : le verdict se lit dans sa
#                    sortie, motif donne apres ':' (diag-*, annuaire)
#             info   sortie informative, jamais en echec (dump, resume)
# L'ordre est celui du diagnostic : d'abord ce qui tourne, puis la
# signalisation locale, puis l'interco, puis le WAN, puis les abonnes.
CATALOGUE=(
  "global|global_check.sh|rc|etat de tous les demons (STP, HLR, MSC, BSC, BTS, PCU, SGSN, GGSN, MGW)"
  "ss7|ss7_check.sh|rc|signalisation SS7 : AS, ASP, routes, catch-all vers l inter-STP"
  "interstp|diag-interstp.sh|motif:INTERCO SS7 : (DOWN|PARTIELLE)|indeterminable|interco vue du HUB : qui est attache, qui manque"
  "operator|diag-stp-operator.sh|motif:INTERCO SS7 : DOWN|interco vue du NOEUD : conf disque vs demon, SCTP vers le hub"
  "wan|wan_ss7_check.sh|rcb|entre noeuds : SIP/RTP, relais SMS, point codes, iptables"
  "annuaire|annuaire.sh|motif:\\[FAIL\\]|abonnes : IMSI, MSISDN, cles Ki, attachement, annuaire SS7"
  "dump|vty-debug-dump.sh|info|dump DEBUG de tous les VTY (--dump seulement)"
  "resume|operator_summary.sh|info|resume par operateur, lu sur le dump (--dump seulement)"
)

champ() { printf '%s' "$1" | cut -d'|' -f"$2"; }
# Le motif peut contenir des '|' (alternative regex) : il occupe donc tout ce
# qui est entre le 3e champ et le DERNIER, la description etant le dernier.
verdict_de()     { printf '%s' "${1#*|*|}" | sed 's/|[^|]*$//'; }
description_de() { printf '%s' "$1" | awk -F'|' '{print $NF}'; }

# applicable <cle> - ce check a-t-il un sens ici ? Ecrit la raison si non.
applicable() {
    case "$1" in
        global|ss7) return 0 ;;
        interstp)
            [ "$HUB_LOCAL" -eq 1 ] && return 0
            printf 'aucun inter-STP interrogeable ici (%s)' "$(osmo_hub_hint 2>/dev/null || echo 'hub distant ou absent')"
            return 1 ;;
        operator)
            [ "$HUB_LOCAL" -eq 1 ] && { printf 'ce noeud EST le hub - voir le check interstp'; return 1; }
            [ -r /etc/osmo-role ] && return 0
            printf '/etc/osmo-role absent : pas de montage WAN sur ce noeud'
            return 1 ;;
        wan)
            [ -r /etc/osmo-wan.conf ] && return 0
            printf '/etc/osmo-wan.conf absent : aucun plan WAN a verifier'
            return 1 ;;
        annuaire)
            [ "$ANNU_FOUND" -eq 1 ] && return 0
            printf 'aucune fiche (osmo-annuaire.csv) : banc lance sans annuaire'
            return 1 ;;
        dump|resume)
            [ "$WANT_DUMP" -eq 1 ] && return 0
            printf 'non demande (--dump)'
            return 1 ;;
    esac
    return 0
}

# args_de <cle> - les options a passer, chaque script n'ayant pas les memes.
args_de() {
    local a=("$MODE_OPT")
    case "$1" in
        global)
            [ "$QUICK" -eq 1 ]        && a+=(--quick)
            [ "$VERBOSE" -eq 1 ]      && a+=(--verbose)
            [ -n "$SPECIFIC_OP" ]     && a+=("--op=$SPECIFIC_OP") ;;
        ss7)
            [ "$QUICK" -eq 1 ]        && a+=(--quick)
            [ "$VERBOSE" -eq 1 ]      && a+=(--verbose) ;;
        interstp)
            # diag-interstp.sh et diag-stp-operator.sh sont anterieurs a
            # _mode.sh : ils lisent /etc/osmo-role et les VTY locaux, et ne
            # connaissent ni --docker ni --native. Leur passer MODE_OPT les
            # ferait sortir en "option inconnue".
            a=()
            [ "$USE_COLOR" -eq 0 ]    && a+=(--no-color) ;;
        operator)
            a=()
            [ "$USE_COLOR" -eq 0 ]    && a+=(--no-color) ;;
        wan)
            a=()
            [ "$VERBOSE" -eq 1 ]      && a+=(--verbose) ;;
        annuaire)
            a=()
            [ -n "$SPECIFIC_OP" ]     && a+=(--op "$SPECIFIC_OP")
            [ "$USE_COLOR" -eq 0 ]    && a+=(--no-color) ;;
        dump)
            a+=(--out "$DUMP_OUT") ;;
        resume)
            a+=("--dump=$DUMP_OUT") ;;
    esac
    printf '%s\n' "${a[@]:-}"
}

# ── --list : dire ce qui serait fait, sans rien faire ─────────────────────────
if [ "$LIST_ONLY" -eq 1 ]; then
    echo "${BOLD}checks disponibles${NC}   (mode ${MODE}, ${MODE_SRC})"
    echo ""
    printf '  %-10s %-24s %s\n' CLE SCRIPT ETAT
    printf '  %s\n' "$(printf '─%.0s' $(seq 1 72))"
    for e in "${CATALOGUE[@]}"; do
        k="$(champ "$e" 1)"; s="$(champ "$e" 2)"
        if raison="$(applicable "$k")"; then
            printf '  %-10s %-24s %slance%s\n' "$k" "$s" "$GREEN" "$NC"
        else
            printf '  %-10s %-24s %signore%s - %s\n' "$k" "$s" "$DIM" "$NC" "$raison"
        fi
        printf '  %-10s %-24s %s%s%s\n' "" "" "$DIM" "$(description_de "$e")" "$NC"
    done
    echo ""
    exit 0
fi

# ── En-tete ───────────────────────────────────────────────────────────────────
DUMP_OUT="/tmp/osmo-check-all-dump-$(date '+%Y%m%d-%H%M%S').txt"
DATE_DEB="$(date '+%Y-%m-%d %H:%M:%S')"

en_tete() {
    echo "${CYAN}${BOLD}"
    echo "╔══════════════════════════════════════════════════════════════════════════╗"
    echo "║   osmo-operator - CHECK COMPLET du banc                                     ║"
    echo "╚══════════════════════════════════════════════════════════════════════════╝"
    echo "${NC}"
    printf '  date       : %s\n' "$DATE_DEB"
    printf '  machine    : %s\n' "$(hostname 2>/dev/null || echo '?')"
    printf '  mode       : %s%s%s (%s)\n' "$CYAN" "$MODE" "$NC" "$MODE_SRC"
    printf '  operateurs : %s   (%s)\n' "${N_OPS:-0}" "${OPS:-aucun}"
    if [ "$HUB_LOCAL" -eq 1 ]; then
        printf '  inter-STP  : %s%s%s (interrogeable ici)\n' "$GREEN" "$HUB_LABEL" "$NC"
    else
        printf '  inter-STP  : %s\n' "$(osmo_hub_hint 2>/dev/null || echo 'non interrogeable ici')"
    fi
    [ -n "$ROLE" ] && printf '  role       : %s\n' "$ROLE"
    [ -n "$LOGFILE" ] && printf '  journal    : %s\n' "$LOGFILE"
    # Les VTY Osmocom ecoutent sur la boucle locale et docker exige le socket :
    # sans droits, la moitie des checks rendront "muet" pour une raison qui n'a
    # rien a voir avec le reseau. On le dit AVANT, pas apres.
    [ "$(id -u)" -ne 0 ] && printf '  %s! lance sans root : certains checks seront incomplets (sudo recommande)%s\n' "$YELLOW" "$NC"
}
en_tete
en_tete_txt="$(en_tete 2>&1)"
journal "$en_tete_txt"

# ── Execution ─────────────────────────────────────────────────────────────────
NB_OK=0; NB_KO=0; NB_IGN=0
RES_CLE=(); RES_ETAT=(); RES_RC=(); RES_DUREE=(); RES_NOTE=()

dans_liste() {   # dans_liste <aiguille> <a,b,c>
    local x; local IFS=','
    for x in $2; do [ "$x" = "$1" ] && return 0; done
    return 1
}

enregistre() {   # enregistre <cle> <etat> <rc> <duree> <note>
    RES_CLE+=("$1"); RES_ETAT+=("$2"); RES_RC+=("$3"); RES_DUREE+=("$4"); RES_NOTE+=("${5:-}")
}

for entree in "${CATALOGUE[@]}"; do
    cle="$(champ "$entree" 1)"
    script="$(champ "$entree" 2)"
    verdict="$(verdict_de "$entree")"
    desc="$(description_de "$entree")"
    chemin="$_here/$script"

    # --only / --skip d'abord : ce que l'utilisateur a exclu n'est meme pas
    # evalue, et n'apparait pas comme "ignore par le banc" - la distinction
    # compte, l'un est un choix, l'autre un constat.
    [ -n "$ONLY" ]      && { dans_liste "$cle" "$ONLY"      || continue; }
    [ -n "$SKIP_LIST" ] && { dans_liste "$cle" "$SKIP_LIST" && continue; }

    if ! raison="$(applicable "$cle")"; then
        banner "[$cle] $script"
        echo "  ${DIM}ignore${NC} - $raison"
        journal ""; journal "== [$cle] $script =="; journal "  ignore - $raison"
        NB_IGN=$((NB_IGN+1)); enregistre "$cle" IGNORE "-" "-" "$raison"
        continue
    fi

    if [ ! -x "$chemin" ]; then
        banner "[$cle] $script"
        echo "  ${RED}absent ou non executable${NC} : $chemin"
        journal ""; journal "== [$cle] $script =="; journal "  absent : $chemin"
        NB_KO=$((NB_KO+1)); enregistre "$cle" ABSENT "-" "-" "$chemin introuvable"
        continue
    fi

    mapfile -t opts < <(args_de "$cle")
    # args_de rend une ligne vide quand il n'y a aucune option : le tableau
    # contiendrait alors un argument vide, que certains scripts refusent.
    [ "${#opts[@]}" -eq 1 ] && [ -z "${opts[0]}" ] && opts=()

    banner "[$cle] $script ${DIM}${opts[*]:-}${NC}"
    echo "  ${DIM}${desc}${NC}"
    echo ""
    journal ""
    journal "════════════════════════════════════════════════════════════════════════"
    journal "== [$cle] $script ${opts[*]:-}"
    journal "   $desc"
    journal "════════════════════════════════════════════════════════════════════════"

    tmp="$(mktemp "${TMPDIR:-/tmp}/osmo-check-$cle-XXXXXX")"
    debut=$SECONDS
    # timeout : un VTY qui ne repond jamais (demon fige, docker exec suspendu)
    # bloquerait tout le rapport. On coupe, on le dit, on continue.
    # PIPESTATUS et non "set -o pipefail" : sous pipefail un "grep -q" en fin
    # de tube, dans l'un des fils, rendrait 141 (voir checks/_mode.sh).
    timeout "$TIMEOUT" "$chemin" "${opts[@]}" 2>&1 | tee "$tmp"
    rc="${PIPESTATUS[0]}"
    duree=$((SECONDS - debut))

    [ -n "$LOGFILE" ] && sed "$STRIP_ANSI" "$tmp" >>"$LOGFILE"
    propre="$(sed "$STRIP_ANSI" "$tmp")"
    rm -f "$tmp"

    etat=OK; note=""
    if [ "$rc" -eq 124 ]; then
        etat=TIMEOUT; note="coupe apres ${TIMEOUT}s"
    else
        case "$verdict" in
            rc)
                # global_check.sh et ss7_check.sh rendent le nombre d'echecs.
                if [ "$rc" -ne 0 ]; then etat=ECHEC; note="$rc test(s) en echec"; fi ;;
            rcb)
                if [ "$rc" -ne 0 ]; then etat=ECHEC; note="code de retour $rc"; fi ;;
            motif:*)
                # Le script rend toujours 0 : son verdict est dans sa sortie.
                # On retient la LIGNE entiere, pas le motif : "[FAIL]" tout seul
                # dans le bilan n'apprend rien, "[FAIL] operateur 1 : VTY STP
                # (4239) muet" dispense d'aller rouvrir le journal.
                m="${verdict#motif:}"
                nb="$(printf '%s\n' "$propre" | grep -Ec -- "$m")"
                if [ "${nb:-0}" -gt 0 ]; then
                    etat=ECHEC
                    note="$(printf '%s\n' "$propre" | grep -Em1 -- "$m" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cut -c1-60)"
                    [ "$nb" -gt 1 ] && note="$note (+$((nb-1)))"
                fi ;;
            info)
                etat=INFO ;;
        esac
    fi

    case "$etat" in
        OK)      NB_OK=$((NB_OK+1));  echo ""; echo "  ${GREEN}→ $cle : OK${NC} (${duree}s)" ;;
        INFO)    NB_OK=$((NB_OK+1));  echo ""; echo "  ${CYAN}→ $cle : termine${NC} (${duree}s)" ;;
        TIMEOUT) NB_KO=$((NB_KO+1));  echo ""; echo "  ${RED}→ $cle : TIMEOUT${NC} - $note" ;;
        *)       NB_KO=$((NB_KO+1));  echo ""; echo "  ${RED}→ $cle : PROBLEMES${NC} - $note (${duree}s)" ;;
    esac
    journal "  → $cle : $etat ${note} (${duree}s, rc=$rc)"

    enregistre "$cle" "$etat" "$rc" "${duree}s" "$note"
done

# ── Bilan ─────────────────────────────────────────────────────────────────────
bilan() {
    echo ""
    printf '%s\n' "$(printf '═%.0s' $(seq 1 74))"
    echo "  ${BOLD}BILAN - $(date '+%Y-%m-%d %H:%M:%S')${NC}"
    printf '%s\n' "$(printf '═%.0s' $(seq 1 74))"
    printf '  %-10s %-9s %-6s %-7s %s\n' CHECK ETAT RC DUREE COMMENTAIRE
    printf '  %s\n' "$(printf '─%.0s' $(seq 1 72))"
    local i coul
    for i in "${!RES_CLE[@]}"; do
        case "${RES_ETAT[$i]}" in
            OK)                coul="$GREEN" ;;
            INFO)              coul="$CYAN" ;;
            IGNORE)            coul="$DIM" ;;
            ECHEC|TIMEOUT|ABSENT) coul="$RED" ;;
            *)                 coul="$YELLOW" ;;
        esac
        printf '  %-10s %s%-9s%s %-6s %-7s %s\n' \
            "${RES_CLE[$i]}" "$coul" "${RES_ETAT[$i]}" "$NC" \
            "${RES_RC[$i]}" "${RES_DUREE[$i]}" "${RES_NOTE[$i]}"
    done
    printf '  %s\n' "$(printf '─%.0s' $(seq 1 72))"
    printf '  %s%d ok%s   %s%d en echec%s   %s%d ignore%s   (mode %s)\n' \
        "$GREEN" "$NB_OK" "$NC" "$RED" "$NB_KO" "$NC" "$DIM" "$NB_IGN" "$NC" "$MODE"
    echo ""
    if [ "$NB_KO" -eq 0 ] && [ "$NB_OK" -eq 0 ]; then
        # Tout ignore ou tout exclu : dire "BANC OK" ici serait un mensonge par
        # omission - rien n'a ete regarde.
        echo "  ${YELLOW}${BOLD}AUCUN CHECK N A TOURNE${NC} - tout etait ignore ou exclu (--list pour savoir pourquoi)"
    elif [ "$NB_KO" -eq 0 ]; then
        echo "  ${GREEN}${BOLD}BANC OK${NC} - ${NB_OK} check(s) passes, aucun en echec"
    else
        echo "  ${RED}${BOLD}PROBLEMES DETECTES${NC} - ${NB_KO} check(s) en echec"
        echo ""
        echo "  ${DIM}rejouer un seul check en detail :${NC}"
        for i in "${!RES_CLE[@]}"; do
            case "${RES_ETAT[$i]}" in
                ECHEC|TIMEOUT) echo "    ./checks/check_all.sh --only=${RES_CLE[$i]} --verbose" ;;
            esac
        done
    fi
    [ -n "$LOGFILE" ] && { echo ""; echo "  ${CYAN}journal complet :${NC} $LOGFILE"; }
    [ "$WANT_DUMP" -eq 1 ] && [ -s "$DUMP_OUT" ] && echo "  ${CYAN}dump VTY        :${NC} $DUMP_OUT"
    printf '%s\n' "$(printf '═%.0s' $(seq 1 74))"
    echo ""
}
bilan
journal "$(bilan)"

# Meme convention que global_check.sh / ss7_check.sh : le code de retour est le
# nombre d'echecs. Plafonne a 125 - au-dela, un code de sortie shell change de
# sens (126, 127 et 128+n sont reserves).
[ "$NB_KO" -gt 125 ] && NB_KO=125
exit "$NB_KO"

checks/diag-interstp.sh

.sh 255 lignes - 12KB

checks/diag-interstp.sh
#!/bin/bash
# =============================================================================
# checks/diag-interstp.sh -- diagnostic SS7 vu du HUB (inter-STP)
#
# Repond a une seule question : QUI est attache, et qui manque ?
# L'identite de ce qui est attache vient du hub lui-meme (colonne AS Name et
# adresse distante de "show cs7 instance 0 asp"). La table /etc/osmo-wan.conf
# ne sert plus qu'a l'appel des absents : un noeud absent de la table reste
# indiscernable d'un noeud eteint.
#
# CE QU'IL VERIFIE
#   1. role et adresse d'ecoute (le multi-homing 0.0.0.0 est signale)
#   2. le demon tourne, le port M3UA ecoute
#   3. chaque AS declare par le hub : AS_ACTIVE / AS_DOWN, et les ASP rattaches
#   4. l'appel de la table WAN : quelle adresse attendue n'est vue nulle part
#   5. les routes : PROHIB / UNAVAIL signales
#   6. le flapping : deux mesures espacees, les ASP dynamiques renumerotes
#      trahissent des associations qui se reetablissent en boucle
#
# Ne modifie rien. Ne redemarre rien. Lecture seule.
#
# Usage : sudo ./checks/diag-interstp.sh [--watch] [--no-color]
#   --watch     seconde mesure a +10 s pour detecter le flapping
# =============================================================================
set -uo pipefail

ROLE_FILE=/etc/osmo-role
WAN_FILE=/etc/osmo-wan.conf
CFG=/etc/osmocom/osmo-stp-interop.cfg
VTY_PORT=4239
M3UA_PORT=2908
WATCH=0; USE_COLOR=1

while [ $# -gt 0 ]; do
  case "$1" in
    --watch)    WATCH=1 ;;
    --no-color) USE_COLOR=0 ;;
    -h|--help)  sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
  esac
  shift
done

if [ "$USE_COLOR" = 1 ] && [ -t 1 ]; then
  G=$'\033[0;32m'; R=$'\033[0;31m'; Y=$'\033[1;33m'; C=$'\033[0;36m'; B=$'\033[1m'; Z=$'\033[0m'
else G=; R=; Y=; C=; B=; Z=; fi
OK="${G}[ OK ]${Z}"; KO="${R}[FAIL]${Z}"; WARN="${Y}[WARN]${Z}"
head(){ printf '\n%s== %s ==%s\n' "$B" "$1" "$Z"; }

vty(){ { printf 'enable\n'; for c in "$@"; do printf '%s\n' "$c"; done; sleep 0.6; } \
       | timeout 8 nc -q1 127.0.0.1 "$VTY_PORT" 2>/dev/null | tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g'; }

getv(){ awk -F= -v k="$1" '$1==k{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' "$ROLE_FILE" 2>/dev/null; }

# ---- identite ---------------------------------------------------------------
head "IDENTITE"
ROLE="$(getv OSMO_ROLE)"; SELF="$(getv OSMO_HUB_IP)"
printf '  hostname : %s\n' "$(hostname)"
printf '  role     : %s%s%s   adresse annoncee : %s%s%s\n' "$C" "${ROLE:-?}" "$Z" "$C" "${SELF:-?}" "$Z"
[ "$ROLE" = "interstp" ] || printf '  %s role=%s : ce script vise le HUB (voir diag-stp-operator.sh)\n' "$WARN" "${ROLE:-?}"

# adresse d'ecoute effective
LISTEN_IP="$(awk '/listen m3ua/{f=1} f&&/local-ip/{print $2; exit}' "$CFG" 2>/dev/null)"
LISTEN_PORT="$(awk '/listen m3ua/{print $3; exit}' "$CFG" 2>/dev/null)"
printf '  ecoute   : %s:%s\n' "${LISTEN_IP:-?}" "${LISTEN_PORT:-?}"
if [ "$LISTEN_IP" = "0.0.0.0" ]; then
  printf '  %s local-ip 0.0.0.0 : SCTP annonce TOUTES les adresses de la machine\n' "$WARN"
  printf '         dans son INIT (NAT, alias 172.20.x, host-only). Les noeuds tentent\n'
  printf '         des chemins morts et les ASP montent puis retombent, en boucle.\n'
  printf '         Corriger : set_stp_ip.sh --inter --ip <adresse du hub>\n'
fi

# ---- demon ------------------------------------------------------------------
head "DEMON"
ST="$(systemctl is-active osmo-interstp 2>/dev/null)"
[ -z "$ST" ] && ST="$(systemctl is-active osmo-stp 2>/dev/null)"
if [ "$ST" = active ]; then printf '  %s service actif (%s)\n' "$OK" "$ST"
else printf '  %s service inactif (%s)\n' "$KO" "${ST:-inconnu}"; fi
if ss -Sanl 2>/dev/null | grep -q ":${LISTEN_PORT:-$M3UA_PORT} "; then
  printf '  %s port M3UA %s en ecoute (SCTP)\n' "$OK" "${LISTEN_PORT:-$M3UA_PORT}"
else
  printf '  %s port M3UA %s PAS en ecoute\n' "$KO" "${LISTEN_PORT:-$M3UA_PORT}"
fi

# ---- table WAN (sert seulement a l appel des absents) -----------------------
declare -A EXP_IP=()
NODES=""
if [ -r "$WAN_FILE" ]; then
  # Ancre en debut de ligne : la ligne de commentaire du fichier rappelle le
  # format WAN_NODES="id:ip:indicatif ..." et etait lue comme une entree, d ou
  # deux noeuds fantomes ("id", "...") toujours portes manquants a l appel.
  spec="$(awk -F\" '/^WAN_NODES=/{print $2}' "$WAN_FILE" 2>/dev/null)"
  for tok in $spec; do
    id="${tok%%:*}"; rest="${tok#*:}"; ip="${rest%%:*}"
    EXP_IP[$id]="$ip"; NODES="$NODES $id"
  done
fi

# ---- AS / ASP ---------------------------------------------------------------
head "AS DECLARES PAR LE HUB"
AS_OUT="$(vty 'show cs7 instance 0 as all')"
ASP_OUT="$(vty 'show cs7 instance 0 asp')"

# Le hub construit ses AS a partir du seul NOMBRE de noeuds et apparie les ASP
# par routing context. Apparier ici as-n<n>op1 avec l'adresse attendue du noeud
# n, lue dans une table locale rangee autrement que celle en service, donnait
# l'etat d'une machine a sa voisine : un noeud sain sortait en panne et l'autre
# en marche. On lit donc le rattachement la ou le hub le declare : colonne
# AS Name et adresse distante de sa propre sortie.
# Ces colonnes ont bouge selon les versions de libosmo-sigtran : l'etat et
# l'adresse sont reperes a leur forme, pas a leur rang.
ASP_ROWS="$(printf '%s\n' "$ASP_OUT" | awk '
  /ASP_/ {
    st = ""; rem = ""
    for (i = 2; i <= NF; i++) {
      if ($i ~ /^ASP_/) st = $i
      if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?$/) rem = $i
    }
    sub(/:[0-9]+$/, "", rem)
    printf "%s|%s|%s|%s\n", $1, $2, (st == "" ? "?" : st), (rem == "" ? "?" : rem)
  }')"
AS_ROWS="$(printf '%s\n' "$AS_OUT" | awk '$1 ~ /^as-/ {print $1 "|" $2}')"

if [ -z "$AS_OUT" ]; then
  printf '  %s VTY injoignable sur 127.0.0.1:%s -- nc absent, ou demon arrete\n' "$KO" "$VTY_PORT"
elif [ -z "$AS_ROWS" ]; then
  printf '  %s aucun AS declare : conf du hub vide, ou autre instance cs7\n' "$KO"
else
  while IFS='|' read -r as_name as_state; do
    [ -n "$as_name" ] || continue
    if [ "$as_state" = AS_ACTIVE ]; then m="$OK"; else m="$KO"; fi
    printf '  %s %-12s %s\n' "$m" "$as_name" "${as_state:-?}"
    attache=0
    while IFS='|' read -r asp_name asp_as asp_state asp_rem; do
      [ "$asp_as" = "$as_name" ] || continue
      attache=1
      printf '         %-12s %-12s depuis %s\n' "$asp_name" "$asp_state" "$asp_rem"
    done <<< "$ASP_ROWS"
    [ "$attache" = 0 ] && printf '         aucun ASP rattache : noeud eteint, asp en shutdown, ou SCTP bloque\n'
  done <<< "$AS_ROWS"
  # NF : sans aucun ASP, printf emet quand meme une ligne vide, dont le champ
  # 2 (vide) ne matche pas /^as-/. Le bloc annoncait alors un orphelin "()" et
  # un desaccord de routing context - dans l'etat ou il n'y a tout simplement
  # rien d'attache, celui-la meme pour lequel on lance ce diagnostic.
  orph="$(printf '%s\n' "$ASP_ROWS" | awk -F'|' 'NF && $2 !~ /^as-/ {printf "%s(%s) ", $1, $4}')"
  if [ -n "$orph" ]; then
    printf '  %s ASP connecte mais rattache a aucun AS : %s\n' "$WARN" "${orph% }"
    printf '         son routing context ne correspond a aucun AS du hub\n'
  fi
fi

head "APPEL DE LA TABLE WAN (${WAN_FILE})"
if [ -z "$ASP_OUT" ]; then
  printf '  %s pas de reponse VTY : rien a confronter a la table\n' "$KO"
elif [ -z "$NODES" ]; then
  printf '  %s table absente ou vide : impossible de dire qui manque a l appel,\n' "$WARN"
  printf '         la liste ci-dessus reste complete pour ce qui est attache\n'
else
  # Une adresse attachee absente de la table = noeud masque (NAT) : le hub voit
  # alors l'adresse du routeur et pas celle du noeud. Sans ce rappel on declare
  # absent un noeud qui parle, au seul motif qu'il est masque.
  tab=" "
  for n in $NODES; do tab="$tab${EXP_IP[$n]} "; done
  hors=""
  for ip in $(printf '%s\n' "$ASP_ROWS" | awk -F'|' '$4 ~ /^[0-9]/ {print $4}' | sort -u); do
    case "$tab" in *" $ip "*) ;; *) hors="$hors $ip" ;; esac
  done
  # Une adresse hors table ne peut masquer QU'UN seul noeud. Sans ce compte,
  # un unique ASP masque - l'etat permanent d'un conteneur derriere le NAT -
  # degradait en WARN tous les noeuds reellement absents, et le FAIL "aucune
  # association" ne pouvait plus jamais sortir.
  n_hors=0
  for _ in $hors; do n_hors=$(( n_hors + 1 )); done
  for n in $NODES; do
    if printf '%s\n' "$ASP_ROWS" | awk -F'|' -v ip="${EXP_IP[$n]}" '$4 == ip {f = 1} END {exit !f}'; then
      printf '  %s %-15s %-20s (noeud %s de la table)\n' "$OK" "${EXP_IP[$n]}" "attachee" "$n"
    elif [ "$n_hors" -gt 0 ]; then
      n_hors=$(( n_hors - 1 ))
      printf '  %s %-15s %-20s (noeud %s de la table)\n' "$WARN" "${EXP_IP[$n]}" "pas vue telle quelle" "$n"
    else
      printf '  %s %-15s %-20s (noeud %s de la table)\n' "$KO" "${EXP_IP[$n]}" "aucune association" "$n"
    fi
  done
  [ -n "$hors" ] && printf '  %s attachees hors table :%s -- masquage NAT probable\n' "$WARN" "$hors"
fi

head "ASP ATTACHES"
if [ -n "$ASP_OUT" ]; then
  printf '%s\n' "$ASP_OUT" | grep -E 'asp-|ASP Name' | sed 's/^/  /' | cut -c1-160
else
  printf '  %s pas de reponse VTY\n' "$KO"
fi

# ---- routes -----------------------------------------------------------------
head "ROUTES"
RT="$(vty 'show cs7 instance 0 route')"
if [ -n "$RT" ]; then
  bad="$(printf '%s\n' "$RT" | grep -E 'PROHIB|UNAVAIL|INACC' || true)"
  n_ok="$(printf '%s\n' "$RT" | grep -cE '^[0-9]+\.[0-9]+\.[0-9]+/' || true)"
  printf '  %s routes declarees\n' "${n_ok:-0}"
  if [ -n "$bad" ]; then
    printf '  %s destinations interdites ou injoignables :\n' "$WARN"
    printf '%s\n' "$bad" | sed 's/^/    /'
    printf '    (TFP herite d un flapping : relancer osmo-stp sur le noeud concerne)\n'
  else
    printf '  %s aucune route PROHIB/UNAVAIL\n' "$OK"
  fi
else
  printf '  %s pas de reponse VTY\n' "$KO"
fi

# ---- flapping ---------------------------------------------------------------
if [ "$WATCH" = 1 ]; then
  head "FLAPPING (2e mesure a +10 s)"
  first="$(printf '%s\n' "$ASP_OUT" | grep -oE 'asp-dyn-[0-9]+' | sort -u | tr '\n' ' ')"
  printf '  t0    : %s\n' "${first:-aucun}"
  sleep 10
  second="$(vty 'show cs7 instance 0 asp' | grep -oE 'asp-dyn-[0-9]+' | sort -u | tr '\n' ' ')"
  printf '  t+10s : %s\n' "${second:-aucun}"
  if [ "$first" = "$second" ]; then
    printf '  %s ASP stables - pas de reetablissement en boucle\n' "$OK"
  else
    printf '  %s les ASP dynamiques ont change de numero : les associations se\n' "$KO"
    printf '         reetablissent en boucle. Cause habituelle : local-ip 0.0.0.0\n'
    printf '         cote hub (multi-homing vers des adresses injoignables).\n'
  fi
fi

# ---- verdict ----------------------------------------------------------------
head "VERDICT"
if [ -n "${AS_OUT:-}" ]; then
  # Deux conventions de nom coexistent : as-n<noeud>op<op> en mode WAN, et
  # as-op<i> hors mode WAN (helpers/create_interop.sh). Le verdict ne comptait
  # que la premiere : la section ci-dessus listait trois AS_ACTIVE et le verdict
  # concluait juste en dessous "aucun AS actif". Une sortie qui se dement
  # elle-meme fait chercher une panne qui n'existe pas.
  tot="$(printf '%s\n' "$AS_OUT" | grep -cE 'as-(n[0-9]+)?op[0-9]+' || true)"
  act="$(printf '%s\n' "$AS_OUT" | grep -E 'as-(n[0-9]+)?op[0-9]+' | grep -c 'AS_ACTIVE' || true)"
  if [ "${act:-0}" -gt 0 ] && [ "${act:-0}" = "${tot:-0}" ]; then
    printf '  %sINTERCO SS7 : UP%s  - %s/%s AS actifs\n' "$G$B" "$Z" "$act" "$tot"
  elif [ "${act:-0}" -gt 0 ]; then
    printf '  %sINTERCO SS7 : PARTIELLE%s - %s/%s AS actifs\n' "$Y$B" "$Z" "$act" "$tot"
    echo   "  les noeuds manquants : demon arrete, ASP en shutdown, ou adresse fausse."
    echo   "  sur le noeud : checks/diag-stp-operator.sh"
  else
    printf '  %sINTERCO SS7 : DOWN%s - aucun AS actif\n' "$R$B" "$Z"
    echo   "  a verifier, dans l ordre :"
    echo   "    - local-ip du hub (0.0.0.0 = multi-homing, voir plus haut)"
    echo   "    - les noeuds pointent-ils la bonne adresse ? (remote-ip)"
    echo   "    - leur ASP est-il en 'no shutdown' ?"
  fi
else
  printf '  %sindeterminable%s : le VTY du hub ne repond pas\n' "$R$B" "$Z"
fi
echo "-------------------------------------------------------------------------"
exit 0

checks/diag-stp-operator.sh

.sh 159 lignes - 7,5KB

checks/diag-stp-operator.sh
#!/bin/bash
# =============================================================================
# diag-stp-operator.sh -- diagnostic SS7/interco vu d'un noeud operateur
#
# Lit le role et le plan WAN, compare la conf sur disque a ce que le demon
# osmo-stp fait tourner reellement (VTY), teste le transport SCTP vers
# l'inter-STP, et rend un verdict clair : interco UP / DOWN + la cause.
#
# Ne modifie rien. Ne redemarre rien. Lecture seule.
#
# Usage : sudo ./diag-stp-operator.sh [--node N] [--hub-ip IP] [--no-color]
# =============================================================================
set -uo pipefail

ROLE_FILE=/etc/osmo-role
WAN_FILE=/etc/osmo-wan.conf
CONF_DIR=/etc/osmocom
STP_CFG="$CONF_DIR/osmo-stp.cfg"
MSC_CFG="$CONF_DIR/osmo-msc.cfg"
BSC_CFG="$CONF_DIR/osmo-bsc.cfg"
VTY_PORT=4239
M3UA_PORT=2908

NODE=""; HUB_IP=""; USE_COLOR=1
while [ $# -gt 0 ]; do
  case "$1" in
    --node) NODE="${2:-}"; shift ;;
    --hub-ip) HUB_IP="${2:-}"; shift ;;
    --no-color) USE_COLOR=0 ;;
    -h|--help) sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
  esac
  shift
done

if [ "$USE_COLOR" = 1 ] && [ -t 1 ]; then
  G=$'\033[0;32m'; R=$'\033[0;31m'; Y=$'\033[1;33m'; C=$'\033[0;36m'; B=$'\033[1m'; Z=$'\033[0m'
else G=; R=; Y=; C=; B=; Z=; fi
OK="${G}[ OK ]${Z}"; KO="${R}[FAIL]${Z}"; WARN="${Y}[WARN]${Z}"; NA="[ -- ]"
PROB=0
hr(){ printf '%s\n' "-------------------------------------------------------------------------"; }
head(){ printf '\n%s== %s ==%s\n' "$B" "$1" "$Z"; }

# ---- identite ---------------------------------------------------------------
getv(){ awk -F= -v k="$1" '$1==k{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' "$ROLE_FILE" 2>/dev/null; }
[ -z "$NODE" ]   && NODE="$(getv OSMO_WAN_NODE)"
[ -z "$HUB_IP" ] && HUB_IP="$(getv OSMO_HUB_IP)"
ROLE="$(getv OSMO_ROLE)"
[ -z "$NODE" ] && NODE="?"

# IP locale de ce noeud, depuis la table WAN si dispo
SELF_IP=""
if [ -r "$WAN_FILE" ]; then
  wl="$(awk -F\" '/WAN_NODES=/{print $2}' "$WAN_FILE" 2>/dev/null)"
  for tok in $wl; do
    id="${tok%%:*}"; rest="${tok#*:}"; ip="${rest%%:*}"
    [ "$id" = "$NODE" ] && SELF_IP="$ip"
  done
fi

head "IDENTITE"
printf '  hostname     : %s\n' "$(hostname)"
printf '  role         : %s%s%s   noeud : %s%s%s\n' "$C" "${ROLE:-?}" "$Z" "$C" "$NODE" "$Z"
printf '  inter-STP    : %s%s%s   ce noeud (WAN) : %s%s%s\n' "$C" "${HUB_IP:-?}" "$Z" "$C" "${SELF_IP:-?}" "$Z"
if [ "$ROLE" != "operator" ]; then
  printf '  %s ce n est pas un noeud operateur (role=%s) -- utilise diag-interstp.sh\n' "$WARN" "${ROLE:-?}"
fi

# plan attendu pour ce noeud
if [ "$NODE" != "?" ]; then
  EXP_PC="1.${NODE}1.2"; EXP_MSC="1.${NODE}1.1"; EXP_BSC="1.${NODE}1.3"
  EXP_RK=$(( NODE*1000 + 150 ))
else
  EXP_PC=""; EXP_MSC=""; EXP_BSC=""; EXP_RK=""
fi

# ---- conf sur disque --------------------------------------------------------
head "CONF SUR DISQUE ($STP_CFG)"
f_pc="$(awk '/^cs7 instance/{c=1} c&&/^ *point-code /{print $2; exit}' "$STP_CFG" 2>/dev/null)"
f_rip="$(awk '/asp asp-to-inter/{f=1} f&&/remote-ip/{print $2; exit}' "$STP_CFG" 2>/dev/null)"
f_lip="$(awk '/asp asp-to-inter/{f=1} f&&/local-ip/{print $2; exit}' "$STP_CFG" 2>/dev/null)"
f_rk="$(awk '/as as-inter/{f=1} f&&/routing-key/{print $2; exit}' "$STP_CFG" 2>/dev/null)"
f_shut="$(awk '/asp asp-to-inter/{f=1} f&&/^ *no shutdown/{print "no"; exit} f&&/^ *shutdown/{print "yes"; exit} f&&/^ *as /{exit}' "$STP_CFG" 2>/dev/null)"
m_pc="$(awk '/^cs7 instance/{c=1} c&&/^ *point-code /{print $2; exit}' "$MSC_CFG" 2>/dev/null)"
b_pc="$(awk '/^cs7 instance/{c=1} c&&/^ *point-code /{print $2; exit}' "$BSC_CFG" 2>/dev/null)"

chk(){ # label got exp
  local label="$1" got="$2" exp="$3" m="$OK"
  if [ -n "$exp" ] && [ "$got" != "$exp" ]; then m="$KO"; PROB=$((PROB+1)); fi
  if [ -z "$got" ]; then m="$WARN"; got="(vide)"; fi
  printf '  %-26s %-16s %s\n' "$label" "$got" "$m$( [ -n "$exp" ]&&[ "$got" != "$exp" ]&&printf ' attendu %s' "$exp")"
}
chk "point-code STP"        "$f_pc"  "$EXP_PC"
chk "asp remote-ip (hub)"   "$f_rip" "$HUB_IP"
chk "asp local-ip (self)"   "$f_lip" "$SELF_IP"
chk "routing-key inter"     "$f_rk"  "$EXP_RK"
if [ "$f_shut" = "no" ]; then printf '  %-26s %-16s %s\n' "asp administratif" "no shutdown" "$OK"
elif [ "$f_shut" = "yes" ]; then printf '  %-26s %-16s %s\n' "asp administratif" "shutdown" "$KO"; PROB=$((PROB+1))
else printf '  %-26s %-16s %s\n' "asp administratif" "(indetermine)" "$WARN"; fi
chk "point-code MSC"        "$m_pc"  "$EXP_MSC"
chk "point-code BSC"        "$b_pc"  "$EXP_BSC"

# ---- services ---------------------------------------------------------------
head "SERVICES (systemd)"
for s in osmo-stp osmo-msc osmo-bsc osmo-hlr; do
  st="$(systemctl is-active "$s" 2>/dev/null)"
  m="$OK"; [ "$st" = active ] || { m="$KO"; PROB=$((PROB+1)); }
  printf '  %-14s %-10s %s\n' "$s" "${st:-inconnu}" "$m"
done

# ---- VTY : ce que le demon fait tourner reellement --------------------------
vty(){ # commands...
  { printf 'enable\n'; for c in "$@"; do printf '%s\n' "$c"; done; sleep 0.5; } \
    | timeout 6 nc -q1 127.0.0.1 "$VTY_PORT" 2>/dev/null | tr -d '\r' | sed 's/\x1b\[[0-9;]*m//g'
}
head "VTY osmo-stp (etat RUNTIME, port $VTY_PORT)"
ASP="$(vty 'show cs7 instance 0 asp')"
if [ -z "$ASP" ]; then
  printf '  %s VTY injoignable -- osmo-stp est-il lance ?\n' "$KO"; PROB=$((PROB+1))
  r_state=""; r_rem=""
else
  line="$(printf '%s\n' "$ASP" | grep -E 'asp-to-inter' | head -1)"
  r_state="$(printf '%s' "$line" | awk '{print $3}')"
  r_rem="$(printf '%s' "$line" | awk '{print $8}')"
  printf '  asp-to-inter  etat=%s%s%s  remote=%s\n' "$C" "${r_state:-absent}" "$Z" "${r_rem:-?}"
  nact="$(printf '%s\n' "$ASP" | grep -c 'ASP_ACTIVE')"
  printf '  ASP locaux ACTIVE (MSC/HLR internes) : %s\n' "$nact"
  # derive interne : fichier corrige mais demon pas recharge
  if [ -n "$r_rem" ] && [ -n "$f_rip" ] && [ "$r_rem" != "${f_rip}:${M3UA_PORT}" ] && [ "${r_rem%:*}" != "$f_rip" ]; then
    printf '  %s DERIVE : disque remote=%s mais demon tourne remote=%s -> reload/restart requis\n' "$WARN" "$f_rip" "$r_rem"
    PROB=$((PROB+1))
  fi
fi

# ---- transport --------------------------------------------------------------
head "TRANSPORT vers inter-STP ${HUB_IP:-?}:$M3UA_PORT"
if [ -n "$HUB_IP" ]; then
  if ping -c1 -W2 "$HUB_IP" >/dev/null 2>&1; then printf '  %s ping %s\n' "$OK" "$HUB_IP"
  else printf '  %s ping %s injoignable\n' "$KO" "$HUB_IP"; PROB=$((PROB+1)); fi
fi
estab="$(ss -Sanp 2>/dev/null | grep -E "${HUB_IP//./\\.}:$M3UA_PORT" | grep -i estab)"
if [ -n "$estab" ]; then printf '  %s association SCTP ETABLIE\n' "$OK"; printf '    %s\n' "$estab"
else printf '  %s aucune association SCTP etablie vers le hub\n' "$KO"; fi

# ---- verdict ----------------------------------------------------------------
head "VERDICT"
if [ "$r_state" = "ASP_ACTIVE" ] && [ -n "$estab" ]; then
  printf '  %sINTERCO SS7 : UP%s  (asp-to-inter ACTIVE, SCTP etabli)\n' "$G$B" "$Z"
else
  printf '  %sINTERCO SS7 : DOWN%s\n' "$R$B" "$Z"
  echo   "  causes probables (par ordre) :"
  [ "$f_shut" = yes ] && echo "    - asp-to-inter en 'shutdown' dans la conf"
  { [ -n "$EXP_PC" ] && [ "$f_pc" != "$EXP_PC" ]; } && echo "    - point-code STP disque ($f_pc) != attendu ($EXP_PC)"
  { [ -n "$HUB_IP" ] && [ "$f_rip" != "$HUB_IP" ]; } && echo "    - remote-ip disque ($f_rip) != hub ($HUB_IP)"
  { [ -n "$r_rem" ] && [ "${r_rem%:*}" != "$f_rip" ]; } && echo "    - demon tourne une conf obsolete -> systemctl restart osmo-stp osmo-msc osmo-bsc"
  [ -z "$estab" ] && echo "    - pas de SCTP etabli (hub down, pare-feu, ou asp pas monte)"
  echo "  (rappel : ce script ne redemarre rien)"
fi
hr
exit 0

checks/global_check.sh

.sh 602 lignes - 26KB

checks/global_check.sh
#!/bin/bash
# global_check.sh - Verification complete de tous les composants Osmocom
#
# Verifie pour chaque operateur :
#   - STP (4239) : ASP, AS, routes
#   - HLR (4258) : connexions GSUP, abonnes
#   - MSC (4254) : ASP, SCCP, subscribers
#   - BSC (4242) : ASP, SCCP, BTS, OML/RSL
#   - BTS (4241) : etat des transceivers, LCHAN
#   - PCU (4240) : BTS, TBF, MS (si GPRS actif)
#   - SGSN (4245) : MM context, PDP context (si GPRS)
#   - GGSN (4260) : PDP context, APN (si GPRS)
#   - MGW (4243) : endpoints MGCP
#   - SIP connector (4255) : etat (si present)
#   - Services : SMS relay, MNCC socket, Asterisk
#
# DOUBLE MODE - docker ou natif
#   docker : chaque operateur est un conteneur osmo-operator-N ; on l'interroge
#            par "docker exec" ; l'inter-STP est le conteneur osmo-inter-stp.
#   natif  : ISO, VM ou machine nue ; un seul operateur, les demons tournent
#            ici, les VTY sont sur 127.0.0.1 (memes ports), les configs dans
#            /etc/osmocom.
# Le mode est DETECTE (checks/_mode.sh) et affiche en tete de sortie : un
# diagnostic qui ne dit pas dans quel monde il a regarde est inexploitable.
# --docker / --native forcent, quand la detection ne peut pas trancher (hote
# qui a docker installe mais fait tourner le lab en natif, par exemple).
#
# Usage :
#   sudo ./global_check.sh [--verbose] [--quick] [--op=N] [--docker|--native]

set -u

# ── Couleurs ──────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'

# ── Bibliotheque de mode (docker | natif) ─────────────────────────────────────
# Chemin RELATIF au script : le depot vit en /home/.../osmo-operator pendant le
# developpement et en /opt/GSM/osmo-operator sur l'ISO ; un chemin absolu casserait
# l'un des deux. Le second candidat couvre le cas d'un appel par lien
# symbolique, ou dirname ne pointe pas sur checks/.
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
    [ -r "$_c" ] && { . "$_c"; break; }
done
command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }

# ── Configuration ─────────────────────────────────────────────────────────────
VERBOSE=0
QUICK=0
SPECIFIC_OP=""
MODE_FORCED=0

# OSMO_MODE pose dans l'environnement est deja une forme de forcage
# ("OSMO_MODE=native ./checks/global_check.sh") : on le signale comme tel.
case "${OSMO_MODE:-}" in docker|native) MODE_FORCED=1 ;; esac

for arg in "$@"; do
    case "$arg" in
        --verbose|-v) VERBOSE=1 ;;
        --quick|-q)   QUICK=1 ;;
        --op=*)       SPECIFIC_OP="${arg#*=}" ;;
        # osmo_mode_force rend 2 si l'argument n'est pas un mode : ici il en est
        # toujours un, le "|| true" ne sert qu'a survivre a un futur set -e.
        --docker|--native|--mode=*) osmo_mode_force "$arg" && MODE_FORCED=1 || true ;;
    esac
done

MODE="$(osmo_mode)"

PASS=0
FAIL=0
WARN=0
SKIP=0

# ── Helpers ───────────────────────────────────────────────────────────────────
ok()   { echo -e "  ${GREEN}✓${NC} $*"; PASS=$((PASS+1)); }
fail() { echo -e "  ${RED}✗${NC} $*"; FAIL=$((FAIL+1)); }
warn() { echo -e "  ${YELLOW}⚠${NC} $*"; WARN=$((WARN+1)); }
skip() { echo -e "  ${YELLOW}-${NC} $*"; SKIP=$((SKIP+1)); }
info() { [[ $VERBOSE -eq 1 ]] && echo -e "    ${CYAN}│${NC} $*"; }

banner() {
    echo ""
    echo -e "${BOLD}$*${NC}"
    echo -e "$(printf '─%.0s' {1..70})"
}

section() {
    echo -e "\n  ${CYAN}${BOLD}$*${NC}"
}

# ── Fonction VTY ──────────────────────────────────────────────────────────────
# Meme signature qu'avant - premier argument : l'ETIQUETTE du noeud
# (osmo-operator-N), que osmo_vty traduit en "docker exec" ou en execution
# locale selon le mode. Les temporisations reproduisent a l'identique celles de
# l'ancienne implementation (0.5 / 2 / nc -q1) : les changer changerait la
# QUANTITE de sortie capturee, donc les "grep -c" qui la comptent plus bas.
vty_cmd() {
    local node="$1"
    local port="$2"
    local cmd="$3"
    local timeout="${4:-2}"

    local raw_output
    raw_output=$(OSMO_VTY_OPEN=0.5 OSMO_VTY_READ="$timeout" OSMO_VTY_Q=1 \
                 osmo_vty "$node" "$port" enable "$cmd" end) || return 1

    # Filtrage identique a l'ancien (motif et ordre des filtres) ; "Free
    # Software lives" en supplement, comme avant.
    printf '%s\n' "$raw_output" | osmo_vty_clean 'Free Software lives'
    return 0
}

# ── Verification rapide de disponibilite VTY ──────────────────────────────────
# En natif, osmo_vty_up sonde d'abord avec ss (passif, n'ouvre pas de session
# VTY) et ne retombe sur la connexion TCP que si ss ne voit rien.
vty_available() {
    osmo_vty_up "$1" "$2"
}

# ── Detection des operateurs ──────────────────────────────────────────────────
# docker : les conteneurs osmo-operator-N en cours.
# natif  : $OSMO_OP_IDS, sinon les netns osmo-opN, sinon N_OPERATORS de
#          globals.conf, sinon 1 - il n'existe pas d'inventaire en natif.
if [ -n "$SPECIFIC_OP" ]; then
    # osmo_op_exists est ANCRE, contrairement a l'ancien "docker ps | grep
    # osmo-operator-N" : "--op=1" acceptait aussi osmo-operator-10.
    if ! osmo_op_exists "$SPECIFIC_OP"; then
        echo -e "${RED}Operateur ${SPECIFIC_OP} non trouve (mode ${MODE})${NC}"
        exit 1
    fi
    OP_IDS=("$SPECIFIC_OP")
else
    mapfile -t OP_IDS < <(osmo_ops)
fi

N_OPS=${#OP_IDS[@]}

# Une ligne de l'encadre, remplie a la largeur exacte (58 colonnes). Le texte
# passe ici reste en ASCII : sous une locale C, ${#t} compterait les octets
# d'un caractere accentue et le cadre se decalerait.
box_line() {
    # Deux instructions, pas une : dans "local t="$1" pad=$(( ... ${#t} ))"
    # l'arithmetique est developpee AVANT l'affectation de t, ce qui donne
    # "variable sans liaison" sous set -u.
    local t="$1"
    local pad=$(( 58 - 5 - ${#t} ))
    [ "$pad" -lt 0 ] && pad=0
    printf "${CYAN}║     %s%*s║${NC}\n" "$t" "$pad" ""
}

echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║     Global Health Check - $(date '+%Y-%m-%d %H:%M:%S')            ║${NC}"
echo -e "${CYAN}║     ${N_OPS} operateur(s) a verifier                              ║${NC}"
# Le mode fait partie du diagnostic : sans lui, un "service DOWN" ne dit pas
# s'il a ete cherche dans un conteneur ou sur cette machine.
if [ "$MODE_FORCED" -eq 1 ]; then
    box_line "mode : ${MODE} (force)"
else
    box_line "mode : ${MODE} (detecte)"
fi
if [ "$MODE" = native ]; then
    box_line "natif : demons locaux, VTY sur 127.0.0.1"
else
    box_line "docker : conteneurs osmo-operator-N"
fi
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"

if [ "$N_OPS" -eq 0 ]; then
    # En natif osmo_ops rend toujours au moins "1" : ce cas est donc celui de
    # docker sans conteneur en cours. On le dit, plutot que de laisser croire a
    # une panne des services.
    echo -e "${RED}Aucun operateur trouve (mode ${MODE})${NC}"
    [ "$MODE" = docker ] && echo -e "     ${CYAN}aucun conteneur osmo-operator-N en cours - sudo ./start.sh${NC}"
    exit 1
fi

# ══════════════════════════════════════════════════════════════════════════════
# Pour chaque operateur
# ══════════════════════════════════════════════════════════════════════════════
for op_id in "${OP_IDS[@]}"; do
    # osmo_node rend "osmo-operator-N" dans les DEUX modes : en docker c'est
    # le nom du conteneur, en natif un nom d'affichage. La banniere et les
    # champs "container=" que relit operator_summary.sh sont preserves.
    container="$(osmo_node "$op_id")"
    banner "OPERATEUR ${op_id} (${container})"

    # =========================================================================
    # 1. STP (port 4239)
    # =========================================================================
    section "STP (Signalisation)"

    if ! vty_available "$container" 4239; then
        fail "STP VTY (4239) inaccessible - service DOWN"
    else
        # ASP locaux
        asp_out=$(vty_cmd "$container" 4239 "show cs7 instance 0 asp")
        n_asp=$(echo "$asp_out" | grep -c 'ASP_ACTIVE' || true)
        
        if [ "$n_asp" -ge 2 ]; then
            ok "ASP actifs : ${n_asp} (MSC+BSC ok)"
        elif [ "$n_asp" -eq 1 ]; then
            warn "ASP actifs : 1 seul (manque MSC ou BSC)"
        else
            fail "ASP actifs : aucun"
        fi

        # AS locaux
        as_out=$(vty_cmd "$container" 4239 "show cs7 instance 0 as all")
        n_as=$(echo "$as_out" | grep -c 'AS_ACTIVE' || true)
        
        if [ "$n_as" -ge 2 ]; then
            ok "AS actifs : ${n_as}"
        else
            warn "AS actifs : ${n_as}"
        fi

        # Routes (pas de PROHIB)
        route_out=$(vty_cmd "$container" 4239 "show cs7 instance 0 route")
        n_prohib=$(echo "$route_out" | grep -ciE 'prohib' || true)
        
        if [ "$n_prohib" -eq 0 ]; then
            ok "Routes : aucun PROHIB"
        else
            fail "Routes : ${n_prohib} PROHIB detectes"
        fi

        # Route par defaut vers inter-STP.
        # L'inter-STP est un conteneur en docker (osmo-inter-stp, 172.20.0.10).
        # En natif il n'existe que si CETTE machine tient ce role, ou s'il est
        # sur une autre machine du WAN : dans ce cas l'absence de route
        # catch-all n'est pas une anomalie, c'est la topologie. On l'ignore
        # explicitement au lieu de fabriquer un avertissement.
        has_default=$(echo "$route_out" | grep -cE '0\.0\.0/0.*avail' || true)
        if [ "$has_default" -gt 0 ]; then
            ok "Route par defaut → inter-STP : presente"
        elif osmo_is_native && ! osmo_hub >/dev/null 2>&1; then
            skip "Route par defaut → inter-STP - ignore (natif) : $(osmo_hub_hint)"
        else
            warn "Route par defaut → inter-STP : absente"
        fi
    fi

    # =========================================================================
    # 2. HLR (port 4258)
    # =========================================================================
    section "HLR (Abonnes)"

    if ! vty_available "$container" 4258; then
        fail "HLR VTY (4258) inaccessible - service DOWN"
    else
        # Connexions GSUP
        gsup_out=$(vty_cmd "$container" 4258 "show gsup-connections")
        vlr_conn=$(echo "$gsup_out" | grep -c "VLR" || true)
        smsc_conn=$(echo "$gsup_out" | grep -c "SMSC" || true)

        [ "$vlr_conn" -gt 0 ] && ok "GSUP : VLR connecte" || fail "GSUP : VLR absent"
        [ "$smsc_conn" -gt 0 ] && ok "GSUP : SMSC connecte" || warn "GSUP : SMSC absent"

        # Nombre d'abonnes
        sub_out=$(vty_cmd "$container" 4258 "show subscriber count")
        sub_count=$(echo "$sub_out" | grep -oE '[0-9]+' | head -1)
        info "Abonnes dans HLR : ${sub_count:-0}"
    fi

    # =========================================================================
    # 3. MSC (port 4254)
    # =========================================================================
    section "MSC (Commutation)"

    if ! vty_available "$container" 4254; then
        fail "MSC VTY (4254) inaccessible - service DOWN"
    else
        # ASP vers STP
        msc_asp=$(vty_cmd "$container" 4254 "show cs7 instance 0 asp")
        msc_active=$(echo "$msc_asp" | grep -c 'ASP_ACTIVE' || true)
        [ "$msc_active" -gt 0 ] && ok "MSC→STP : ASP_ACTIVE" || fail "MSC→STP : pas de ASP actif"

        # SCCP
        msc_sccp=$(vty_cmd "$container" 4254 "show cs7 instance 0 sccp users")
        if echo "$msc_sccp" | grep -qE 'SSN 254'; then
            ok "SCCP SSN 254 (MSC-A) enregistre"
        else
            fail "SCCP SSN 254 absent"
        fi

        # Subscribers VLR
        vlr_out=$(vty_cmd "$container" 4254 "show subscriber count")
        vlr_count=$(echo "$vlr_out" | grep -oE '[0-9]+' | head -1 || echo "0")
        info "Subscribers dans VLR : ${vlr_count}"
    fi

    # =========================================================================
    # 4. BSC (port 4242)
    # =========================================================================
    section "BSC (Controleur BTS)"

    if ! vty_available "$container" 4242; then
        fail "BSC VTY (4242) inaccessible - service DOWN"
    else
        # ASP vers STP
        bsc_asp=$(vty_cmd "$container" 4242 "show cs7 instance 0 asp")
        bsc_active=$(echo "$bsc_asp" | grep -c 'ASP_ACTIVE' || true)
        [ "$bsc_active" -gt 0 ] && ok "BSC→STP : ASP_ACTIVE" || fail "BSC→STP : pas de ASP actif"

        # SCCP
        bsc_sccp=$(vty_cmd "$container" 4242 "show cs7 instance 0 sccp users")
        echo "$bsc_sccp" | grep -qE 'SSN 254' && ok "SCCP SSN 254 (BSC) enregistre" || fail "SCCP SSN 254 absent"

        # BTS 0 etat
        bts_out=$(vty_cmd "$container" 4242 "show bts 0")
        
        if echo "$bts_out" | grep -qE "Oper 'Enabled'.*Avail 'OK'"; then
            ok "BTS 0 : Enabled / OK"
        elif echo "$bts_out" | grep -qE "Oper 'Enabled'"; then
            warn "BTS 0 : Enabled mais Avail != OK"
        else
            fail "BTS 0 : pas Enabled"
        fi

        # OML dans show bts 0
        oml_up=$(echo "$bts_out" | grep -c 'OML Link state: connected' || true)
        [ "$oml_up" -gt 0 ] && ok "OML : connecte" || fail "OML : deconnecte"

        # RSL : verifier dans show trx 0 0 (pas dans show bts 0)
        trx_out=$(vty_cmd "$container" 4242 "show trx 0 0")
        rsl_up=$(echo "$trx_out" | grep -c 'RSL State: connected' || true)
        
        if [ "$rsl_up" -gt 0 ]; then
            ok "RSL : connecte (TRX 0)"
        else
            # Fallback : verifier si c'est mentionne ailleurs
            rsl_alt=$(echo "$bts_out" | grep -c 'RSL' || true)
            if [ "$rsl_alt" -gt 0 ]; then
                warn "RSL : statut ambigu (verifier manuellement)"
            else
                fail "RSL : deconnecte"
            fi
        fi

        # Nombre de BTS
        n_bts=$(vty_cmd "$container" 4242 "show bts all" | grep -c '^bts' || true)
        info "BTS configurees : ${n_bts}"
    fi

    # =========================================================================
    # 5. BTS (Station de base)
    # =========================================================================
    section "BTS (Station de base)"

    if ! vty_available "$container" 4242; then
        skip "BSC VTY (4242) inaccessible - impossible de verifier les BTS"
    else
        # Verifier BTS 0 directement
        bts0_out=$(vty_cmd "$container" 4242 "show bts 0")
        
        if [ -z "$bts0_out" ]; then
            fail "Impossible d'obtenir les informations de BTS 0"
        else
            # Verifier que la BTS existe (presence du nom)
            if echo "$bts0_out" | grep -q "BTS 0 is of osmo-bts type"; then
                ok "BTS 0 : configuree"
                
                # Etat operationnel
                if echo "$bts0_out" | grep -q "Oper 'Enabled'.*Avail 'OK'"; then
                    ok "BTS 0 : operationnelle (Enabled/OK)"
                elif echo "$bts0_out" | grep -q "Oper 'Enabled'"; then
                    warn "BTS 0 : Enabled mais Avail != OK"
                else
                    fail "BTS 0 : pas Enabled"
                fi
                
                # OML
                if echo "$bts0_out" | grep -q "OML Link state: connected"; then
                    ok "OML : connecte"
                else
                    fail "OML : deconnecte"
                fi
                
                # RSL (present dans les statistiques en bas)
                if echo "$bts0_out" | grep -q "Number of RSL links connected.* 1"; then
                    ok "RSL : connecte"
                elif echo "$bts0_out" | grep -q "Number of RSL links connected.* [1-9]"; then
                    ok "RSL : connecte (${BASH_REMATCH[1]})"
                else
                    # Verifier via TRX
                    trx0_out=$(vty_cmd "$container" 4242 "show trx 0 0" 2>/dev/null)
                    if echo "$trx0_out" | grep -q "RSL State: connected"; then
                        ok "RSL : connecte (TRX 0)"
                    else
                        fail "RSL : deconnecte"
                    fi
                fi
                
                # ARFCN
                arfcn=$(echo "$bts0_out" | grep -oE 'ARFCNs: [0-9]+' | head -1)
                [ -n "$arfcn" ] && info "$arfcn"
                
                # Uptime
                uptime=$(echo "$bts0_out" | grep "Seconds of uptime" | grep -oE '[0-9]+ s')
                [ -n "$uptime" ] && info "Uptime : ${uptime}"
                
                # Verifier s'il y a d'autres BTS
                n_bts=$(vty_cmd "$container" 4242 "show bts all" | grep -c '^bts' || true)
                if [ "$n_bts" -gt 1 ]; then
                    ok "BTS multiples : ${n_bts} (BTS 0 OK)"
                fi
            else
                fail "BTS 0 non trouvee dans la configuration"
            fi
        fi
    fi
    # =========================================================================
    # 6. PCU (port 4240) - GPRS
    # =========================================================================
    section "PCU (Controleur paquets)"

    if ! vty_available "$container" 4240; then
        skip "PCU VTY (4240) - non disponible (GPRS peut-etre desactive)"
    else
        # BTS
        pcu_bts=$(vty_cmd "$container" 4240 "show bts all")
        if [ -n "$pcu_bts" ]; then
            ok "PCU : connecte aux BTS"
        else
            warn "PCU : aucune BTS"
        fi

        # TBF
        tbf_out=$(vty_cmd "$container" 4240 "show tbf all")
        tbf_count=$(echo "$tbf_out" | grep -c 'TBF' || true)
        info "TBF actifs : ${tbf_count}"
    fi

    # =========================================================================
    # 7. SGSN (port 4245) - Core paquets
    # =========================================================================
    section "SGSN (Serving GPRS)"

    if ! vty_available "$container" 4245; then
        skip "SGSN VTY (4245) - non disponible (GPRS peut-etre desactive)"
    else
        # MM context
        mm_out=$(vty_cmd "$container" 4245 "show mm-context all")
        mm_count=$(echo "$mm_out" | grep -c 'IMSI' || true)
        info "MM contexts : ${mm_count}"

        # PDP context
        pdp_out=$(vty_cmd "$container" 4245 "show pdp-context all")
        pdp_count=$(echo "$pdp_out" | grep -c 'PDP' || true)
        info "PDP contexts : ${pdp_count}"

        # NS entities
        ns_out=$(vty_cmd "$container" 4245 "show ns entities")
        ns_count=$(echo "$ns_out" | grep -c 'NSVC' || true)
        [ "$ns_count" -gt 0 ] && ok "NS entities : ${ns_count}" || warn "NS entities : 0"
    fi

    # =========================================================================
    # 8. GGSN (port 4260) - Gateway GPRS
    # =========================================================================
    section "GGSN (Gateway GPRS)"

    if ! vty_available "$container" 4260; then
        skip "GGSN VTY (4260) - non disponible (GPRS peut-etre desactive)"
    else
        # PDP contexts
        ggsn_pdp=$(vty_cmd "$container" 4260 "show pdp-context all")
        ggsn_count=$(echo "$ggsn_pdp" | grep -c 'PDP' || true)
        info "PDP contexts GGSN : ${ggsn_count}"

        # APN
        apn_out=$(vty_cmd "$container" 4260 "show apn all")
        apn_count=$(echo "$apn_out" | grep -c 'APN' || true)
        [ "$apn_count" -gt 0 ] && ok "APN configures : ${apn_count}" || warn "APN configures : 0"
    fi

    # =========================================================================
    # 9. MGW (port 4243) - Media Gateway
    # =========================================================================
    section "MGW (Media Gateway)"

    if ! vty_available "$container" 4243; then
        fail "MGW VTY (4243) inaccessible"
    else
        # Endpoints
        ep_out=$(vty_cmd "$container" 4243 "show mgcp endpoint all")
        ep_count=$(echo "$ep_out" | grep -c 'Endpoint' || true)
        info "Endpoints MGCP : ${ep_count}"

        # Connexions actives
        conn_out=$(vty_cmd "$container" 4243 "show mgcp stats")
        if echo "$conn_out" | grep -qE "active calls.*[1-9]"; then
            ok "MGW : appels actifs"
        else
            info "MGW : aucun appel actif"
        fi
    fi

    # =========================================================================
    # 10. SIP connector (port 4255)
    # =========================================================================
    section "SIP connector (Voix)"

    if ! vty_available "$container" 4255; then
        skip "SIP connector VTY (4255) - non disponible"
    else
        ok "SIP connector : accessible"
        # Pas beaucoup de commandes show utiles dans osmo-sip-connector
    fi

    # =========================================================================
    # 11. Services
    # =========================================================================
    section "Services"

    # MNCC socket - osmo_sock interroge ss AVANT "test -S" : sous PrivateTmp
    # (unit systemd de l'ISO) le socket existe sans etre visible par test -S, et
    # un "voix impossible" mensonger en decoulerait.
    if osmo_sock "$container" /tmp/msc_mncc; then
        ok "MNCC socket present (MSC ↔ SIP connector)"
    else
        fail "MNCC socket absent - voix impossible"
    fi

    # SMS relay - awk sur le port exact, et non "grep -c :7890" qui aurait
    # aussi accepte 17890.
    if osmo_port "$container" 7890 tcp; then
        ok "SMS relay : ecoute sur port 7890"
    else
        warn "SMS relay : non detecte"
    fi

    # Asterisk
    if osmo_running "$container" asterisk; then
        ok "Asterisk : en cours d'execution"

        # Verification rapide Asterisk
        ast_out=$(osmo_ast "$container" "core show channels" || true)
        channels=$(echo "$ast_out" | grep -c "active channels" || true)
        info "Asterisk : ${channels} canaux actifs"
    else
        warn "Asterisk : non detecte"
    fi

    # Verification des processus cles
    section "Processus"

    # En natif, osmo_running essaie systemctl PUIS pgrep -x PUIS pgrep -f : les
    # demons sont des units sur l'ISO mais des processus detaches quand systemd
    # est absent, et /proc/PID/comm est tronque a 15 caracteres ("pgrep -x
    # osmo-sip-connector" ne trouve jamais rien).
    if [ "$MODE" = native ] && [ "$N_OPS" -gt 1 ]; then
        info "natif multi-operateur : ip netns exec n'isole pas les PID, ces etats portent sur la machine"
    fi

    for proc in osmo-stp osmo-hlr osmo-msc osmo-bsc osmo-bts-trx osmo-mgw osmo-sip-connector; do
        if osmo_running "$container" "$proc"; then
            ok "$proc : en cours"
        else
            # Certains processus peuvent ne pas etre presents selon configuration
            skip "$proc : non trouve"
        fi
    done

    echo ""
done

# ══════════════════════════════════════════════════════════════════════════════
# RESUME GLOBAL
# ══════════════════════════════════════════════════════════════════════════════
echo ""
echo -e "$(printf '═%.0s' {1..70})"
echo -e "  ${BOLD}RESUME GLOBAL${NC}"
echo -e "$(printf '═%.0s' {1..70})"

TOTAL=$((PASS + FAIL + WARN + SKIP))

echo -e "  ${CYAN}mode   : ${MODE}${NC}  (${N_OPS} operateur(s))"
echo -e "  ${GREEN}✓ PASS : ${PASS}${NC}"
echo -e "  ${RED}✗ FAIL : ${FAIL}${NC}"
echo -e "  ${YELLOW}⚠ WARN : ${WARN}${NC}"
echo -e "  ${YELLOW}- SKIP : ${SKIP}${NC}"
echo -e "$(printf '─%.0s' {1..70})"

if [ "$FAIL" -eq 0 ] && [ "$WARN" -eq 0 ]; then
    echo -e "  ${GREEN}${BOLD}TOUT EST OK${NC}  -  Tous les composants fonctionnent"
elif [ "$FAIL" -eq 0 ]; then
    echo -e "  ${YELLOW}${BOLD}FONCTIONNEL AVEC AVERTISSEMENTS${NC}  -  Verifier les warnings"
else
    echo -e "  ${RED}${BOLD}PROBLEMES DETECTES${NC}  -  ${FAIL} test(s) en echec"
fi
echo -e "$(printf '═%.0s' {1..70})"
echo ""

exit "$FAIL"

checks/_mode.sh

.sh 867 lignes - 39KB

checks/_mode.sh
# =============================================================================
#  checks/_mode.sh - une seule regle de detection docker/natif, pour tous
#                    les scripts de checks/
#  Bibliotheque : a SOURCER, jamais a executer dans une boucle de tests.
# =============================================================================
#
#  POURQUOI CE FICHIER EXISTE
#  --------------------------
#  Le lab vit dans deux mondes, et les checks doivent dire la verite dans les
#  deux :
#
#    docker - un conteneur osmo-operator-N par operateur sur le reseau
#             gsm-inter (172.20.0.0/24), le hub SS7 est le conteneur
#             osmo-inter-stp (172.20.0.10) ; on atteint tout par "docker exec".
#    natif  - ISO, VM ou machine nue : un seul operateur (sauf topologie netns),
#             tout tourne ici, les VTY sont sur 127.0.0.1, les configs dans
#             /etc/osmocom, les demons sont des units systemd ou des processus.
#
#  Les PORTS VTY sont les MEMES dans les deux mondes - seul l'hote change :
#      osmo-stp 4239 · osmo-msc 4254 · osmo-bsc 4242 · osmo-hlr 4258
#      osmo-sgsn 4245 · osmo-ggsn 4260 · osmo-mgw 4243 · mobile/BB 4247
#
#  Avant ce fichier, la meme detection etait recopiee dans cinq scripts, avec
#  cinq variantes : deux d'entre elles employaient "docker ps | grep -aq", qui
#  rend 141 sous "set -o pipefail" (grep sort au premier match, le producteur
#  prend un SIGPIPE) - l'inter-STP etait alors declare absent alors qu'il
#  tournait, de facon intermittente. Une seule regle, ecrite une fois, corrigee
#  une fois.
#
#  CONTRAT
#  -------
#   • le fichier ne definit QUE des fonctions et des variables ": =" ;
#     il n'affiche rien, ne pose aucune couleur, ne compte aucun PASS/FAIL :
#     chaque check garde ses ok/fail/warn/skip, donc son format de sortie et
#     son code de retour ;
#   • les sondes rendent 0 = vrai, non-0 = faux ;
#   • sourcable deux fois sans effet de bord (garde OSMO_MODE_LIB_LOADED) ;
#   • aucun "grep -aq" en fin de pipeline, aucun "awk ... exit" au milieu d'un
#     tube : sous "set -o pipefail" ces deux formes rendent 141 ;
#   • compatible "set -euo pipefail" chez l'appelant.
#
#  DEUX QUESTIONS DIFFERENTES - POURQUOI start-direct.sh GARDE SA DETECTION
#  ----------------------------------------------------------------------
#  start-direct.sh:159 a sa propre detect_runtime_env(), et elle N'EST PAS un
#  doublon de osmo_mode() : les deux fonctions repondent a deux questions qui
#  n'ont pas la meme reponse.
#
#    detect_runtime_env()  "ou est-ce que JE tourne ?"  → docker | vm | bare
#        Question de l'INTERIEUR du noeud. Elle decide du PLAN D'ADRESSAGE :
#        en docker l'inter-STP est 172.20.0.10 et le noeud a deja son IP ; en
#        VM/ISO l'inter-STP est une autre machine (192.168.1.49 sur le banc, ou une host-only selon le montage) et l'adresse
#        arrive par DHCP, donc peut manquer au lancement.
#
#    osmo_mode()           "comment j'ATTEINS les noeuds d'ici ?" → docker | native
#        Question de PILOTAGE. Elle decide entre "docker exec" et "executer
#        ici".
#
#  Elles se CONTREDISENT volontairement dans un cas, et c'est la preuve qu'il ne
#  faut pas les fusionner : DANS un conteneur du depot, detect_runtime_env rend
#  "docker" (le plan d'adressage est bien celui de docker) alors qu'osmo_mode
#  rend "native" - car "docker exec" vers soi-meme n'a aucun sens, meme
#  quand le socket est monte. Un script qui prendrait l'une pour l'autre
#  s'enverrait des commandes a lui-meme par le demon docker.
#
#  Ce qui EST partage, en revanche, c'est la convention aval : start-direct.sh
#  transmet "--$NODE_MODE" (--docker / --native) a network/setup-wan-mesh.sh
#  (start-direct.sh:756), et c'est exactement ce que consomme osmo_mode_force().
#  Les deux vocabulaires coincident ; seules les fonctions restent distinctes.
#
#  DESIGNER UN NOEUD - <op>
#  -----------------------
#  Toutes les fonctions d'execution prennent en premier argument un <op>, qui
#  accepte indifferemment :
#      3                    le numero d'operateur (forme normale)
#      osmo-operator-3      l'etiquette (ce que rendent osmo_ops/osmo_node)
#      hub | osmo-inter-stp le hub SS7
#  En docker c'est le nom du conteneur ; en natif c'est cette machine, avec le
#  prefixe "ip netns exec osmo-opN" si et seulement si l'espace de noms
#  existe (topologie multi-operateur native, run_modules/12-netns.sh).
#
#  L'ETIQUETTE osmo-operator-N EST CONSERVEE EN NATIF. Ce n'est pas une
#  coquetterie : checks/vty-debug-dump.sh ecrit "container=osmo-operator-N"
#  dans son dump, et checks/operator_summary.sh y accroche quinze extractions.
#  L'etiquette est une cle de correlation, pas un objet interrogeable.
#
#  SOURCING (le meme bloc partout ; marche depuis le depot et depuis /opt) :
#      _here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
#      for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
#          [ -r "$_c" ] && { . "$_c"; break; }
#      done
#      command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }
# =============================================================================

[ -n "${OSMO_MODE_LIB_LOADED:-}" ] && return 0
OSMO_MODE_LIB_LOADED=1

# Racine du depot : ce fichier est dans checks/, la racine est un cran au-dessus.
# Sert a retrouver globals.conf ; surchargeable pour les tests.
_osmo_lib_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
: "${OSMO_REPO:=$(dirname "$_osmo_lib_here")}"
unset _osmo_lib_here

# Plan d'adressage de la dorsale inter-operateurs. La MEME valeur sert aux deux
# mondes : docker la pose avec "--network gsm-inter --ip", le natif
# multi-operateur la recree en netns (BACKBONE_NET, run_modules/12-netns.sh:47).
: "${OSMO_BACKBONE_NET:=172.20.0}"
: "${OSMO_NETNS_PREFIX:=${NETNS_PREFIX:-osmo-op}}"

# Prefixe de racine, mode natif seulement : permet de viser un arbre de test
# (OSMO_NATIVE_ROOT=/tmp/essai) au lieu du /etc de la machine. Vide en
# production. Meme role que NATIVE_ROOT dans network/setup-wan-mesh.sh.
: "${OSMO_NATIVE_ROOT:=}"

# Memo "quel client VTY ce noeud possede-t-il ?", pour ne pas relancer un
# "command -v nc" a chaque commande VTY (global_check en passe des dizaines).
# "-g" : la bibliotheque doit rester utilisable meme sourcee DEPUIS une
# fonction, ou un "declare" nu creerait une variable locale qui disparaitrait
# au retour.
declare -gA _OSMO_VTY_TOOL 2>/dev/null || declare -A _OSMO_VTY_TOOL

# =============================================================================
#  1. MODE
# =============================================================================

# osmo_in_container - tourne-t-on DANS un conteneur DE CE DEPOT ?
# /.dockerenv seul serait vrai dans n'importe quel conteneur ; on exige aussi
# /etc/docker-entrypoint-cmd, depose par scripts/entrypoint.sh. C'est le couple
# qu'emploient start.sh:20 et start-direct.sh:159.
osmo_in_container() {
    [ -f /.dockerenv ] && [ -f /etc/docker-entrypoint-cmd ]
}

# Detection, du plus explicite au plus deduit.
_osmo_mode_detect() {
    # Dans un conteneur du depot, on EST le noeud : "docker exec" vers
    # soi-meme n'a pas de sens, meme si le socket docker est monte (c'est le cas
    # du conteneur du dashboard). C'est la regle de start-direct.sh:237.
    if osmo_in_container; then printf 'native\n'; return 0; fi

    # OSMO_NATIVE=1 est deja pose par l'ISO (services/osmo-egprs-web.service).
    if [ "${OSMO_NATIVE:-}" = "1" ]; then printf 'native\n'; return 0; fi

    # LA regle du depot (checks/wan_ss7_check.sh:88, network/setup-wan-mesh.sh:122) :
    # on ne se fie pas a la presence du binaire docker - l'hote du lab l'a, une
    # machine de dev aussi - mais a celle d'un conteneur operateur EN COURS.
    # On capture la liste AVANT de la filtrer : "docker ps | grep -aq" rendrait
    # 141 sous pipefail.
    if command -v docker >/dev/null 2>&1; then
        local names
        names="$(docker ps --format '{{.Names}}' 2>/dev/null || true)"
        if grep -ax 'osmo-operator-1' >/dev/null 2>&1 <<<"$names"; then
            printf 'docker\n'; return 0
        fi
    fi
    printf 'native\n'
}

# osmo_mode - ecrit "docker" ou "native". Memoise dans OSMO_MODE, qui fait
# aussi office d'entree : "OSMO_MODE=native ./checks/global_check.sh" force.
osmo_mode() {
    case "${OSMO_MODE:-}" in
        docker|native) : ;;
        *) OSMO_MODE="$(_osmo_mode_detect)" ;;
    esac
    printf '%s\n' "$OSMO_MODE"
}

osmo_is_docker() { [ "$(osmo_mode)" = docker ]; }
osmo_is_native() { [ "$(osmo_mode)" = native ]; }

# osmo_mode_force <docker|native|--docker|--native|--mode=X>
# A glisser tel quel dans la boucle "case" d'analyse des options de chaque
# script : l'explicite gagne toujours sur la detection. Rend 2 sur un argument
# qui n'est pas un mode (l'appelant sait alors que l'option ne le concerne pas).
osmo_mode_force() {
    local m="${1:-}"
    case "$m" in
        --mode=*) m="${m#*=}" ;;
        --docker) m=docker ;;
        --native) m=native ;;
    esac
    case "$m" in
        docker|native) OSMO_MODE="$m"; return 0 ;;
        *) return 2 ;;
    esac
}

# =============================================================================
#  2. INVENTAIRE
# =============================================================================

# Lit une cle de globals.conf ("N_OPERATORS=3"). On lit le fichier avec awk
# directement : pas de tube, donc pas de piege pipefail.
_osmo_globals() {
    local k="${1:-}" f v
    for f in "${OSMO_GLOBALS:-}" "$OSMO_REPO/globals.conf" \
             /opt/GSM/osmo-operator/globals.conf ./globals.conf; do
        [ -n "$f" ] && [ -r "$f" ] || continue
        v="$(awk -F= -v k="$k" '$1 == k { gsub(/[" \r\t]/, "", $2); if ($2 != "") print $2 }' "$f" | tail -1)"
        [ -n "$v" ] && { printf '%s\n' "$v"; return 0; }
    done
    return 1
}

# osmo_ops - identifiants des operateurs presents, un par ligne, tries.
#   docker : les conteneurs osmo-operator-N en cours.
#   natif  : $OSMO_OP_IDS (pose par services/osmo-egprs-web.service), puis les
#            netns osmo-opN (multi-operateur natif), puis N_OPERATORS de
#            globals.conf, sinon 1. Aucun inventaire n'existe en natif : la vie
#            d'un noeud se deduit des demons, pas d'une liste de conteneurs.
# ══ LA TOPOLOGIE MULTI-OPERATEUR ═════════════════════════════════════════════
# [2026-08-31] Ces checks supposaient UN SEUL mode pour tout le banc :
# osmo_mode() rend "docker" ou "native", et osmo_exec/osmo_ops/osmo_hub_ip s en
# servent globalement. C etait vrai tant qu une machine portait soit des
# conteneurs, soit un operateur natif.
#
# Le banc multi-operateur est MIXTE : op 1 natif sur l hote, op 2 et 3 en
# conteneurs, hub en conteneur. Le mode global tranchait donc pour tout le
# monde, et le check partait interroger un "osmo-operator-1" qui n existe plus,
# au point code 1.23.2 qui n est celui de personne - d ou des "MSC VTY 4254
# inaccessible" qui ne disaient rien de l etat reel du banc, juste qu on
# sondait la mauvaise adresse.
#
# /etc/osmocom/osmo-multi.conf est ecrit par addition.sh et fait autorite :
#   MULTI_OPS="1:native::1.1.2:150 2:docker:172.20.0.12:1.2.2:250 ..."
#   MULTI_HUB_IP=172.20.0.10  MULTI_HUB_NAME=osmo-inter-stp
# On le lit quand il existe ; sinon RIEN ne change, l ancienne detection reste.
OSMO_MULTI_CONF="${OSMO_MULTI_CONF:-/etc/osmocom/osmo-multi.conf}"
_OSMO_MULTI_LOADED=""

_osmo_multi_load() {
    [ -n "$_OSMO_MULTI_LOADED" ] && return "$_OSMO_MULTI_LOADED"
    _OSMO_MULTI_LOADED=1                      # 1 = pas de topologie
    [ -r "$OSMO_MULTI_CONF" ] || return 1
    # shellcheck disable=SC1090
    . "$OSMO_MULTI_CONF" 2>/dev/null || return 1
    [ -n "${MULTI_OPS:-}" ] || return 1
    _OSMO_MULTI_LOADED=0
    return 0
}

# osmo_multi_active - rc 0 si une topologie multi-operateur est declaree.
osmo_multi_active() { _osmo_multi_load; }

# osmo_op_mode <id> - "native" ou "docker" POUR CET OPERATEUR.
# Sans topologie, retombe sur le mode global : le comportement d avant.
osmo_op_mode() {
    local want="${1:-}" spec idx mode
    if _osmo_multi_load; then
        for spec in $MULTI_OPS; do
            IFS=: read -r idx mode _ <<< "$spec"
            [ "$idx" = "$want" ] && { printf '%s\n' "$mode"; return 0; }
        done
    fi
    osmo_mode
}

# osmo_hub_pc - point code du hub, depuis la topologie ; vide sinon.
osmo_hub_pc() {
    _osmo_multi_load || return 1
    [ -n "${MULTI_HUB_PC:-}" ] || return 1
    printf '%s\n' "$MULTI_HUB_PC"
}

# osmo_op_pc <id> - point code attendu, depuis la topologie ; vide sinon.
osmo_op_pc() {
    local want="${1:-}" spec idx mode ip pc
    _osmo_multi_load || return 1
    for spec in $MULTI_OPS; do
        IFS=: read -r idx mode ip pc _ <<< "$spec"
        [ "$idx" = "$want" ] && { printf '%s\n' "$pc"; return 0; }
    done
    return 1
}

osmo_ops() {
    local ids=""
    # La topologie declaree prime sur toute detection : elle enumere les
    # operateurs ATTENDUS, natif compris - un operateur arrete doit apparaitre
    # dans le bilan comme manquant, pas disparaitre de la liste.
    if _osmo_multi_load; then
        ids="$(tr ' ' '\n' <<<"$MULTI_OPS" | cut -d: -f1 | sed -n 's/^\([0-9][0-9]*\)$/\1/p')"
        [ -n "$ids" ] && { sort -n -u <<<"$ids" | sed '/^$/d'; return 0; }
    fi
    if [ "$(osmo_mode)" = docker ]; then
        local names
        names="$(docker ps --format '{{.Names}}' 2>/dev/null || true)"
        ids="$(sed -n 's/^osmo-operator-\([0-9][0-9]*\)$/\1/p' <<<"$names")"
    else
        ids="$(tr ', ' '\n\n' <<<"${OSMO_OP_IDS:-}" | sed -n 's/^\([0-9][0-9]*\)$/\1/p')"
        if [ -z "$ids" ]; then
            ids="$(ip netns list 2>/dev/null | awk '{print $1}' \
                   | sed -n "s/^${OSMO_NETNS_PREFIX}\([0-9][0-9]*\)\$/\1/p")"
        fi
        if [ -z "$ids" ]; then
            local n; n="$(_osmo_globals N_OPERATORS || true)"
            case "$n" in ''|*[!0-9]*) n=1 ;; esac
            [ "$n" -ge 1 ] 2>/dev/null || n=1
            ids="$(seq 1 "$n")"
        fi
    fi
    [ -n "$ids" ] || return 0
    sort -n -u <<<"$ids" | sed '/^$/d'
}

# osmo_op_exists <id> - l'operateur existe-t-il ?
# Remplace "docker ps | grep osmo-operator-N" de global_check.sh:131, dont le
# motif n'etait pas ancre : "--op=1" y acceptait aussi osmo-operator-10.
osmo_op_exists() {
    local want="${1:-}" id
    case "$want" in ''|*[!0-9]*) return 2 ;; esac
    while read -r id; do
        [ "$id" = "$want" ] && return 0
    done <<<"$(osmo_ops)"
    return 1
}

# osmo_node <op> - etiquette du noeud : "osmo-operator-N" ou "osmo-inter-stp".
# En docker c'est le nom du conteneur ; en natif c'est un NOM D'AFFICHAGE, et
# c'est ce qui preserve mot pour mot la banniere "OPERATEUR 1 (osmo-operator-1)"
# et le champ "container=" du dump.
osmo_node() {
    case "${1:-}" in
        hub|osmo-inter-stp) printf 'osmo-inter-stp\n' ;;
        osmo-operator-*)    printf '%s\n' "$1" ;;
        [0-9]*)             printf 'osmo-operator-%s\n' "$1" ;;
        *)                  printf '%s\n' "${1:-}" ;;
    esac
}

# Numero d'operateur d'un <op> ; vide pour le hub ou pour l'inconnu.
_osmo_id() {
    case "${1:-}" in
        hub|osmo-inter-stp) printf '' ;;
        osmo-operator-*)    printf '%s\n' "${1##*-}" ;;
        [0-9]*)             printf '%s\n' "$1" ;;
        *)                  printf '' ;;
    esac
}

# osmo_hub - un inter-STP est-il interrogeable ICI ? Ecrit son etiquette si oui.
#   docker : le conteneur osmo-inter-stp tourne.
#   natif  : cette machine EST le hub - OSMO_ROLE=interstp dans /etc/osmo-role
#            (pose par build-iso.sh), ou la sonde de start-interstp.sh:100.
# Sur un noeud operateur natif le hub est une AUTRE machine : rc 1, et c'est
# osmo_hub_hint qui dit quoi faire. Un "0 AS actif" fabrique se lirait comme
# une panne.
osmo_hub() {
    if [ "$(osmo_mode)" = docker ]; then
        local names
        names="$(docker ps --format '{{.Names}}' 2>/dev/null || true)"
        grep -ax 'osmo-inter-stp' >/dev/null 2>&1 <<<"$names" || return 1
        printf 'osmo-inter-stp\n'; return 0
    fi
    if [ -r /etc/osmo-role ] \
       && grep -aE '^OSMO_ROLE[[:space:]]*=[[:space:]]*interstp' /etc/osmo-role >/dev/null 2>&1; then
        printf 'osmo-inter-stp\n'; return 0
    fi
    if pgrep -f 'osmo-stp .*osmo-stp-interop\.cfg' >/dev/null 2>&1; then
        printf 'osmo-inter-stp\n'; return 0
    fi
    return 1
}

# OSMO_HUB_M3UA_PORT - port M3UA du hub inter-STP (helpers/create_interop.sh
# ecrit "listen m3ua 2908"). Les STP operateur, eux, ecoutent sur 2905.
: "${OSMO_HUB_M3UA_PORT:=2908}"

# _osmo_hub_ip_probe - demande aux noeuds operateur ou est leur hub. Deux
# sources, dans cet ordre :
#   1. OSMO_HUB_IP, injecte dans chaque conteneur par start.sh (-e OSMO_HUB_IP)
#      des que --hub-ip / --wan designe un inter-STP hors dorsale docker ;
#   2. le "remote-ip" de l'asp asp-to-inter dans /etc/osmocom/osmo-stp.cfg, qui
#      reste vrai meme sans environnement (conteneur relance a la main, ISO).
# Rend 1 sans rien ecrire si aucun noeud ne sait repondre.
_osmo_hub_ip_probe() {
    local op v=""
    for op in $(osmo_ops 2>/dev/null); do
        v="$(osmo_exec "$op" printenv OSMO_HUB_IP 2>/dev/null | tr -d "\r" | tail -1)"
        [ -n "$v" ] && { printf '%s\n' "$v"; return 0; }
        v="$(osmo_exec "$op" cat /etc/osmocom/osmo-stp.cfg 2>/dev/null \
             | awk '/asp asp-to-inter/{f=1} f&&/remote-ip/{print $2; exit}' | tr -d "\r")"
        [ -n "$v" ] && { printf '%s\n' "$v"; return 0; }
    done
    return 1
}

# osmo_hub_ip - adresse du hub. Rien a dire → rc 1.
#   docker + hub local  : la dorsale, 172.20.0.10 ;
#   docker sans hub local : l'adresse WAN vue par les operateurs (--hub-ip) ;
#   natif : OSMO_HUB_IP de /etc/osmo-role (idiome de wan_ss7_check.sh:193),
#           puis, a defaut, la meme sonde par les noeuds.
# Le resultat est mis en cache : la sonde coute un "docker exec" par noeud, et
# les checks appellent cette fonction plusieurs fois par passage.
_OSMO_HUB_IP_CACHE=""
osmo_hub_ip() {
    if [ -n "$_OSMO_HUB_IP_CACHE" ]; then
        [ "$_OSMO_HUB_IP_CACHE" = "-" ] && return 1
        printf '%s\n' "$_OSMO_HUB_IP_CACHE"; return 0
    fi
    local v=""
    # MULTI_HUB_IP fait autorite : sur un banc mixte, deduire l adresse du hub
    # du mode global menait a la mauvaise (la boucle locale pour un natif,
    # alors que le hub vit dans un conteneur).
    if _osmo_multi_load && [ -n "${MULTI_HUB_IP:-}" ]; then
        _OSMO_HUB_IP_CACHE="$MULTI_HUB_IP"
        printf '%s\n' "$MULTI_HUB_IP"; return 0
    fi
    if [ "$(osmo_mode)" = docker ]; then
        if osmo_hub >/dev/null 2>&1; then
            printf '%s.10\n' "$OSMO_BACKBONE_NET"; return 0
        fi
    else
        [ -r /etc/osmo-role ] && \
            v="$(awk -F= '/^OSMO_HUB_IP=/ { gsub(/[ \r\t]/, "", $2); print $2 }' /etc/osmo-role 2>/dev/null | tail -1)"
    fi
    [ -n "$v" ] || v="$(_osmo_hub_ip_probe 2>/dev/null || true)"
    if [ -z "$v" ]; then _OSMO_HUB_IP_CACHE="-"; return 1; fi
    _OSMO_HUB_IP_CACHE="$v"
    printf '%s\n' "$v"
}

# osmo_hub_is_remote - rc 0 (et ecrit l'IP) quand un hub est CONFIGURE mais
# vit AILLEURS : c'est le cas WAN (--hub-ip / --wan), ou le noeud operateur
# natif. Une adresse de dorsale docker ou de boucle locale sans conteneur hub
# ne compte pas : la, le hub n'est pas distant, il est simplement absent.
osmo_hub_is_remote() {
    local ip
    osmo_hub >/dev/null 2>&1 && return 1
    ip="$(osmo_hub_ip 2>/dev/null)" || return 1
    [ -n "$ip" ] || return 1
    case "$ip" in
        "${OSMO_BACKBONE_NET}."*|127.*|localhost) return 1 ;;
    esac
    printf '%s\n' "$ip"
}

# osmo_hub_assoc [ip] [port] - combien de noeuds locaux ont une association
# SCTP ETABLIE vers le hub ? Le lien inter-STP est du M3UA sur SCTP : une
# sonde TCP ("nc -z" sur 2908) rend "Connection refused" meme quand le lien
# est parfaitement monte. C'est "ss --sctp" qui dit la verite, et il la dit
# depuis le NOEUD, pas depuis l'hote : le conteneur a sa propre pile reseau.
# Ecrit "etablies/total" ; rc 0 si au moins une, 1 si aucune, 2 si la sonde
# n'est pas praticable (ni ss, ni support --sctp, ni noeud interrogeable).
osmo_hub_assoc() {
    local ip="${1:-}" port="${2:-$OSMO_HUB_M3UA_PORT}" op out probed=0 n=0 tot=0 pat
    [ -n "$ip" ] || ip="$(osmo_hub_ip 2>/dev/null || true)"
    [ -n "$ip" ] || return 2
    pat="${ip//./\\.}:${port}"
    for op in $(osmo_ops 2>/dev/null); do
        tot=$((tot+1))
        out="$(osmo_exec "$op" ss -an --sctp 2>/dev/null || true)"
        [ -n "$out" ] || continue
        probed=1
        grep -aqE "ESTAB.*[[:space:]]${pat}([[:space:]]|$)" <<<"$out" && n=$((n+1))
    done
    [ "$probed" -eq 1 ] || return 2
    printf '%s/%s\n' "$n" "$tot"
    [ "$n" -gt 0 ]
}

# osmo_hub_hint - phrase prete pour skip(), quand le hub existe mais est
# ailleurs. Le VTY Osmocom n'ecoute que sur 127.0.0.1 (aucune conf du depot ne
# pose de "bind" sous "line vty") : depuis un noeud operateur, l'etat des AS
# et des ASP du hub n'est PAS observable. La commande citee existe deja.
osmo_hub_hint() {
    local ip
    ip="$(osmo_hub_ip 2>/dev/null || true)"
    if [ -n "$ip" ]; then
        printf 'hub distant %s : VTY 4239 non joignable depuis ce noeud - lancez ./start-interstp.sh --status sur le hub\n' "$ip"
    else
        printf 'aucun inter-STP local, et aucune adresse de hub connue ici (OSMO_HUB_IP, --hub-ip) - hub non interrogeable\n'
    fi
}

# =============================================================================
#  3. EXECUTION SUR UN NOEUD
# =============================================================================

# Prefixe netns du mode natif. Vide en mono-operateur : la dorsale et les
# espaces de noms ne sont crees que si N_OPERATORS > 1.
_osmo_ns_cmd() {
    local id ns
    id="$(_osmo_id "${1:-}")"
    [ -n "$id" ] || return 0
    ns="${OSMO_NETNS_PREFIX}${id}"
    if [ -e "/run/netns/$ns" ] || [ -e "/var/run/netns/$ns" ]; then
        printf 'ip netns exec %s' "$ns"
    fi
}

# osmo_exec <op> <argv...> - substitution 1:1 de "docker exec "$container" ...".
osmo_exec() {
    local op="${1:-}"; shift || return 2
    [ $# -ge 1 ] || return 2
    # Le mode se decide operateur par operateur : sur un banc mixte, le natif
    # s interroge en local pendant que ses voisins passent par docker exec.
    if [ "$(osmo_op_mode "$(_osmo_id "$op")")" = docker ]; then
        local c; c="$(osmo_node "$op")"
        [ -n "$c" ] || return 2
        docker exec "$c" "$@"
        return $?
    fi
    local ns; ns="$(_osmo_ns_cmd "$op")"
    # shellcheck disable=SC2086  # ns doit etre decoupe en mots (ip netns exec X)
    if [ -n "$ns" ]; then $ns "$@"; else "$@"; fi
}

# Idem, mais l'entree standard est transmise au noeud ("docker exec -i").
# Interne : reserve au dialogue VTY, ou le tube est l'entree de nc/telnet.
_osmo_exec_in() {
    local op="${1:-}"; shift || return 2
    if [ "$(osmo_op_mode "$(_osmo_id "$op")")" = docker ]; then
        docker exec -i "$(osmo_node "$op")" "$@"
        return $?
    fi
    local ns; ns="$(_osmo_ns_cmd "$op")"
    # shellcheck disable=SC2086
    if [ -n "$ns" ]; then $ns "$@"; else "$@"; fi
}

# osmo_cat <op> <chemin> - contenu d'un fichier du noeud (conf, marqueur...).
osmo_cat() {
    local op="${1:-}" path="${2:-}"
    [ -n "$path" ] || return 2
    if [ "$(osmo_mode)" = docker ]; then
        docker exec "$(osmo_node "$op")" cat "$path" 2>/dev/null
    else
        cat "${OSMO_NATIVE_ROOT}${path}" 2>/dev/null
    fi
}

# osmo_ast <op> <commande CLI> - asterisk -rx, identique dans les deux mondes.
osmo_ast() {
    local op="${1:-}" cli="${2:-}"
    [ -n "$cli" ] || return 2
    osmo_exec "$op" asterisk -rx "$cli" 2>/dev/null
}

# osmo_running <op> <demon> - ce demon tourne-t-il sur ce noeud ?
#
# En natif il faut les DEUX formes : sur l'ISO les demons sont des units
# (services/osmo-bts-trx.service...) mais core_svc_start retombe sur un lancement
# detache quand systemd est absent - auquel cas seul pgrep repond.
#
# Le repli "pgrep -f" n'est pas decoratif : /proc/PID/comm est TRONQUE a 15
# caracteres, donc "pgrep -ax osmo-sip-connector" (18) ne trouve jamais rien,
# quand bien meme le demon tourne. On retente alors sur la ligne de commande.
#
# Limite assumee du natif multi-operateur : "ip netns exec" n'isole pas les
# PID, la reponse porte donc sur la machine, pas sur l'operateur.
osmo_running() {
    local op="${1:-}" name="${2:-}"
    [ -n "$name" ] || return 2
    if [ "$(osmo_mode)" = docker ]; then
        osmo_exec "$op" pgrep -ax "$name" >/dev/null 2>&1 && return 0
        osmo_exec "$op" pgrep -f "(^|/)${name}([[:space:]]|\$)" >/dev/null 2>&1
        return $?
    fi
    if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
        systemctl is-active --quiet "$name" 2>/dev/null && return 0
    fi
    pgrep -ax "$name" >/dev/null 2>&1 && return 0
    pgrep -f "(^|/)${name}([[:space:]]|\$)" >/dev/null 2>&1
}

# osmo_sock <op> <chemin> - socket UNIX en ecoute ?
# L'ORDRE EST OBLIGATOIRE : on interroge ss AVANT le test de fichier. Sous
# PrivateTmp, /tmp/msc_mncc EXISTE sans etre visible par "test -S" (constate,
# run_modules/13-msc.sh:14) ; un "test -S" seul donnerait un "voix
# impossible" mensonger.
# awk lit TOUTE son entree (pas d'"exit" au milieu du tube) : sous pipefail,
# un awk qui sort tot ferait mourir ss en SIGPIPE et rendrait 141.
osmo_sock() {
    local op="${1:-}" p="${2:-}" out
    [ -n "$p" ] || return 2
    out="$(osmo_exec "$op" ss -xlH 2>/dev/null || true)"
    awk -v w="$p" '{ for (i = 1; i <= NF; i++) if ($i == w) f = 1 } END { exit !f }' <<<"$out" && return 0
    osmo_exec "$op" test -S "$p" 2>/dev/null
}

# osmo_port <op> <port> [tcp|udp|sctp] - ce port est-il en ecoute sur le noeud ?
# Remplace le "ss -tlnp | grep -ac ':7890'" de global_check.sh:493 : pas de
# tube fragile, et ":7890" ne peut plus se confondre avec 17890 ou 78901.
osmo_port() {
    local op="${1:-}" port="${2:-}" proto="${3:-tcp}" out
    local -a opts
    case "$port" in ''|*[!0-9]*) return 2 ;; esac
    case "$proto" in
        tcp)  opts=(-H -ltn) ;;
        udp)  opts=(-H -lun) ;;
        sctp) opts=(-H -ln --sctp) ;;
        *)    return 2 ;;
    esac
    out="$(osmo_exec "$op" ss "${opts[@]}" 2>/dev/null || true)"
    # Le 4e champ de ss est "adresse:port" local, pour tcp, udp et sctp.
    awk -v want="$port" '
        { a = $4
          if (a == "" || match(a, /:[0-9]+$/) == 0) next
          if (substr(a, RSTART + 1) == want) f = 1 }
        END { exit !f }' <<<"$out"
}

# osmo_host <op> - l'hote a joindre pour ce noeud DEPUIS ICI (relais SMS, SIP,
# M3UA...). Ce n'est PAS l'hote du VTY : celui-ci est toujours 127.0.0.1 vu de
# l'interieur du noeud, et osmo_vty s'en charge.
#   docker           : l'IP du conteneur sur la dorsale (172.20.0.1N, .10 = hub)
#   natif + netns    : la meme dorsale, recreee en netns
#   natif mono-op    : 127.0.0.1 - il n'y a ni pont gsm-inter ni 172.20.0.0/24
#   hub natif        : OSMO_HUB_IP de /etc/osmo-role, sinon 127.0.0.1
osmo_host() {
    local op="${1:-}" id
    case "$op" in
        hub|osmo-inter-stp)
            if [ "$(osmo_mode)" = docker ]; then
                printf '%s.10\n' "$OSMO_BACKBONE_NET"
            else
                local h; h="$(osmo_hub_ip 2>/dev/null || true)"
                printf '%s\n' "${h:-127.0.0.1}"
            fi
            return 0 ;;
    esac
    id="$(_osmo_id "$op")"
    if [ -n "$id" ] && { [ "$(osmo_mode)" = docker ] || [ -n "$(_osmo_ns_cmd "$op")" ]; }; then
        printf '%s.%s\n' "$OSMO_BACKBONE_NET" "$((10 + id))"
    else
        printf '127.0.0.1\n'
    fi
}

# =============================================================================
#  4. VTY
# =============================================================================

# Quel client VTY ce noeud possede-t-il ? nc de preference, telnet en repli.
# L'ISO installe netcat-openbsd ET telnet (build-iso.sh:693) mais PAS socat :
# ne pas s'appuyer sur socat comme le fait core_vty_ask de run_modules/.
# Le diagnostic est ecrit UNE FOIS par noeud, sur stderr, pour ne pas polluer la
# sortie du check ni la repeter a chaque commande.
_osmo_vty_tool() {
    local op="${1:-}" key tool
    key="$(osmo_node "$op")"
    tool="${_OSMO_VTY_TOOL[$key]:-}"
    if [ -z "$tool" ]; then
        if osmo_exec "$op" sh -c 'command -v nc' >/dev/null 2>&1; then
            tool=nc
        elif osmo_exec "$op" sh -c 'command -v telnet' >/dev/null 2>&1; then
            tool=telnet
        else
            tool=none
            printf 'checks/_mode.sh : aucun client VTY sur %s - installez netcat-openbsd ou telnet (apt-get install -y netcat-openbsd telnet)\n' \
                   "$key" >&2
        fi
        _OSMO_VTY_TOOL[$key]="$tool"
    fi
    [ "$tool" = none ] && return 1
    printf '%s\n' "$tool"
}

# osmo_vty_up <op> <port> - le VTY repond-il ?
# Remplace les cinq copies de "docker exec ... echo >/dev/tcp/127.0.0.1/PORT".
# En natif on essaie d'abord la sonde PASSIVE (ss) : elle n'ouvre ni ne referme
# de session VTY. Si elle ne voit rien (ss absent, /proc restreint), on retombe
# sur la sonde active - pas de faux negatif.
osmo_vty_up() {
    local op="${1:-}" port="${2:-}"
    case "$port" in ''|*[!0-9]*) return 2 ;; esac
    if [ "$(osmo_mode)" != docker ] && command -v ss >/dev/null 2>&1; then
        osmo_port "$op" "$port" tcp && return 0
    fi
    osmo_exec "$op" bash -c "echo >/dev/tcp/127.0.0.1/${port}" 2>/dev/null
}

# osmo_vty <op> <port> <commande>... - envoie les commandes au VTY, rend la
# sortie BRUTE sur stdout (banniere et invite comprises : c'est osmo_vty_clean
# qui filtre, et les scripts n'ont pas tous le meme filtre).
#
# Rend 1 SANS RIEN ECRIRE si le VTY ne repond pas, ou si le noeud n'a ni nc ni
# telnet : c'est le contrat des vty_cmd() actuelles, que les checks utilisent
# pour distinguer "service muet" de "service sans reponse a cette commande".
# La sonde prealable est celle d'osmo_vty_up (une connexion TCP ouverte puis
# refermee, comme aujourd'hui).
#
# L'appelant fournit ses propres "enable" / "end" : la bibliotheque
# n'invente aucune commande.
#
# LES TEMPORISATIONS SONT DES PARAMETRES, par variables d'environnement, parce
# que chaque script a les siennes et que les changer changerait la QUANTITE de
# sortie capturee - donc les "grep -ac" qui la comptent :
#     OSMO_VTY_OPEN  attente avant d'ecrire (0.5 ; ss7_check : 1)
#     OSMO_VTY_READ  attente de lecture     (2   ; ss7_check : 1.5)
#     OSMO_VTY_Q     nc -q<N>               (1   ; vty-debug-dump : 2)
# Le VTY Osmocom ignore ce qui arrive avant la fin de sa negociation telnet :
# ces sleep cadencent le DIALOGUE, ce ne sont pas des barrieres d'attente.
osmo_vty() {
    local op="${1:-}" port="${2:-}"
    [ $# -ge 3 ] || return 2
    shift 2
    case "$port" in ''|*[!0-9]*) return 2 ;; esac

    osmo_vty_up "$op" "$port" || return 1

    local body="" c
    for c in "$@"; do body="${body}${c}"$'\n'; done

    local tool
    tool="$(_osmo_vty_tool "$op")" || return 1

    local open="${OSMO_VTY_OPEN:-0.5}" read_s="${OSMO_VTY_READ:-2}" q="${OSMO_VTY_Q:-1}"

    # nc/telnet ferment souvent avant d'avoir tout lu : le sous-shell producteur
    # prend alors un SIGPIPE et le tube rend 141. "|| true" garde la fonction
    # utilisable sous "set -euo pipefail".
    if [ "$tool" = nc ]; then
        { sleep "$open"; printf '%s' "$body"; sleep "$read_s"; } \
            | _osmo_exec_in "$op" nc "-q${q}" 127.0.0.1 "$port" 2>/dev/null || true
    else
        { sleep "$open"; printf '%s' "$body"; sleep "$read_s"; } \
            | _osmo_exec_in "$op" telnet 127.0.0.1 "$port" 2>/dev/null || true
    fi
    return 0
}

# osmo_vty_clean [motif supplementaire] - filtre stdin→stdout : retire banniere,
# invite et lignes vides. Le motif de base et L'ORDRE des filtres sont ceux des
# scripts, au caractere pres : y toucher changerait leur sortie.
# global_check.sh et ss7_check.sh filtrent en plus "Free Software lives", que
# vty-debug-dump.sh garde - d'ou l'argument optionnel :
#     osmo_vty_clean 'Free Software lives'
osmo_vty_clean() {
    local extra="${1:-}"
    local re="^(Trying|Connected|Escape|Welcome|OsmoSTP|OsmoHLR|OsmoMSC|\
OsmoBSC|OsmoBTS|OsmoMGW|OsmoPCU|OsmoSGSN|OsmoGGSN|OsmoSIP|\
VTY server|Use.*help|Press.*tab|[A-Za-z0-9_-]+[#>] |\
Enter password|% Unknown|% Command incomplete|% Error|\
Connection closed)"
    [ -n "$extra" ] && re="${re%)}|${extra})"
    grep -avE "$re" | sed 's/\r//' | grep -av '^[[:space:]]*$' || true
}

# osmo_vty_interactive <op> <port> [hote] - ouvre une session VTY INTERACTIVE
# (un humain devant le clavier), la ou osmo_vty envoie des commandes et rend la
# sortie. Substitution 1:1 de "docker exec -ti <conteneur> telnet <ip> <port>".
# L'hote reste un PARAMETRE parce que les groupes Baseband ecoutent sur
# 127.0.0.1, 127.0.0.2... (tools/vty-menu.sh:79) : c'est la seule cible VTY du
# depot qui ne soit pas 127.0.0.1.
osmo_vty_interactive() {
    local op="${1:-}" port="${2:-}" host="${3:-127.0.0.1}"
    case "$port" in ''|*[!0-9]*) return 2 ;; esac
    if [ "$(osmo_mode)" = docker ]; then
        docker exec -ti "$(osmo_node "$op")" telnet "$host" "$port"
        return $?
    fi
    local ns; ns="$(_osmo_ns_cmd "$op")"
    # shellcheck disable=SC2086  # ns doit etre decoupe en mots (ip netns exec X)
    if [ -n "$ns" ]; then $ns telnet "$host" "$port"; else telnet "$host" "$port"; fi
}

# osmo_vty_target <op> - "cible" a remettre a un client VTY qui N'EST PAS du
# bash et ne peut donc pas sourcer cette bibliotheque.
#
# Le cas existe et il est unique : tools/vty-connect.exp est ecrit en expect. La
# bibliotheque ne l'atteindra jamais ; le mode doit donc etre decide cote bash
# (tools/vty-menu.sh) et lui etre passe en ARGUMENT. La convention est :
#     docker → le nom du conteneur, qui devient "docker exec -ti <nom> telnet"
#     natif  → "-", qui devient un "telnet" direct
# "-" plutot que la chaine vide : une chaine vide se perd dans un [lindex]
# expect comme dans un "$@" bash, et l'on ne distingue plus "natif" de
# "argument oublie".
osmo_vty_target() {
    if [ "$(osmo_mode)" = docker ]; then osmo_node "${1:-}"; else printf -- '-\n'; fi
}

# =============================================================================
#  5. CATEGORIE A - REFUSER PROPREMENT QUAND DOCKER MANQUE
# =============================================================================
#
#  Certains scripts ne se convertissent pas : ils PILOTENT docker par nature -
#  ils construisent une image, creent un reseau 172.20.N.0/24, lancent un
#  conteneur. En natif il n'y a ni image, ni reseau a creer, ni conteneur : le
#  noeud, c'est la machine. Les convertir reviendrait a reecrire start-direct.sh
#  et start-interstp.sh, qui existent deja - le depot a deja paye ce prix
#  ailleurs (cf. l'en-tete de start-nitb.sh : 4 copies divergentes de
#  si_bridge.py, 3 de gapk-start.sh).
#
#  Ce qui leur manque, c'est un REFUS. build-iso.sh embarque start.sh,
#  build.sh et launch/osmo-launch.sh sur une ISO ou build-iso.sh:707 garantit
#  qu'il n'y a PAS de docker : aujourd'hui ces scripts y meurent en 127 sur
#  "docker: command not found", apres avoir deja touche l'hote, sans nommer
#  l'alternative qui, elle, est installee juste a cote.

# osmo_docker_ok - docker est-il REELLEMENT utilisable depuis ici ?
# Le binaire ne suffit pas : une machine de dev l'a souvent sans demon actif, et
# "docker build" y echoue alors sur un message de socket. On interroge le
# demon. Aucun tube : pas de piege pipefail.
osmo_docker_ok() {
    command -v docker >/dev/null 2>&1 || return 1
    docker info >/dev/null 2>&1
}

# osmo_require_docker [ligne d'alternative...] - refus propre, puis exit 3.
# Rend 0 (et ne dit rien) si docker est utilisable : s'appelle donc en tete de
# script sans condition.
#
# Le message suit le gabarit de launch/start-oqc.sh:41-51, le meilleur du
# depot : dire CE QUI MANQUE, POURQUOI ce script ne peut pas s'en passer, et
# SURTOUT quoi lancer a la place, par son nom exact. Sans les alternatives
# fournies en argument, on cite les deux du depot.
#
# Sortie sur stderr, code 3 : distinct de 1 (echec d'execution) et de 2 (mauvais
# argument), pour qu'un appelant puisse reconnaitre "mauvais monde" sans lire
# le texte.
osmo_require_docker() {
    osmo_docker_ok && return 0
    {
        printf '\n%s\n\n' "Docker n'est pas utilisable sur cette machine."
        if command -v docker >/dev/null 2>&1; then
            printf '%s\n' "  Le binaire docker est la, mais le demon ne repond pas."
            printf '%s\n' "  → sudo systemctl start docker, puis relancer ce script."
        else
            printf '%s\n' "  La commande docker est absente - c'est le cas normal sur l'ISO,"
            printf '%s\n' "  en VM et sur une machine nue : la pile y est installee en dur."
        fi
        printf '\n%s\n' "Ce script pilote docker (images, reseaux, conteneurs). Il n'a pas"
        printf '%s\n' "d'equivalent natif a ecrire : l'equivalent EXISTE DEJA, et il a un nom."
        printf '%s\n\n' "Lancez plutot :"
        if [ $# -gt 0 ]; then
            local _l; for _l in "$@"; do printf '  %s\n' "$_l"; done
        else
            printf '  %s\n' "./start-direct.sh     - la pile complete de CE noeud (un operateur)"
            printf '  %s\n' "./start-interstp.sh   - le hub SS7 inter-operateurs"
        fi
        printf '\n%s\n' "Sur l'ISO ces deux-la sont deja dans le PATH : osmo-start-direct."
        printf '%s\n\n' "Pour verifier l'etat sans rien demarrer : ./checks/global_check.sh"
    } >&2
    exit 3
}

# =============================================================================
#  6. DIVERS
# =============================================================================

# osmo_qgrep <args...> - "grep -aq" sans le piege.
# "cmd | grep -aq motif" : grep sort a la PREMIERE correspondance, le
# producteur recoit un SIGPIPE et meurt en 141 - et sous "set -o pipefail"
# c'est 141 que rend le tube. "trouve" se lit alors "pas trouve", de facon
# intermittente, selon que la sortie tenait dans le tampon du tube. Ici grep lit
# toute son entree et on jette sa sortie. Copie conforme de qgrep()
# (network/setup-wan-mesh.sh:69).
osmo_qgrep() { grep "$@" >/dev/null; }

# =============================================================================
#  Execute au lieu d'etre source : on ne fait rien de destructeur, on affiche
#  ce que la bibliotheque VOIT. Sert de test rapide, et evite qu'un futur
#  "for f in checks/*.sh; do $f; done" ne tombe dans le vide.
# =============================================================================
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    printf 'mode      : %s\n' "$(osmo_mode)"
    printf 'operateurs: %s\n' "$(osmo_ops | tr '\n' ' ')"
    if hub="$(osmo_hub)"; then
        printf 'inter-STP : %s (interrogeable ici)\n' "$hub"
    else
        printf 'inter-STP : %s\n' "$(osmo_hub_hint)"
    fi
    printf 'hote op.1 : %s\n' "$(osmo_host 1)"
    exit 0
fi

checks/operator_summary.sh

.sh 485 lignes - 21KB

checks/operator_summary.sh
#!/bin/bash
# operator_summary.sh - Resume des caracteristiques principales de chaque operateur
#
# Analyse le dump produit par checks/vty-debug-dump.sh et en extrait, operateur
# par operateur, les quelques chiffres qui disent si le reseau vit : ASP actifs,
# routes SS7, abonnes HLR/VLR, etat des BTS, contextes PDP, appels en cours.
#
# DOUBLE MODE (docker / natif)
# ----------------------------
# Ce script ne parle a AUCUN demon : il lit un fichier. Le mode ne change donc
# pas ses quinze extractions - il change trois choses, et il faut les dire :
#
#   1. la GENERATION du dump (--live) : le generateur doit regarder le MEME
#      monde que nous, sinon on analyse le dump d'un autre lab. Le mode lui est
#      transmis par OSMO_MODE, y compris quand il a ete force ici.
#
#   2. l'INTER-STP : en docker c'est le conteneur osmo-inter-stp ; en natif
#      c'est soit cette machine (role interstp), soit une AUTRE machine - et
#      dans ce dernier cas son VTY n'est pas joignable d'ici, donc le dump n'en
#      contient rien. La section est alors IGNOREE, explicitement : un
#      "0 AS actif" fabrique se lirait comme une panne du coeur SS7.
#
#   3. l'etiquette "container=osmo-operator-N" du dump : en natif elle reste
#      la meme (checks/_mode.sh, osmo_node). C'est une CLE DE CORRELATION entre
#      les blocs du dump, pas un objet docker interrogeable - on ne la renomme
#      donc pas, sous peine de casser toutes les extractions ci-dessous.
#
# Le mode retenu est affiche en tete : un resume qui ne dit pas dans quel monde
# il a regarde n'est pas exploitable.
#
# Usage :
#   sudo ./operator_summary.sh [--dump=/chemin/dump.txt] [--live]
#                              [--docker|--native]

set -euo pipefail

# ── Bibliotheque commune de detection docker/natif ────────────────────────────
# Chemin relatif a CE fichier (jamais absolu) : le depot vit dans /home/... en
# developpement et dans /opt/GSM/osmo-operator sur l'ISO.
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
    [ -r "$_c" ] && { . "$_c"; break; }
done
command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }

# ── Couleurs ──────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'

# ── Configuration ─────────────────────────────────────────────────────────────
DUMP_FILE=""
LIVE_MODE=0
TEMP_DUMP=""

# OSMO_MODE herite de l'environnement vaut choix explicite, au meme titre que
# --docker/--native : on le signale comme "force" dans l'en-tete.
MODE_FORCED=0
[ -n "${OSMO_MODE:-}" ] && MODE_FORCED=1

for arg in "$@"; do
    case "$arg" in
        --dump=*) DUMP_FILE="${arg#*=}" ;;
        --live)   LIVE_MODE=1 ;;
        --docker|--native|--mode=*)
            # osmo_mode_force rend 2 si l'argument n'est pas un mode : seul
            # "--mode=n'importe-quoi" peut encore echouer ici.
            if osmo_mode_force "$arg"; then
                MODE_FORCED=1
            else
                echo "Mode inconnu : ${arg} (attendu : --docker, --native)" >&2
                exit 1
            fi
            ;;
        --help|-h)
            echo "Usage: $0 [--dump=/chemin/dump.txt] [--live] [--docker|--native]"
            echo "  --dump   : Utilise un fichier de dump existant"
            echo "  --live   : Genere un dump live avant l'analyse"
            echo "  --docker : Force le mode conteneurs (osmo-operator-N)"
            echo "  --native : Force le mode natif (ISO, VM, machine nue)"
            echo "             sans option, le mode est detecte automatiquement"
            exit 0
            ;;
    esac
done

MODE="$(osmo_mode)"
if [ "$MODE_FORCED" -eq 1 ]; then MODE_SRC="force"; else MODE_SRC="detecte"; fi

# ── Helper d'affichage ────────────────────────────────────────────────────────
print_header() {
    echo ""
    echo -e "${BOLD}$1${NC}"
    echo -e "$(printf '─%.0s' {1..70})"
}

print_row() {
    printf "  %-20s : %s\n" "$1" "$2"
}

print_section() {
    echo -e "\n  ${CYAN}${BOLD}$1${NC}"
}

# Ligne de la boite d'en-tete, calee sur les 58 colonnes du filet ╔══╗.
box_line() {
    local txt="$1" pad
    pad=$(( 58 - 5 - ${#txt} ))
    [ "$pad" -ge 0 ] || pad=0
    echo -e "${CYAN}║     ${txt}$(printf '%*s' "$pad" '')║${NC}"
}

# ── Extraction de donnees depuis le dump ──────────────────────────────────────
#
# PIEGE PIPEFAIL, valable pour TOUTES les extractions de ce fichier : sous
# "set -o pipefail", un grep qui ne trouve rien (rc 1) ou un "head -1" qui
# sort tot (le producteur meurt en SIGPIPE, rc 141) fait echouer le tube entier,
# donc l'affectation "var=$(...)", donc - sous "set -e" - le script lui-meme.
# Sur un dump vide ou partiel (mode natif ou tout est absent, dump produit
# ailleurs) le resume s'interrompait sans un mot. D'ou les "|| true" : ils ne
# masquent aucune erreur, ils disent "rien trouve = zero", ce que wc et grep -c
# ont deja ecrit sur leur sortie.

extract_value() {
    local container="$1"
    local pattern="$2"
    local default="${3:-?}"

    grep -A20 "\[.*\] port=.* container=${container}" "$DUMP_FILE" \
        | grep -E "$pattern" \
        | head -1 \
        | sed -E 's/^[[:space:]]+//' \
        || echo "$default"
}

extract_count() {
    local container="$1"
    local section="$2"
    local pattern="$3"

    grep -A50 "\[.*\] port=.* container=${container}.*$section" "$DUMP_FILE" \
        | grep -E "$pattern" \
        | wc -l \
        | tr -d ' ' \
        || true
}

extract_subscriber_count() {
    local container="$1"
    local count

    count=$(grep -A20 "\[OsmoHLR\]" "$DUMP_FILE" \
        | grep -A5 "container=${container}" \
        | grep -E "subscriber count.*[0-9]+" \
        | grep -oE '[0-9]+' \
        | head -1 || true)

    echo "${count:-0}"
}

# Renseigne BTS_STATE, OML_OK et RSL_OK - des variables GLOBALES, a dessein.
# La version precedente ecrivait l'etat sur stdout ET posait OML_OK/RSL_OK dans
# la foulee ; appelee en "$( )" elle tournait dans un SOUS-SHELL, ou ces deux
# affectations mouraient avec lui : les lignes OML et RSL affichaient
# "✗ deconnecte" meme quand le dump montrait des liens connectes. On renseigne
# donc les trois au meme endroit, sans substitution de commande.
extract_bts_info() {
    local container="$1"
    local bts_id="$2"
    local bts_block oper avail oml rsl

    bts_block=$(grep -A50 "\[OsmoBSC\]" "$DUMP_FILE" \
        | grep -A30 "container=${container}" \
        | grep -A20 "bts ${bts_id}" \
        | sed -n '/bts [0-9]/,/^$/p' || true)

    oper=$(echo "$bts_block" | grep -c "Oper 'Enabled'" || true)
    avail=$(echo "$bts_block" | grep -c "Avail 'OK'" || true)
    oml=$(echo "$bts_block" | grep -c "OML Link state: connected" || true)
    rsl=$(echo "$bts_block" | grep -c "RSL State: connected" || true)

    if [ "$oper" -gt 0 ] && [ "$avail" -gt 0 ]; then
        BTS_STATE="OK"
    elif [ "$oper" -gt 0 ]; then
        BTS_STATE="Enabled"
    else
        BTS_STATE="Down"
    fi

    # Renseigne aussi OML/RSL pour info
    [ "$oml" -gt 0 ] && OML_OK=1 || OML_OK=0
    [ "$rsl" -gt 0 ] && RSL_OK=1 || RSL_OK=0
}

extract_asp_state() {
    local container="$1"
    local component="$2"  # STP, MSC, BSC

    local asp_block=""
    if [ "$component" = "STP" ]; then
        asp_block=$(grep -A30 "\[OsmoSTP\]" "$DUMP_FILE" \
            | grep -A20 "container=${container}" || true)
    elif [ "$component" = "MSC" ]; then
        asp_block=$(grep -A30 "\[OsmoMSC\]" "$DUMP_FILE" \
            | grep -A20 "container=${container}" || true)
    elif [ "$component" = "BSC" ]; then
        asp_block=$(grep -A30 "\[OsmoBSC\]" "$DUMP_FILE" \
            | grep -A20 "container=${container}" || true)
    fi

    echo "$asp_block" | grep -c "ASP_ACTIVE" || true
}

# ── Generation d'un dump live si demande ──────────────────────────────────────
if [ "$LIVE_MODE" -eq 1 ]; then
    echo -e "${CYAN}Generation d'un dump live (mode ${MODE})...${NC}"
    TEMP_DUMP="/tmp/vty-live-$$.txt"

    # Le generateur est cherche A COTE DE NOUS (BASH_SOURCE), plus dans le
    # repertoire courant : "./vty-debug-dump.sh" n'existait que si l'on avait
    # fait "cd checks/" au prealable, ce qui n'est pas le cas depuis l'ISO.
    DUMPER="$_here/vty-debug-dump.sh"
    [ -x "$DUMPER" ] || DUMPER="./vty-debug-dump.sh"

    # OSMO_MODE est transmis explicitement : le generateur doit interroger le
    # meme monde que celui qu'on analysera, meme si --native/--docker a force
    # un mode qu'il n'aurait pas detecte seul.
    if ! OSMO_MODE="$MODE" "$DUMPER" --out "$TEMP_DUMP" > /dev/null; then
        echo -e "${RED}Echec de la generation du dump (${DUMPER}, mode ${MODE}).${NC}" >&2
        if [ "$MODE" = native ]; then
            echo "  Mode natif : verifiez que les demons tournent (VTY 4239/4254/4242/4258...)." >&2
        else
            echo "  Mode docker : verifiez que les conteneurs osmo-operator-N sont en cours." >&2
        fi
        rm -f "$TEMP_DUMP"
        exit 1
    fi
    DUMP_FILE="$TEMP_DUMP"
    echo -e "${GREEN}✓ Dump genere : $DUMP_FILE${NC}"
fi

if [ -z "$DUMP_FILE" ] || [ ! -f "$DUMP_FILE" ]; then
    echo -e "${RED}Erreur : fichier de dump non specifie ou introuvable${NC}"
    echo "Utilisez --dump=/chemin/vers/dump.txt ou --live pour generer un dump"
    exit 1
fi

# ── Detection des noeuds presents DANS LE DUMP ─────────────────────────────────
# "container=osmo-operator-N" est ecrit par vty-debug-dump.sh dans les deux
# mondes : en docker c'est le nom du conteneur, en natif l'etiquette rendue par
# osmo_node(). On lit donc le dump de la meme facon des deux cotes.
mapfile -t CONTAINERS < <(
    grep -E "container=osmo-operator-[0-9]+" "$DUMP_FILE" \
        | sed -E 's/.*container=([^ ]+).*/\1/' \
        | sort -u
)

INTER_STP=$(grep -E "container=osmo-inter-stp" "$DUMP_FILE" \
    | sed -E 's/.*container=([^ ]+).*/\1/' \
    | head -1 || true)

N_OPS=${#CONTAINERS[@]}

# ── En-tete ───────────────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║     Resume Operateurs - $(date '+%Y-%m-%d %H:%M')              ║${NC}"
echo -e "${CYAN}║     ${N_OPS} operateur(s) trouve(s)                             ║${NC}"
box_line "Mode : ${MODE} (${MODE_SRC})"
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"

# Dump muet : on le dit. Le comparer a l'inventaire vivant distingue "rien ne
# tourne" de "le dump vient d'ailleurs / est partiel" - deux pannes tres
# differentes, surtout en natif ou aucun "docker ps" ne peut trancher.
if [ "$N_OPS" -eq 0 ]; then
    live_ops="$(osmo_ops 2>/dev/null | tr '\n' ' ' || true)"
    live_ops="${live_ops% }"
    echo ""
    echo -e "  ${YELLOW}Aucun bloc "container=osmo-operator-N" dans ce dump.${NC}"
    print_row "Operateurs vus ici" "${live_ops:-aucun} (mode ${MODE})"
    print_row "Dump" "vide, partiel, ou produit sur un autre noeud"
fi

# ── Inter-STP ─────────────────────────────────────────────────────────────────
if [ -n "$INTER_STP" ]; then
    print_header "INTER-STP HUB (${INTER_STP})"

    asp_count=$(extract_asp_state "$INTER_STP" "STP")
    print_row "ASP actifs" "$asp_count"

    # Routes
    routes=$(grep -A50 "\[OsmoSTP-hub\]" "$DUMP_FILE" \
        | grep -A30 "container=${INTER_STP}" \
        | grep -E '^[0-9]+\.[0-9]+\.[0-9]+/[0-9]+' \
        | wc -l || true)
    print_row "Routes configurees" "$routes"

    # AS
    as_count=$(grep -A50 "\[OsmoSTP-hub\]" "$DUMP_FILE" \
        | grep -A30 "container=${INTER_STP}" \
        | grep -c "AS_ACTIVE" || true)
    print_row "AS actifs" "$as_count"

elif osmo_is_native; then
    # Rien sur le hub dans le dump, et nous sommes en natif : soit cette machine
    # EST le hub et le dump est plus vieux qu'elle, soit le hub est une AUTRE
    # machine, dont le VTY 4239 n'ecoute que sur son propre 127.0.0.1 - il n'est
    # donc PAS observable d'ici. On l'ignore visiblement au lieu d'afficher des
    # compteurs a zero qui se liraient comme un coeur SS7 mort.
    print_header "INTER-STP HUB - ignore (natif)"
    if hub_here="$(osmo_hub)"; then
        print_row "Hub local" "${hub_here} present ici, mais absent du dump"
        print_row "Pour l'inclure" "relancez avec --live"
    else
        print_row "Hub" "$(osmo_hub_hint)"
    fi

elif hub_remote="$(osmo_hub_is_remote 2>/dev/null)"; then
    # WAN : le hub vit sur une AUTRE machine (--hub-ip / OSMO_HUB_IP), il ne
    # peut donc pas figurer dans un dump pris ici, et son VTY 4239 n'ecoute que
    # sur sa propre boucle locale. Ce qui est observable d'ici, c'est le LIEN
    # vers lui : l'association SCTP que chaque noeud tient ouverte.
    print_header "INTER-STP HUB - distant (${hub_remote})"
    hub_assoc="$(osmo_hub_assoc "$hub_remote" 2>/dev/null)" && hub_rc=0 || hub_rc=$?
    case "$hub_rc" in
        0) print_row "Lien M3UA/SCTP" "${hub_assoc} noeud(s) associe(s) a ${hub_remote}:${OSMO_HUB_M3UA_PORT}" ;;
        1) print_row "Lien M3UA/SCTP" "aucune association vers ${hub_remote}:${OSMO_HUB_M3UA_PORT}" ;;
        *) print_row "Lien M3UA/SCTP" "non sondable ici (ss --sctp absent des noeuds)" ;;
    esac
    print_row "Etat interne" "$(osmo_hub_hint)"

elif hub_here="$(osmo_hub)"; then
    # Mode docker : le conteneur tourne mais ne figure pas dans le dump - le
    # dump est donc anterieur au hub. Sans conteneur hub, on ne dit rien, comme
    # avant : il n'y a simplement pas d'inter-STP dans ce lab.
    print_header "INTER-STP HUB (${hub_here})"
    print_row "Etat" "conteneur en cours, mais absent du dump"
    print_row "Pour l'inclure" "relancez avec --live"
fi

# ── Boucle sur les operateurs ─────────────────────────────────────────────────
# "${CONTAINERS[@]+...}" : sur un dump muet le tableau est vide, et sous
# "set -u" un ""${CONTAINERS[@]}"" nu casserait au lieu de ne rien faire.
for container in ${CONTAINERS[@]+"${CONTAINERS[@]}"}; do
    # Le numero est le suffixe de l'etiquette ; la mapfile n'a laisse passer que
    # "osmo-operator-<chiffres>", donc pas de sous-shell ni de grep ici.
    op_id="${container##*-}"

    print_header "OPERATEUR ${op_id} (${container})"

    # ── STP local ────────────────────────────────────────────────────────
    print_section "SS7"

    # ASP actifs dans STP
    stp_asp=$(extract_asp_state "$container" "STP")
    print_row "ASP actifs (STP)" "$stp_asp"

    # Routes
    routes=$(grep -A50 "\[OsmoSTP\]" "$DUMP_FILE" \
        | grep -A30 "container=${container}" \
        | grep -E '^[0-9]+\.[0-9]+\.[0-9]+/[0-9]+' \
        | wc -l || true)
    print_row "Routes configurees" "$routes"

    # ── MSC ───────────────────────────────────────────────────────────────
    print_section "MSC"

    msc_asp=$(extract_asp_state "$container" "MSC")
    print_row "ASP actif" "$([ "$msc_asp" -gt 0 ] && echo "✓" || echo "✗")"

    # Subscribers VLR
    vlr_count=$(grep -A30 "\[OsmoMSC\]" "$DUMP_FILE" \
        | grep -A20 "container=${container}" \
        | grep -E "subscriber count.*[0-9]+" \
        | grep -oE '[0-9]+' \
        | head -1 || true)
    print_row "Subscribers VLR" "${vlr_count:-0}"

    # Transactions
    trans=$(grep -A30 "\[OsmoMSC\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "transaction" || true)
    print_row "Transactions" "$trans"

    # ── BSC ───────────────────────────────────────────────────────────────
    print_section "BSC"

    bsc_asp=$(extract_asp_state "$container" "BSC")
    print_row "ASP actif" "$([ "$bsc_asp" -gt 0 ] && echo "✓" || echo "✗")"

    # BTS 0 etat
    OML_OK=0
    RSL_OK=0
    BTS_STATE="Down"
    extract_bts_info "$container" 0
    print_row "BTS 0 etat" "$BTS_STATE"
    print_row "  ├─ OML" "$([ "$OML_OK" -eq 1 ] && echo "✓ connecte" || echo "✗ deconnecte")"
    print_row "  └─ RSL" "$([ "$RSL_OK" -eq 1 ] && echo "✓ connecte" || echo "✗ deconnecte")"

    # Nombre de BTS total
    n_bts=$(grep -A50 "\[OsmoBSC\]" "$DUMP_FILE" \
        | grep -A30 "container=${container}" \
        | grep -c "^ bts [0-9]" || true)
    print_row "BTS totales" "$n_bts"

    # ── HLR ───────────────────────────────────────────────────────────────
    print_section "HLR"

    subs_count=$(extract_subscriber_count "$container")
    print_row "Abonnes" "$subs_count"

    # Connexions GSUP
    gsup=$(grep -A20 "\[OsmoHLR\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "GSUP" || true)
    print_row "Connexions GSUP" "$gsup"

    # ── BTS (si disponible) ───────────────────────────────────────────────
    print_section "BTS (phy)"

    # LCHAN actifs
    lchan=$(grep -A30 "\[OsmoBTS\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "lchan" || true)
    print_row "LCHAN" "$lchan"

    # ── SGSN/GGSN (si GPRS actif) ─────────────────────────────────────────
    print_section "GPRS"

    # NS entities
    ns=$(grep -A30 "\[OsmoSGSN\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "NS entity" || true)
    print_row "NS entities" "$ns"

    # PDP contexts
    pdp=$(grep -A30 "\[OsmoGGSN\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "PDP context" || true)
    print_row "PDP contexts" "$pdp"

    # ── Services auxiliaires ─────────────────────────────────────────────
    print_section "Services"

    # SIP connector
    sip_logs=$(grep -A10 "\[OsmoSIPconn\]" "$DUMP_FILE" \
        | grep -c "container=${container}" || true)
    print_row "SIP connector" "$([ "$sip_logs" -gt 0 ] && echo "✓ actif" || echo "?")"

    # SMS relay port
    sms=$(grep -A10 "\[.*\]" "$DUMP_FILE" \
        | grep -A5 "container=${container}" \
        | grep -c "port 7890" || true)
    print_row "SMS relay" "$([ "$sms" -gt 0 ] && echo "✓ actif" || echo "?")"

    # ── Statistiques rapides ─────────────────────────────────────────────
    print_section "Statistiques"

    # Appels actifs
    calls=$(grep -A20 "\[OsmoMSC\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "call-leg" || true)
    print_row "Appels actifs" "$calls"

    # Paging
    paging=$(grep -A20 "\[OsmoBSC\]" "$DUMP_FILE" \
        | grep -A10 "container=${container}" \
        | grep -c "paging" || true)
    print_row "Paging actifs" "$paging"

done

# ── Nettoyage ─────────────────────────────────────────────────────────────────
if [ "$LIVE_MODE" -eq 1 ] && [ -n "$TEMP_DUMP" ]; then
    rm -f "$TEMP_DUMP"
fi

echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║  Analyse terminee - Dump utilise : ${DUMP_FILE}${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""

checks/ss7_check.sh

.sh 495 lignes - 24KB

checks/ss7_check.sh
#!/bin/bash
# ss7_check.sh - Verification SS7 pour architecture Inter-STP central
#
# Architecture cible :
#   - Inter-STP (0.23.0) central
#   - Chaque operateur a STP local (N.23.2) avec route par defaut vers inter-STP
#   - Routes dynamiques apprises via M3UA
#
# Ce qui est verifie :
#   1. Inter-STP : AS actifs pour chaque operateur
#   2. Inter-STP : ASP connectes
#   3. Inter-STP : pas de PROHIB
#   4. Chaque operateur : ASP actif vers inter-STP
#   5. Chaque operateur : routes locales (MSC/BSC) actives
#   6. Chaque operateur : route par defaut (0.0.0/0) vers inter-STP
#   7. Matrice de connectivite basique
#
# DOUBLE MODE - docker ET natif (ISO, VM, machine nue)
# ----------------------------------------------------
# Toute la mecanique de detection et d'acces aux noeuds vit dans checks/_mode.sh ;
# ici on ne fait qu'appeler ses fonctions. Les deux mondes :
#
#   docker : un conteneur osmo-operator-N par operateur, hub = conteneur
#            osmo-inter-stp ; on atteint tout par "docker exec".
#   natif  : un seul operateur (sauf topologie netns), les VTY sont sur
#            127.0.0.1 de CETTE machine. Le hub, lui, est SOIT cette machine
#            (OSMO_ROLE=interstp dans /etc/osmo-role), SOIT une AUTRE machine du
#            WAN - et dans ce second cas son VTY n'est PAS observable d'ici : le
#            VTY Osmocom n'ecoute que sur la boucle locale. On l'annonce alors
#            "ignore", on ne fabrique pas un "0 AS actif" qui se lirait comme
#            une panne alors que le hub tourne tres bien ailleurs.
#
# Les PORTS VTY sont les memes dans les deux mondes (4239 STP, 4254 MSC,
# 4242 BSC, 4258 HLR) : seul l'hote change, et c'est _mode.sh qui s'en occupe.
#
# Usage :
#   sudo ./ss7_check.sh [--verbose] [--quick] [--docker|--native]

set -u

# ── Bibliotheque commune (detection docker/natif, VTY, inventaire) ────────────
# Chemin RELATIF au script : le depot tourne aussi bien depuis un clone que
# depuis /opt/GSM/osmo-operator sur l'ISO, et checks/ est copie en entier par
# build-iso.sh - la bibliotheque est donc toujours a cote de ce fichier.
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
    [ -r "$_c" ] && { . "$_c"; break; }
done
command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }

# ── Couleurs ──────────────────────────────────────────────────────────────────
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m'

# ── Configuration ─────────────────────────────────────────────────────────────
VERBOSE=0
QUICK=0

for arg in "$@"; do
    case "$arg" in
        --verbose|-v) VERBOSE=1 ;;
        --quick|-q)   QUICK=1 ;;
        # --docker / --native / --mode=X : forcer le monde au lieu de le
        # detecter. osmo_mode_force rend 2 si l'argument n'est pas un mode, ce
        # qui ne peut pas arriver ici puisque le motif du case l'a deja filtre.
        --docker|--native|--mode=*)
            osmo_mode_force "$arg" || { echo "mode inconnu : $arg" >&2; exit 1; } ;;
        -h|--help)
            echo "Usage : $0 [--verbose|-v] [--quick|-q] [--docker|--native]"
            echo "  --docker / --native : force le mode ; sans option, detection automatique."
            exit 0 ;;
    esac
done

INTER_STP="osmo-inter-stp"
PASS=0
FAIL=0
WARN=0
SKIP=0

# ── Helpers ───────────────────────────────────────────────────────────────────
ok()   { echo -e "  ${GREEN}✓${NC} $*"; PASS=$((PASS+1)); }
fail() { echo -e "  ${RED}✗${NC} $*"; FAIL=$((FAIL+1)); }
warn() { echo -e "  ${YELLOW}⚠${NC} $*"; WARN=$((WARN+1)); }
skip() { echo -e "  ${YELLOW}-${NC} $*"; SKIP=$((SKIP+1)); }
info() { [[ $VERBOSE -eq 1 ]] && echo -e "    ${CYAN}│${NC} $*"; }

banner() {
    echo ""
    echo -e "${BOLD}$*${NC}"
    echo -e "$(printf '─%.0s' {1..60})"
}

# ── Fonction VTY ──────────────────────────────────────────────────────────────
# Meme contrat qu'avant : rend 1 sans rien ecrire si le VTY ne repond pas,
# sinon la sortie nettoyee. Le dialogue lui-meme (nc/telnet, docker exec ou
# local, netns) est dans osmo_vty ; on garde ICI les temporisations de ce
# script - les changer changerait la QUANTITE de sortie capturee, donc les
# "grep -ac" qui la comptent juste en dessous.
: "${OSMO_VTY_OPEN:=1}"    # sleep avant d'ecrire   (valeur historique de ss7_check)
: "${OSMO_VTY_READ:=1.5}"  # sleep de lecture       (idem)
: "${OSMO_VTY_Q:=1}"       # nc -q1                 (idem)

vty_cmd() {
    local node="$1"
    local port="$2"
    local cmd="$3"

    local raw_output
    raw_output=$(osmo_vty "$node" "$port" enable "$cmd" end) || return 1

    # Filtrer les lignes de banniere/prompt (motif et ordre identiques a avant).
    printf '%s\n' "$raw_output" | osmo_vty_clean 'Free Software lives'
    return 0
}

# ── Verification rapide de disponibilite VTY ──────────────────────────────────
vty_available() {
    osmo_vty_up "$1" "$2"
}

# ── Detection du monde et inventaire ──────────────────────────────────────────
MODE="$(osmo_mode)"

# Operateurs : conteneurs osmo-operator-N en docker ; en natif, OSMO_OP_IDS,
# puis les netns osmo-opN, puis N_OPERATORS de globals.conf, sinon 1.
mapfile -t OPS < <(osmo_ops)
N_OPS=${#OPS[@]}

# Hub : conteneur osmo-inter-stp en docker ; en natif, cette machine SI elle
# porte le role interstp. Sur un noeud operateur natif, le hub est ailleurs :
# osmo_hub rend 1 et osmo_hub_hint dit quoi lancer, et ou.
HAS_INTER_STP=0
HUB_NODE="$INTER_STP"
if HUB_NODE="$(osmo_hub)"; then
    HAS_INTER_STP=1
else
    HUB_NODE="$INTER_STP"
fi

# HUB_REMOTE_IP - adresse du hub quand il ne tourne PAS ici : WAN docker
# (start.sh --wan/--hub-ip injecte OSMO_HUB_IP dans les conteneurs), ou noeud
# operateur natif. Vide si le hub est local, ou si aucune adresse n'est connue.
HUB_REMOTE_IP="$(osmo_hub_is_remote 2>/dev/null || true)"

# HUB_EXPECTED - "un hub devrait exister" (≠ "je peux l'interroger").
# Un hub distant reste attendu des qu'une adresse est connue - OSMO_HUB_IP de
# /etc/osmo-role en natif, l'adresse --hub-ip vue par les conteneurs en docker
# WAN : la route par defaut du STP local vers ce hub, elle, est parfaitement
# verifiable d'ici, meme si l'etat interne du hub ne l'est pas. En docker SANS
# WAN les deux notions se confondent toujours : pas de conteneur, pas de hub.
HUB_EXPECTED=$HAS_INTER_STP
if [ "$HUB_EXPECTED" -eq 0 ] && [ -n "$HUB_REMOTE_IP" ]; then
    HUB_EXPECTED=1
fi

# Nombre d'AS/ASP attendus sur le hub. En docker c'est le nombre d'operateurs
# LOCAUX, qui sont exactement ceux qui s'attachent au hub. En natif le hub sert
# des operateurs DISTANTS : aucun inventaire local ne dit combien il devrait y
# en avoir - on juge alors sur la presence, pas sur un total invente.
EXPECT_AS=0
[ "$MODE" = docker ] && EXPECT_AS=$N_OPS

if osmo_is_docker; then
    MODE_LABEL="docker (conteneurs osmo-operator-N)"
elif osmo_in_container; then
    MODE_LABEL="natif (dans le conteneur ${HOSTNAME:-?})"
else
    MODE_LABEL="natif (cette machine)"
fi

# Ligne de cadre : la largeur est calculee en CARACTERES (${#s}) et non en
# octets - "printf %-58s" compte les octets et decale d'un cran par accent.
boxline() {
    local s="$1" n
    n=$(( 58 - ${#s} )); [ "$n" -lt 0 ] && n=0
    echo -e "${CYAN}║${s}$(printf '%*s' "$n" '')║${NC}"
}

echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
boxline "          SS7 Health Check - $(date '+%H:%M:%S')"
boxline "          mode : ${MODE_LABEL}"
if [ -n "$HUB_REMOTE_IP" ]; then
    boxline "          ${N_OPS} operateur(s)  inter-stp : distant ${HUB_REMOTE_IP}"
else
    boxline "          ${N_OPS} operateur(s)  inter-stp=${HAS_INTER_STP}"
fi
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"

if [ "$N_OPS" -eq 0 ] && [ "$HAS_INTER_STP" -eq 0 ] && [ -z "$HUB_REMOTE_IP" ]; then
    if [ "$MODE" = docker ]; then
        echo -e "${RED}Aucun container trouve${NC}"
    else
        echo -e "${RED}Aucun operateur ni inter-STP sur cette machine${NC}"
    fi
    exit 1
fi

# ══════════════════════════════════════════════════════════════════════════════
# CHECK 1 : Inter-STP (hub central)
# ══════════════════════════════════════════════════════════════════════════════
if [ "$HAS_INTER_STP" -eq 1 ]; then
    # [2026-08-31] Le point code etait ecrit en dur "0.23.0", comme celui des
    # operateurs l etait en "${op_id}.23.2". Le hub est en 0.0.0 (c est ce que
    # start.sh annonce au lancement, et ce que porte osmo-multi.conf) : le
    # bandeau affichait donc une adresse qui n est celle de personne.
    banner "INTER-STP (hub SS7) - $(osmo_hub_pc 2>/dev/null || echo 0.0.0)"

    if ! vty_available "$HUB_NODE" 4239; then
        fail "VTY inter-STP (4239) inaccessible"
    else
        ok "VTY accessible"

        # Verifier que l'inter-STP voit tous les operateurs comme AS
        as_output=$(vty_cmd "$HUB_NODE" 4239 "show cs7 instance 0 as all")
        n_as_total=$(echo "$as_output" | grep -acE 'as-op[0-9]+' || true)
        n_as_active=$(echo "$as_output" | grep -acE 'AS_ACTIVE' || true)
        info "AS declares sur le hub : ${n_as_total}"

        if [ "$EXPECT_AS" -gt 0 ]; then
            if [ "$n_as_active" -eq "$EXPECT_AS" ]; then
                ok "AS : ${n_as_active}/${EXPECT_AS} actifs (tous)"
            elif [ "$n_as_active" -gt 0 ]; then
                warn "AS : ${n_as_active}/${EXPECT_AS} actifs"
            else
                fail "AS : aucun actif"
            fi
        elif [ "$n_as_active" -gt 0 ]; then
            # Natif : les operateurs sont des machines distantes, le total
            # attendu n'est pas connaissable d'ici.
            ok "AS : ${n_as_active} actif(s) (total attendu inconnu en natif)"
        else
            fail "AS : aucun actif"
        fi

        # Verifier les ASP connectes
        asp_output=$(vty_cmd "$HUB_NODE" 4239 "show cs7 instance 0 asp")
        n_asp_active=$(echo "$asp_output" | grep -acE 'ASP_ACTIVE' || true)

        if [ "$EXPECT_AS" -gt 0 ]; then
            if [ "$n_asp_active" -eq "$EXPECT_AS" ]; then
                ok "ASP : ${n_asp_active}/${EXPECT_AS} connectes (tous)"
            else
                warn "ASP : ${n_asp_active}/${EXPECT_AS} connectes"
            fi
        elif [ "$n_asp_active" -gt 0 ]; then
            ok "ASP : ${n_asp_active} connecte(s) (total attendu inconnu en natif)"
        else
            warn "ASP : aucun connecte"
        fi

        # Verifier l'absence de PROHIB (routes bloquees)
        route_output=$(vty_cmd "$HUB_NODE" 4239 "show cs7 instance 0 route")
        n_prohib=$(echo "$route_output" | grep -aciE 'prohib' || true)

        if [ "$n_prohib" -eq 0 ]; then
            ok "Routes : aucun PROHIB"
        else
            fail "Routes : ${n_prohib} PROHIB detectees"
        fi
    fi
elif [ -n "$HUB_REMOTE_IP" ]; then
    # Hub DISTANT (WAN) : "conteneur absent" serait un contresens - le hub
    # tourne, ailleurs. Ce qui est observable d'ici, c'est le LIEN vers lui.
    banner "INTER-STP (hub distant ${HUB_REMOTE_IP})"

    # Le lien operateur → hub est du M3UA sur SCTP (osmo-stp.cfg : "asp
    # asp-to-inter 2908 2910 m3ua"). Une sonde TCP sur 2908 rendrait
    # "Connection refused" sur un lien parfaitement monte : c'est l'association
    # SCTP, vue depuis les noeuds, qui fait foi.
    hub_assoc="$(osmo_hub_assoc "$HUB_REMOTE_IP" 2>/dev/null)" && hub_rc=0 || hub_rc=$?
    case "$hub_rc" in
        0) ok "Lien M3UA/SCTP → ${HUB_REMOTE_IP}:${OSMO_HUB_M3UA_PORT} : ${hub_assoc} noeud(s) associe(s)" ;;
        1) fail "Aucune association SCTP vers ${HUB_REMOTE_IP}:${OSMO_HUB_M3UA_PORT} (${hub_assoc}) - hub arrete, filtre, ou ASP shutdown" ;;
        *) skip "Association SCTP vers ${HUB_REMOTE_IP} non sondable ici (ss --sctp absent des noeuds)" ;;
    esac

    # Pas de faux echec sur l'etat INTERNE du hub : son VTY n'ecoute que sur SA
    # boucle locale. La phrase vient de _mode.sh et nomme l'IP reelle.
    skip "$(osmo_hub_hint)"
else
    banner "INTER-STP"
    if [ "$MODE" = docker ]; then
        skip "Container ${INTER_STP} absent"
    else
        # Pas de faux echec : le hub est peut-etre parfaitement vivant, mais son
        # VTY n'ecoute que sur SA boucle locale. La phrase vient de _mode.sh et
        # nomme l'IP reelle du hub quand /etc/osmo-role la donne.
        skip "$(osmo_hub_hint)"
    fi
fi

# ══════════════════════════════════════════════════════════════════════════════
# CHECK 2 : Chaque operateur
# ══════════════════════════════════════════════════════════════════════════════
for op in ${OPS[@]+"${OPS[@]}"}; do
    op_id="$op"
    # osmo_node garde l'etiquette "osmo-operator-N" meme en natif : c'est le
    # nom d'affichage que les autres checks correlent.
    container="$(osmo_node "$op")"
    # ── LE LIBELLE DIT CE QUI EST, PAS UNE FORMULE ──────────────────────────
    # [2026-08-31] Le point code etait ECRIT EN DUR comme "${op_id}.23.2" -
    # d ou "STP 1.23.2", "2.23.2", "3.23.2", des adresses qui ne sont celles de
    # personne (le plan local donne 1.<op>.2). Et le nom affichait toujours
    # "osmo-operator-N", y compris pour l operateur NATIF, qui n est pas un
    # conteneur : on lisait "OPERATEUR 1 (osmo-operator-1)" pour une pile qui
    # tourne sur l hote, et on cherchait un conteneur inexistant.
    # On lit desormais la topologie (osmo-multi.conf) quand elle existe.
    _pc="$(osmo_op_pc "$op_id" 2>/dev/null || true)"
    if [ "$(osmo_op_mode "$op_id" 2>/dev/null)" = "native" ]; then
        _label="natif (hote)"
    else
        _label="$container"
    fi
    banner "OPERATEUR ${op_id} (${_label}) - STP ${_pc:-inconnu}"

    # ── STP local (4239) ─────────────────────────────────────────────────
    echo -e "  ${BOLD}STP local :${NC}"
    if ! vty_available "$op" 4239; then
        fail "STP VTY (4239) inaccessible"
    else
        # Verifier la connexion vers l'inter-STP
        as_out=$(vty_cmd "$op" 4239 "show cs7 instance 0 as all")
        inter_state=$(echo "$as_out" | grep -aoP 'as-inter\s+\K(AS_\w+)' || echo "ABSENT")

        if [[ "$inter_state" == "AS_ACTIVE" ]]; then
            ok "as-inter → hub : ACTIVE"
        elif [[ "$inter_state" == "ABSENT" ]] && [ "$HUB_EXPECTED" -eq 1 ]; then
            fail "as-inter absent (pas de connexion au hub)"
        elif [[ "$inter_state" == "ABSENT" ]]; then
            skip "as-inter : ignore (aucun inter-STP connu depuis ce noeud)"
        else
            warn "as-inter : ${inter_state}"
        fi

        # Verifier les ASP locaux (MSC/BSC)
        asp_out=$(vty_cmd "$op" 4239 "show cs7 instance 0 asp")
        n_asp_ok=$(echo "$asp_out" | grep -acE 'ASP_ACTIVE' || true)

        if [ "$n_asp_ok" -ge 2 ]; then
            ok "ASP locaux : au moins 2 actifs (MSC+BSC)"
        elif [ "$n_asp_ok" -eq 1 ]; then
            warn "ASP locaux : 1 seul actif (manque MSC ou BSC)"
        else
            fail "ASP locaux : aucun actif"
        fi

        # Verifier la route par defaut vers l'inter-STP (element cle).
        # Verifiable des qu'un hub est ATTENDU : le hub distant d'un lab natif
        # se voit dans la table de routage du STP local, meme si son VTY a lui
        # est hors de portee.
        route_out=$(vty_cmd "$op" 4239 "show cs7 instance 0 route")
        has_default=$(echo "$route_out" | grep -acE '0\.0\.0/0.*avail' || true)

        if [ "$has_default" -gt 0 ] && [ "$HUB_EXPECTED" -eq 1 ]; then
            ok "Route par defaut → inter-STP : presente et avail"
        elif [ "$HUB_EXPECTED" -eq 1 ]; then
            fail "Route par defaut → inter-STP absente ou non avail"
        elif [ "$MODE" != docker ]; then
            skip "Route par defaut → inter-STP : ignore (natif, aucun hub declare ici)"
        fi

        # Verifier l'absence de PROHIB
        n_prohib=$(echo "$route_out" | grep -aciE 'prohib' || true)
        if [ "$n_prohib" -gt 0 ]; then
            fail "Routes PROHIB detectees : ${n_prohib}"
        fi

        # Compter les routes dynamiques (juste pour info)
        n_dyn=$(echo "$route_out" | grep -ac 'dyn' || true)
        info "Routes dynamiques apprises : ${n_dyn} (normal: devrait augmenter avec le nombre d'operateurs)"
    fi

    # En mode quick, on passe au prochain operateur
    if [ "$QUICK" -eq 1 ]; then
        continue
    fi

    # ── MSC (4254) ───────────────────────────────────────────────────────
    echo -e "  ${BOLD}MSC :${NC}"
    if ! vty_available "$op" 4254; then
        fail "MSC VTY (4254) inaccessible"
    else
        msc_asp=$(vty_cmd "$op" 4254 "show cs7 instance 0 asp")
        msc_active=$(echo "$msc_asp" | grep -acE 'ASP_ACTIVE' || true)
        [ "$msc_active" -gt 0 ] && ok "MSC→STP : ASP_ACTIVE" || fail "MSC→STP : pas de ASP actif"

        # SCCP - osmo_qgrep, et non "grep -aq" : en bout de tube, grep -aq sort
        # au premier match et tue le producteur en SIGPIPE (141 sous pipefail).
        msc_sccp=$(vty_cmd "$op" 4254 "show cs7 instance 0 sccp users")
        echo "$msc_sccp" | osmo_qgrep -aE 'SSN 254' && ok "SCCP SSN 254 (MSC-A) enregistre" || fail "SCCP SSN 254 absent"
    fi

    # ── BSC (4242) ───────────────────────────────────────────────────────
    echo -e "  ${BOLD}BSC :${NC}"
    if ! vty_available "$op" 4242; then
        fail "BSC VTY (4242) inaccessible"
    else
        bsc_asp=$(vty_cmd "$op" 4242 "show cs7 instance 0 asp")
        bsc_active=$(echo "$bsc_asp" | grep -acE 'ASP_ACTIVE' || true)
        [ "$bsc_active" -gt 0 ] && ok "BSC→STP : ASP_ACTIVE" || fail "BSC→STP : pas de ASP actif"

        # SCCP
        bsc_sccp=$(vty_cmd "$op" 4242 "show cs7 instance 0 sccp users")
        echo "$bsc_sccp" | osmo_qgrep -aE 'SSN 254' && ok "SCCP SSN 254 (BSC) enregistre" || fail "SCCP SSN 254 absent"

        # BTS state (optionnel)
        bsc_bts=$(vty_cmd "$op" 4242 "show bts 0" 2>/dev/null || echo "")
        if echo "$bsc_bts" | osmo_qgrep -aE "Oper 'Enabled'.*Avail 'OK'"; then
            ok "BTS 0 : OK"
        fi
    fi

    # ── HLR (4258) ───────────────────────────────────────────────────────
    echo -e "  ${BOLD}HLR :${NC}"
    if ! vty_available "$op" 4258; then
        fail "HLR VTY (4258) inaccessible"
    else
        hlr_gsup=$(vty_cmd "$op" 4258 "show gsup-connections")
        vlr_conn=$(echo "$hlr_gsup" | grep -acE "VLR" || true)
        [ "$vlr_conn" -gt 0 ] && ok "GSUP : VLR connecte" || fail "GSUP : VLR absent"
    fi
done

# ══════════════════════════════════════════════════════════════════════════════
# CHECK 3 : Matrice de connectivite simple
# ══════════════════════════════════════════════════════════════════════════════
# La matrice croise les operateurs LOCAUX entre eux via le hub : elle a du sens
# des qu'un hub est ATTENDU (local, ou distant en WAN - le trafic op1↔op2 le
# traverse dans les deux cas) et qu'il y a plus d'un operateur ici. Sur un
# mono-operateur elle se reduirait a une case "self" - on l'annonce ignoree
# plutot que de la dessiner vide, et on renvoie vers le check qui, lui, sait
# regarder entre machines.
if [ "$HUB_EXPECTED" -eq 1 ] && [ "$N_OPS" -gt 1 ]; then
    if [ -n "$HUB_REMOTE_IP" ]; then
        banner "MATRICE DE CONNECTIVITE (via inter-STP distant ${HUB_REMOTE_IP})"
    else
        banner "MATRICE DE CONNECTIVITE (via inter-STP)"
    fi

    printf "  %-8s" ""
    for j in "${OPS[@]}"; do printf "  Op%-3s" "$j"; done
    echo ""

    for op in "${OPS[@]}"; do
        src_id="$op"
        printf "  Op%-5s" "${src_id}"

        # On ne teste que la connectivite de base : l'operateur a-t-il une route par defaut ?
        route_out=$(vty_cmd "$op" 4239 "show cs7 instance 0 route" 2>/dev/null || echo "")
        has_default=$(echo "$route_out" | grep -acE '0\.0\.0/0.*avail' || true)

        for j in "${OPS[@]}"; do
            if [ "$j" -eq "$src_id" ]; then
                printf "  ${CYAN}%-5s${NC}" "self"
            elif [ "$has_default" -gt 0 ]; then
                # Si route par defaut, on suppose que la connectivite est possible via l'inter-STP
                printf "  ${GREEN}%-5s${NC}" "via"
            else
                printf "  ${RED}%-5s${NC}" "FAIL"
                FAIL=$((FAIL+1))
            fi
        done
        echo ""
    done
    echo ""
    echo "  Note: 'via' signifie que l'operateur a une route par defaut vers l'inter-STP"
    echo "  Les routes dynamiques specifiques sont apprises au fur et a mesure"
elif [ "$MODE" != docker ]; then
    banner "MATRICE DE CONNECTIVITE (via inter-STP)"
    skip "ignore (natif) : ${N_OPS} operateur(s) local(aux) - la connectivite entre operateurs passe par le WAN, voir checks/wan_ss7_check.sh"
fi

# ══════════════════════════════════════════════════════════════════════════════
# RESUME
# ══════════════════════════════════════════════════════════════════════════════
echo ""
echo -e "$(printf '═%.0s' {1..60})"

if [ "$FAIL" -eq 0 ]; then
    echo -e "  ${GREEN}${BOLD}SS7 OK${NC}  -  ${GREEN}${PASS} pass${NC}  ${YELLOW}${WARN} warn${NC}  ${SKIP} skip"
elif [ "$FAIL" -le 3 ]; then
    echo -e "  ${YELLOW}${BOLD}SS7 DEGRADE${NC}  -  ${GREEN}${PASS} pass${NC}  ${RED}${FAIL} fail${NC}  ${YELLOW}${WARN} warn${NC}  ${SKIP} skip"
else
    echo -e "  ${RED}${BOLD}SS7 KO${NC}  -  ${GREEN}${PASS} pass${NC}  ${RED}${FAIL} fail${NC}  ${YELLOW}${WARN} warn${NC}  ${SKIP} skip"
fi
echo -e "$(printf '═%.0s' {1..60})"
echo ""

exit "$FAIL"

checks/vty-debug-dump.sh

.sh 472 lignes - 18KB

checks/vty-debug-dump.sh
#!/bin/bash
# vty-debug-dump.sh
#
# Parcourt tous les VTY Osmocom de chaque operateur ET de l'inter-STP,
# active le niveau de log DEBUG, execute les commandes "show" pertinentes,
# et consolide dans un log.
#
# DEUX MONDES, detection automatique (checks/_mode.sh) :
#   docker : un conteneur osmo-operator-N par operateur, le hub SS7 est le
#            conteneur osmo-inter-stp ; on interroge les VTY par "docker exec".
#   natif  : ISO, VM ou machine nue - un seul operateur (sauf topologie netns),
#            les VTY sont en telnet sur 127.0.0.1. Ce qui n'existe que sous
#            docker (le conteneur inter-STP) est IGNORE EXPLICITEMENT : un dump
#            qui inventerait un hub absent se lirait comme une panne SS7.
#
# L'etiquette "container=osmo-operator-N" est ecrite dans le dump DANS LES
# DEUX MODES : checks/operator_summary.sh y accroche toutes ses extractions
# (grep -A20 "container=..."). En natif c'est un nom d'affichage, une cle de
# correlation, pas un objet interrogeable.
#
# VTY couverts par operateur :
#   OsmoSTP          4239
#   OsmoHLR          4258
#   OsmoMGW          4243
#   OsmoMSC          4254
#   OsmoBSC          4242
#   OsmoBTS          4241
#   OsmoPCU          4240
#   OsmoSGSN         4245
#   OsmoGGSN         4260
#   OsmoSIP-conn     4255
#
# VTY de l'inter-STP (hub SS7) :
#   OsmoSTP (hub)    4239
#
# Usage :
#   sudo ./vty-debug-dump.sh [--out /chemin/log.txt] [--docker|--native]

set -euo pipefail
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'

# Memorise AVANT le sourcing : la bibliotheque ecrit dans OSMO_MODE (memoisation),
# on ne saurait plus apres coup si la valeur venait de l'environnement.
MODE_ENV="${OSMO_MODE:-}"

# ── Bibliotheque commune docker/natif ─────────────────────────────────────────
# Chemin RELATIF au script (BASH_SOURCE) : le depot et l'ISO n'ont pas la meme
# racine, et l'ISO embarque checks/ en entier - donc _mode.sh est toujours a
# cote de ce fichier, jamais a un chemin absolu du depot.
_here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for _c in "$_here/_mode.sh" /opt/GSM/osmo-operator/checks/_mode.sh; do
    [ -r "$_c" ] && { . "$_c"; break; }
done
command -v osmo_mode >/dev/null || { echo "checks/_mode.sh introuvable" >&2; exit 1; }

# ── Options ───────────────────────────────────────────────────────────────────
# --out reste accepte A N'IMPORTE QUELLE POSITION : operator_summary.sh --live
# lance "./vty-debug-dump.sh --out /tmp/vty-live-$$.txt" et doit continuer a
# marcher meme precede de --native.
OUTFILE="${OUTFILE:-/tmp/vty-debug-dump.txt}"
MODE_FORCED=0
while [ $# -gt 0 ]; do
    case "$1" in
        --out)
            [ -n "${2:-}" ] || { echo "--out attend un chemin" >&2; exit 1; }
            OUTFILE="$2"; shift 2 ;;
        --out=*)
            OUTFILE="${1#*=}"; shift ;;
        --docker|--native|--mode=*)
            osmo_mode_force "$1" || { echo "mode inconnu : $1" >&2; exit 1; }
            MODE_FORCED=1; shift ;;
        -h|--help)
            echo "Usage : sudo ./vty-debug-dump.sh [--out /chemin/log.txt] [--docker|--native]"
            echo "  --docker  force le mode conteneurs (docker exec)"
            echo "  --native  force le mode natif (VTY locaux sur 127.0.0.1)"
            echo "  sans option : detection automatique"
            exit 0 ;;
        *)
            echo "option inconnue : $1" >&2
            echo "Usage : sudo ./vty-debug-dump.sh [--out /chemin/log.txt] [--docker|--native]" >&2
            exit 1 ;;
    esac
done

INTER_STP_CONTAINER="$(osmo_node hub)"   # "osmo-inter-stp" dans les deux mondes

# ── Tables VTY : deux jeux (operateur / inter-stp) ───────────────────────────
declare -A OP_VTY_NAME OP_VTY_SHOW
declare -A ISTP_VTY_NAME ISTP_VTY_SHOW

register_op_vty() {
    local port="$1" name="$2" show_cmds="$3"
    OP_VTY_NAME[$port]="$name"
    OP_VTY_SHOW[$port]="$show_cmds"
}

register_istp_vty() {
    local port="$1" name="$2" show_cmds="$3"
    ISTP_VTY_NAME[$port]="$name"
    ISTP_VTY_SHOW[$port]="$show_cmds"
}

# ── Commandes show operateur - enrichies ──────────────────────────────────────

register_op_vty 4239 "OsmoSTP" "\
show cs7 instance 0 users|\
show cs7 instance 0 as all|\
show cs7 instance 0 asp|\
show cs7 instance 0 route|\
show cs7 instance 0 sccp users|\
show cs7 instance 0 sccp connections|\
show cs7 instance 0 sccp timers|\
show logging level|\
show talloc-context all brief"

register_op_vty 4258 "OsmoHLR" "\
show subscriber all|\
show subscriber count|\
show gsup-connections|\
show logging level|\
show talloc-context all brief"

register_op_vty 4243 "OsmoMGW" "\
show mgcp stats|\
show mgcp active|\
show mgcp endpoint Name trunk-0|\
show logging level|\
show talloc-context all brief"

register_op_vty 4254 "OsmoMSC" "\
show network|\
show subscriber all|\
show subscriber count|\
show transaction all|\
show call-legs|\
show connection|\
show sgs-connections|\
show cs7 instance 0 users|\
show cs7 instance 0 asp|\
show cs7 instance 0 as all|\
show cs7 instance 0 sccp users|\
show cs7 instance 0 sccp connections|\
show logging level|\
show stats|\
show talloc-context all brief"

register_op_vty 4242 "OsmoBSC" "\
show network|\
show bts all|\
show bts 0|\
show trx 0 0|\
show timeslot all|\
show lchan all|\
show lchan summary|\
show paging all|\
show paging-group all|\
show conns|\
show cs7 instance 0 users|\
show cs7 instance 0 asp|\
show cs7 instance 0 as all|\
show cs7 instance 0 sccp users|\
show cs7 instance 0 sccp connections|\
show logging level|\
show stats|\
show talloc-context all brief"

register_op_vty 4241 "OsmoBTS" "\
show bts 0|\
show trx 0 0|\
show ts all|\
show lchan all|\
show lchan summary|\
show phy 0 inst all|\
show logging level|\
show talloc-context all brief"

register_op_vty 4240 "OsmoPCU" "\
show bts all|\
show tbf all|\
show ms all|\
show logging level|\
show talloc-context all brief"

register_op_vty 4245 "OsmoSGSN" "\
show sgsn|\
show mm-context all|\
show pdp-context all|\
show subscriber cache|\
show ns|\
show ns stats|\
show ns entities|\
show bssgp|\
show logging level|\
show stats|\
show talloc-context all brief"

register_op_vty 4260 "OsmoGGSN" "\
show ggsn|\
show pdp-context all|\
show apn all|\
show logging level|\
show talloc-context all brief"

register_op_vty 4255 "OsmoSIPconn" "\
show logging level|\
show talloc-context all brief"

OP_VTY_PORTS=(4239 4258 4243 4254 4242 4241 4240 4245 4260 4255)

# ── Commandes show inter-STP - balayage instances 0..9 ────────────────────────
# L'inter-STP hub peut avoir N instances CS7 (une par lien operateur).

ISTP_SHOW_CMDS="show cs7 instance 0 users|\
show cs7 instance 0 as all|\
show cs7 instance 0 as all active|\
show cs7 instance 0 asp|\
show cs7 instance 0 route|\
show cs7 instance 0 sccp users|\
show cs7 instance 0 sccp connections|\
show cs7 instance 0 sccp timers|\
show cs7 instance 1 users|\
show cs7 instance 1 as all|\
show cs7 instance 1 asp|\
show cs7 instance 1 route|\
show cs7 instance 1 sccp users|\
show cs7 instance 1 sccp connections|\
show cs7 instance 2 users|\
show cs7 instance 2 as all|\
show cs7 instance 2 asp|\
show cs7 instance 2 route|\
show cs7 instance 2 sccp users|\
show cs7 instance 2 sccp connections|\
show cs7 instance 3 users|\
show cs7 instance 3 as all|\
show cs7 instance 3 asp|\
show cs7 instance 3 route|\
show cs7 instance 4 as all|\
show cs7 instance 4 asp|\
show cs7 instance 4 route|\
show cs7 instance 5 as all|\
show cs7 instance 5 asp|\
show cs7 instance 5 route|\
show cs7 instance 6 as all|\
show cs7 instance 6 asp|\
show cs7 instance 6 route|\
show cs7 instance 7 as all|\
show cs7 instance 7 asp|\
show cs7 instance 7 route|\
show cs7 instance 8 as all|\
show cs7 instance 8 asp|\
show cs7 instance 8 route|\
show cs7 instance 9 as all|\
show cs7 instance 9 asp|\
show cs7 instance 9 route|\
show logging level|\
show stats|\
show talloc-context all brief"

register_istp_vty 4239 "OsmoSTP-hub" "$ISTP_SHOW_CMDS"

ISTP_VTY_PORTS=(4239)

# ── Inventaire des noeuds - docker ou natif ────────────────────────────────────
MODE="$(osmo_mode)"
# Un OSMO_MODE d'environnement qui ne vaut ni docker ni native est ignore par la
# bibliotheque (elle redetecte) : on ne l'annonce donc pas comme un forcage.
if   [ "$MODE_FORCED" -eq 1 ]; then MODE_SRC="force par option"
elif [ "$MODE_ENV" = docker ] || [ "$MODE_ENV" = native ]; then MODE_SRC="force par OSMO_MODE"
else MODE_SRC="detection automatique"
fi

# osmo_ops : conteneurs osmo-operator-N en docker ; OSMO_OP_IDS / netns /
# N_OPERATORS (sinon 1) en natif. Deja trie numeriquement et dedoublonne, d'ou
# la disparition du "sort -t - -k 3 -n".
mapfile -t OPS < <(osmo_ops)
OP_CONTAINERS=()
for op in "${OPS[@]:-}"; do
    [ -n "$op" ] && OP_CONTAINERS+=("$(osmo_node "$op")")
done

# Inter-STP : present SI on peut l'interroger D'ICI.
#   docker : le conteneur osmo-inter-stp tourne.
#   natif  : cette machine EST le hub (OSMO_ROLE=interstp, ou un osmo-stp lance
#            sur osmo-stp-interop.cfg). Si le hub est une autre machine, son VTY
#            n'ecoute que sur SON 127.0.0.1 : rien n'est dumpable d'ici, et on le
#            dit - un bloc "OsmoSTP-hub" vide serait lu comme un hub en panne.
HAS_INTER_STP=0
osmo_hub >/dev/null 2>&1 && HAS_INTER_STP=1

if [ ${#OP_CONTAINERS[@]} -eq 0 ] && [ "$HAS_INTER_STP" -eq 0 ]; then
    echo -e "${RED}Aucun container osmo-operator-N ni ${INTER_STP_CONTAINER} trouve.${NC}"
    if [ "$MODE" = docker ]; then
        echo -e "  Containers actifs : $(docker ps --format '{{.Names}}' | tr '\n' ' ')"
    else
        echo -e "  Mode natif : aucun operateur declare (OSMO_OP_IDS, netns ${OSMO_NETNS_PREFIX}N, N_OPERATORS)."
    fi
    exit 1
fi

N_OPS=${#OP_CONTAINERS[@]}
# Le mode est annonce AVANT tout le reste : un diagnostic qui ne dit pas dans
# quel monde il a regarde n'est pas exploitable.
echo -e "${CYAN}Mode : ${MODE} (${MODE_SRC})${NC}"
echo -e "${GREEN}=== VTY Debug Dump - ${N_OPS} operateur(s) + inter-stp=${HAS_INTER_STP} → ${OUTFILE} ===${NC}"
echo ""

# ── Vider / initialiser le fichier de sortie ─────────────────────────────────
: > "$OUTFILE"
log() { echo -e "$*" | tee -a "$OUTFILE"; }
logsep() {
    local title="$1"
    log ""
    log "$(printf '═%.0s' {1..72})"
    log "  ${title}"
    log "$(printf '═%.0s' {1..72})"
}
logsubsep() {
    local title="$1"
    log ""
    log "  $(printf '─%.0s' {1..68})"
    log "  ${title}"
    log "  $(printf '─%.0s' {1..68})"
}

log "VTY DEBUG DUMP - $(date '+%Y-%m-%d %H:%M:%S')"
log "Mode       : ${MODE} (${MODE_SRC})"
log "Operateurs : ${N_OPS}   Containers : ${OP_CONTAINERS[*]:-aucun}"
if   [ "$HAS_INTER_STP" -eq 1 ]; then log "Inter-STP  : present"
elif [ "$MODE" = docker        ]; then log "Inter-STP  : absent"
else                                  log "Inter-STP  : ignore (natif) - $(osmo_hub_hint)"
fi

# ── Interroger un VTY sur un noeud ─────────────────────────────────────────────
# $1=<op> (numero d'operateur ou "hub")  $2=port  $3=nom  $4=commandes show
# osmo_vty_up / osmo_vty remplacent le couple "docker exec ... echo
# >/dev/tcp/127.0.0.1/PORT" + "docker exec ... nc" : meme dialogue, mais local
# en natif. Le champ "container=" du dump garde l'etiquette du noeud.
query_vty() {
    local op="$1"
    local port="$2"
    local name="$3"
    local show_str="$4"
    local container
    container="$(osmo_node "$op")"

    # Test de connectivite rapide. En natif la sonde passive (ss) passe d'abord :
    # elle n'ouvre aucune session VTY.
    if ! osmo_vty_up "$op" "$port"; then
        echo -e "  ${YELLOW}[SKIP]${NC} ${name} (port ${port}) - non disponible"
        log "  [SKIP] ${name} port ${port} - non disponible"
        return 1
    fi

    echo -ne "  ${CYAN}${name}${NC} (${port}) ... "

    # Construire le script VTY : passage en DEBUG, puis les show, puis end.
    local -a VTY_CMDS=(enable "logging level all debug" "logging filter all 1")
    IFS='|' read -ra SHOW_CMDS <<< "$show_str"
    for cmd in "${SHOW_CMDS[@]}"; do
        VTY_CMDS+=("$cmd")
    done
    VTY_CMDS+=(end)

    # Temporisations d'origine conservees telles quelles (nc -q2, 0,5 s avant
    # d'ecrire, 2 s de lecture) : ce sont de gros jeux de commandes, et changer
    # ces valeurs changerait la QUANTITE de sortie capturee - donc le compteur
    # de lignes affiche juste apres.
    local raw_output
    raw_output="$(OSMO_VTY_OPEN=0.5 OSMO_VTY_READ=2 OSMO_VTY_Q=2 \
                  osmo_vty "$op" "$port" "${VTY_CMDS[@]}" || true)"

    # Filtrer banniere/prompt VTY - meme motif et meme ordre qu'avant.
    # Sans argument : ce dump GARDE la ligne "Free Software lives", contrairement
    # a global_check.sh et ss7_check.sh.
    local clean_output
    clean_output="$(printf '%s\n' "$raw_output" | osmo_vty_clean)"

    local n_lines
    n_lines=$(echo "$clean_output" | wc -l)
    echo -e "${GREEN}${n_lines} lignes${NC}"

    {
        echo ""
        echo "  [${name}] port=${port}  container=${container}"
        echo "  Commandes show : $(echo "$show_str" | tr '|' ', ')"
        echo ""
        echo "$clean_output" | sed 's/^/    /'
    } >> "$OUTFILE"

    return 0
}

# ── Boucle principale : noeuds operateur ───────────────────────────────────────
TOTAL_OK=0
TOTAL_SKIP=0

for op in "${OPS[@]:-}"; do
    [ -n "$op" ] || continue
    container="$(osmo_node "$op")"
    logsep "Container : ${container}  -  $(date '+%H:%M:%S')"
    echo -e "${GREEN}--- ${container} ---${NC}"

    for port in "${OP_VTY_PORTS[@]}"; do
        logsubsep "${OP_VTY_NAME[$port]} (port ${port})"
        if query_vty "$op" "$port" \
                "${OP_VTY_NAME[$port]}" "${OP_VTY_SHOW[$port]}"; then
            TOTAL_OK=$(( TOTAL_OK + 1 ))
        else
            TOTAL_SKIP=$(( TOTAL_SKIP + 1 ))
        fi
    done
    echo ""
done

# ── Inter-STP ─────────────────────────────────────────────────────────────────
if [ "$HAS_INTER_STP" -eq 1 ]; then
    logsep "Container : ${INTER_STP_CONTAINER} (hub SS7)  -  $(date '+%H:%M:%S')"
    echo -e "${GREEN}--- ${INTER_STP_CONTAINER} ---${NC}"

    for port in "${ISTP_VTY_PORTS[@]}"; do
        logsubsep "${ISTP_VTY_NAME[$port]} (port ${port})"
        if query_vty hub "$port" \
                "${ISTP_VTY_NAME[$port]}" "${ISTP_VTY_SHOW[$port]}"; then
            TOTAL_OK=$(( TOTAL_OK + 1 ))
        else
            TOTAL_SKIP=$(( TOTAL_SKIP + 1 ))
        fi
    done
    echo ""
elif [ "$MODE" = docker ]; then
    log ""
    log "  [INFO] Container ${INTER_STP_CONTAINER} absent - skipped"
else
    # Natif sans hub local : ni OK ni echec, et surtout pas de compteur touche -
    # il n'y a rien a interroger ici, ce n'est pas une panne.
    echo -e "  ${YELLOW}${INTER_STP_CONTAINER} - ignore (natif)${NC}"
    echo -e "  ${YELLOW}  $(osmo_hub_hint)${NC}"
    log ""
    log "  [INFO] ${INTER_STP_CONTAINER} - ignore (natif) : $(osmo_hub_hint)"
fi

# ── Resume ────────────────────────────────────────────────────────────────────
logsep "RESUME - $(date '+%Y-%m-%d %H:%M:%S')"
log "  Mode                  : ${MODE} (${MODE_SRC})"
log "  VTY interroges        : ${TOTAL_OK}"
log "  VTY non disponibles   : ${TOTAL_SKIP}"
log "  Operateurs            : ${N_OPS}"
if   [ "$HAS_INTER_STP" -eq 1 ]; then log "  Inter-STP             : oui"
elif [ "$MODE" = docker        ]; then log "  Inter-STP             : non"
else                                  log "  Inter-STP             : ignore (natif)"
fi
log "  Fichier               : ${OUTFILE}"
log "  Taille                : $(wc -c < "$OUTFILE") octets  /  $(wc -l < "$OUTFILE") lignes"

echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║  Dump complet → ${OUTFILE}${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "  ${CYAN}Consulter :${NC}  less ${OUTFILE}"
echo -e "  ${CYAN}Filtrer   :${NC}  grep -A5 'OsmoBSC' ${OUTFILE}"
echo -e "  ${CYAN}SS7 hub   :${NC}  grep -A10 'OsmoSTP-hub' ${OUTFILE}"
echo -e "  ${CYAN}Routes    :${NC}  grep -A20 'show cs7.*route' ${OUTFILE}"
echo -e "  ${CYAN}ASP etat  :${NC}  grep -iE 'asp|active|inactive|down' ${OUTFILE}"
echo -e "  ${CYAN}SCCP      :${NC}  grep -A10 'sccp' ${OUTFILE}"
echo -e "  ${CYAN}Erreurs   :${NC}  grep -iE 'error|fail|warn|prohib|timeout|refused' ${OUTFILE}"

# Copie locale si lance depuis l'hote
if [ -w "$(pwd)" ]; then
    cp "$OUTFILE" "$(pwd)/vty-debug-dump-$(date '+%Y%m%d-%H%M%S').txt" 2>/dev/null && \
        echo -e "  ${CYAN}Copie locale :${NC} $(pwd)/vty-debug-dump-$(date '+%Y%m%d-%H%M%S').txt" || true
fi

checks/wan_ss7_check.sh

.sh 333 lignes - 19KB

checks/wan_ss7_check.sh
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# checks/wan_ss7_check.sh - etat du WAN, et etat du SS7 VIS-A-VIS du WAN
#
# checks/ss7_check.sh verifie le SS7 D'UN noeud (inter-STP, ASP, routes). Ce
# script-ci repond a la question qui vient juste apres : "et entre noeuds ?"
#
# CE QU'IL FAUT SAVOIR AVANT DE LIRE LA SORTIE
# --------------------------------------------
# Le WAN d'osmo-operator ne transporte PAS de SS7. Entre deux noeuds il n'y a que :
#     • SIP/RTP  (Asterisk ↔ Asterisk)  → la voix
#     • TCP 789x (sms-interop-relay)    → les SMS
# Le SS7/M3UA (osmo-stp, point codes 1.N.2, inter-STP 0.0.0) reste INTERNE a un
# noeud : l'ASP de chaque operateur pointe sur 172.20.0.10, une adresse du plan
# docker prive, injoignable depuis l'exterieur - et le catch-all "route 0.0.0
# 0.0.0 → as-inter" envoie tout PC inconnu vers CET inter-STP local.
#
# Ce n'est donc pas une panne a reparer, c'est la frontiere du montage : un
# appel inter-noeud est une TERMINAISON SIP, pas un relais MAP/ISUP. Ce script
# le VERIFIE au lieu de le supposer - et signale les deux cas ou l'on croit a
# tort avoir du SS7 inter-noeud :
#   1. deux noeuds qui reutilisent les MEMES point codes (ils le font tous :
#      1.1.2, 1.2.2... sont recalcules a l'identique sur chaque machine) ;
#   2. un port M3UA (2905/2908) expose sur l'interface publique.
#
# Usage : sudo checks/wan_ss7_check.sh [--verbose] [--conf /etc/osmo-wan.conf]
# ══════════════════════════════════════════════════════════════════════════════
set -u

GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'
RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m'

VERBOSE=0
CONF="/etc/osmo-wan.conf"
while [ $# -gt 0 ]; do
    case "$1" in
        --verbose|-v) VERBOSE=1 ;;
        --conf)       CONF="${2:-}"; shift ;;
        --conf=*)     CONF="${1#*=}" ;;
        -h|--help)    sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    esac
    shift
done

PASS=0; FAIL=0; WARN=0; SKIP=0
ok()   { echo -e "  ${GREEN}✓${NC} $*"; PASS=$((PASS+1)); }
fail() { echo -e "  ${RED}✗${NC} $*"; FAIL=$((FAIL+1)); }
warn() { echo -e "  ${YELLOW}⚠${NC} $*"; WARN=$((WARN+1)); }
skip() { echo -e "  ${YELLOW}-${NC} $*"; SKIP=$((SKIP+1)); }
info() { [ $VERBOSE -eq 1 ] && echo -e "    ${CYAN}│${NC} $*" || true; }
banner(){ echo ""; echo -e "${BOLD}$*${NC}"; printf '─%.0s' $(seq 1 64); echo ""; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║   WAN - diagnostic voix / SMS / SS7 entre noeuds osmo-operator      ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"

# ── 0. Table des noeuds ───────────────────────────────────────────────────────
banner "0. Table des noeuds WAN"
if [ ! -r "$SCRIPT_DIR/network/wan-nodes.sh" ]; then
    fail "network/wan-nodes.sh introuvable - depot incomplet"; exit 1
fi
# shellcheck source=network/wan-nodes.sh
. "$SCRIPT_DIR/network/wan-nodes.sh"

if ! wan_nodes_load "$CONF" 2>/dev/null; then
    fail "$CONF illisible - aucun WAN configure sur ce noeud"
    echo -e "     ${CYAN}sudo ./start.sh --wan${NC}  ou  ${CYAN}sudo ./start-direct.sh --wan${NC}"
    exit 1
fi
[ "${WAN_NODE_ID:-0}" = "0" ] && wan_nodes_detect_self 2>/dev/null
if wan_nodes_validate 2>/dev/null; then ok "table valide : ${WAN_NODE_COUNT} noeud(s)"
else fail "table incoherente (voir messages ci-dessus)"; fi
wan_nodes_summary

LOCAL_IND="$(wan_local_ind)"
REMOTES=(); for id in "${WAN_NODE_LIST[@]}"; do [ "$id" = "${WAN_NODE_ID:-0}" ] || REMOTES+=("$id"); done
if [ "${#REMOTES[@]}" -eq 0 ]; then
    warn "table a un seul noeud : aucun pair, rien a verifier cote WAN"
    echo -e "     ${CYAN}Ajoutez des noeuds : sudo ./start.sh --wan${NC}"
    exit 0
fi

# ── Mode ──────────────────────────────────────────────────────────────────────
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^osmo-operator-1$'; then
    MODE=docker
    N_OPS=$(docker ps --format '{{.Names}}' | grep -c '^osmo-operator-[0-9]*$')
else
    MODE=native; N_OPS=1
fi
info "mode=${MODE} operateurs locaux=${N_OPS}"

# ── Ce que porte un pair, et ou il ecoute ────────────────────────────────────
# Ces deux fonctions repliquent ce que network/setup-wan-mesh.sh a REELLEMENT
# ecrit dans les configs. Sans elles, ce controle bouclait sur le nombre
# d'operateurs LOCAUX et cherchait chez chaque pair autant de trunks que nous en
# avons : des qu'un pair en portait moins - le montage un noeud par conteneur,
# soit le banc courant - il sortait "trunk wan_n2_op2 ABSENT, relancez
# setup-wan-mesh.sh" et rendait 1. Un echec faux, avec un conseil qui n'y
# changeait rien : le trunk manquant n'a jamais eu lieu d'exister.
peer_nops() {   # $1 = id de noeud
    local n="${WAN_NOPS[$1]:-}"
    if [[ "$n" =~ ^[1-9][0-9]*$ ]]; then echo "$n"; return; fi
    # Table sans le 4e champ (une table ecrite a la main, ou anterieure) : un
    # pair du backbone est un conteneur, donc un operateur ; ailleurs, on
    # suppose le pair monte comme nous. C'est la meme regle que node_nops dans
    # network/setup-wan-mesh.sh - il faut lire ce que le maillage a ecrit, pas
    # ce qu'on aurait souhaite qu'il ecrive.
    if peer_is_backbone "${WAN_IP[$1]:-}"; then echo 1; else echo "${N_OPS:-1}"; fi
}
# Un pair du backbone docker est joint a son adresse propre : le conteneur y
# ecoute sur 5060 et sur le port de base du relais SMS. Les 5080/5082 et 7891
# sont les ports PUBLIES sur l'hote, ils ne veulent rien dire de ce cote.
peer_is_backbone() { case "$1" in 172.20.0.*) return 0 ;; *) return 1 ;; esac; }

ast() { # $1=op  $2=commande
    if [ "$MODE" = docker ]; then docker exec "osmo-operator-$1" asterisk -rx "$2" 2>/dev/null
    else asterisk -rx "$2" 2>/dev/null; fi
}
inside() { # $1=op  $2=commande shell
    if [ "$MODE" = docker ]; then docker exec "osmo-operator-$1" bash -c "$2" 2>/dev/null
    else bash -c "$2" 2>/dev/null; fi
}

# ── 1. Joignabilite des pairs ─────────────────────────────────────────────────
banner "1. Pairs - joignabilite IP et ports WAN"
for r in "${REMOTES[@]}"; do
    rip="${WAN_IP[$r]}"
    if ping -c1 -W2 "$rip" >/dev/null 2>&1; then ok "node ${r} ${rip} repond au ping"
    else warn "node ${r} ${rip} ne repond pas au ping (ICMP filtre ? noeud eteint ?)"; fi
    for i in $(seq 1 "$(peer_nops "$r")"); do
        if peer_is_backbone "$rip"; then sip=5060; sms=7890
        else sip=$(( 5080 + (i - 1) * 2 )); sms=$(( 7890 + i - 1 )); fi
        if command -v nc >/dev/null 2>&1; then
            nc -z -w2 "$rip" "$sms" 2>/dev/null \
                && ok "node ${r} op${i} : relais SMS ${rip}:${sms} ouvert" \
                || warn "node ${r} op${i} : relais SMS ${rip}:${sms} ferme - SMS WAN KO dans ce sens"
        else skip "nc absent : test TCP ${rip}:${sms} impossible"; fi
        info "SIP attendu sur ${rip}:${sip}/udp (UDP : pas de test fiable sans trafic)"
    done
done

# ── 2. Asterisk - trunks WAN ──────────────────────────────────────────────────
banner "2. Asterisk - trunks WAN (voix)"
for i in $(seq 1 "$N_OPS"); do
    out="$(ast "$i" 'pjsip show endpoints')"
    if [ -z "$out" ]; then fail "op${i} : Asterisk ne repond pas"; continue; fi
    for r in "${REMOTES[@]}"; do
        for j in $(seq 1 "$(peer_nops "$r")"); do
            ep="wan_n${r}_op${j}"
            if echo "$out" | grep -q "$ep"; then
                state="$(ast "$i" "pjsip show aor $ep" | grep -iE 'Avail|Unavail|Contact' | head -3 | tr '\n' ' ')"
                if echo "$out" | grep -E "Endpoint:[[:space:]]+$ep" | grep -qi 'Unavailable'; then
                    warn "op${i} → ${ep} present mais Unavailable (pair injoignable)"
                else ok "op${i} → ${ep}"; fi
                info "$state"
            else
                fail "op${i} : trunk ${ep} ABSENT - relancez network/setup-wan-mesh.sh"
            fi
        done
    done
done

# ── 3. Dialplan - indicatifs ──────────────────────────────────────────────────
banner "3. Dialplan - un indicatif route par pair"
for i in $(seq 1 "$N_OPS"); do
    for r in "${REMOTES[@]}"; do
        ind="${WAN_IND[$r]}"
        if ast "$i" "dialplan show wan_out" | grep -q "_${ind}"; then
            ok "op${i} : indicatif ${ind} → node ${r} present dans [wan_out]"
        else
            fail "op${i} : indicatif ${ind} absent de [wan_out]"
        fi
    done
    ast "$i" "dialplan show gsm_in" | grep -q "_${LOCAL_IND}" \
        && ok "op${i} : indicatif local ${LOCAL_IND} traite en interne" \
        || warn "op${i} : indicatif local ${LOCAL_IND} non traite - un appel 'plein numero' echouera"
done

# ── 4. SMS - routes et strip ──────────────────────────────────────────────────
banner "4. SMS - routes WAN et retrait d'indicatif"
for i in $(seq 1 "$N_OPS"); do
    conf="$(inside "$i" 'cat /etc/osmocom/sms-routing.conf')"
    if [ -z "$conf" ]; then fail "op${i} : sms-routing.conf illisible"; continue; fi
    for r in "${REMOTES[@]}"; do
        ind="${WAN_IND[$r]}"
        line="$(echo "$conf" | grep -E "^${ind}[0-9]+ =" | head -1)"
        if [ -z "$line" ]; then
            fail "op${i} : aucune route SMS pour l'indicatif ${ind}"
        elif echo "$line" | grep -q 'strip='; then
            ok "op${i} : route SMS ${ind} avec strip - le HLR distant verra le numero nu"
        else
            fail "op${i} : route SMS ${ind} SANS strip= - le noeud distant cherchera "${ind}10001" dans son HLR et repondra "not found""
        fi
    done
    # Le relais doit savoir lire strip= : une conf correcte sur un relais qui
    # l'ignore est exactement aussi cassee qu'une conf sans strip.
    if inside "$i" 'grep -q lookup_strip /opt/GSM/osmo-operator/scripts/sms-interop-relay.py 2>/dev/null || grep -q lookup_strip /etc/osmocom/sms-interop-relay.py 2>/dev/null || grep -rq lookup_strip /usr/local/bin 2>/dev/null'; then
        ok "op${i} : le relais SMS gere strip="
    else
        warn "op${i} : relais SMS sans lookup_strip - version anterieure, SMS WAN muets"
    fi
done

# ── 5. SS7 - ce qui traverse, ce qui ne traverse pas ──────────────────────────
banner "5. SS7 - portee reelle vis-a-vis du WAN"
echo -e "  ${CYAN}Attendu : le SS7 s'arrete au bord du noeud. On verifie que c'est bien${NC}"
echo -e "  ${CYAN}le cas, et qu'on ne croit pas avoir un lien SS7 inter-noeud.${NC}"
echo ""

# Le hub change la donne : s'il y en a un, le SS7 TRAVERSE, et ce sont les
# point codes qu'il faut verifier - pas leur absence.
HUB_IP=""
[ -r /etc/osmo-role ] && HUB_IP="$(awk -F= '/^OSMO_HUB_IP=/{print $2}' /etc/osmo-role)"
# En docker, /etc/osmo-role n'existe pas sur l'hote : l'adresse du hub est
# injectee dans les conteneurs par start.sh (-e OSMO_HUB_IP, cf. --hub-ip).
# Sans ce repli, un hub WAN parfaitement declare passait pour "un lien non
# maitrise" faute d'etre reconnu comme LE hub.
if [ -z "$HUB_IP" ]; then
    for _i in $(seq 1 "$N_OPS"); do
        HUB_IP="$(inside "$_i" 'printenv OSMO_HUB_IP' | tr -d '\r' | tail -1)"
        [ -n "$HUB_IP" ] && break
    done
fi

# 5a - ou pointe l'ASP local ?
for i in $(seq 1 "$N_OPS"); do
    cfg="$(inside "$i" 'cat /etc/osmocom/osmo-stp.cfg')"
    if [ -z "$cfg" ]; then skip "op${i} : osmo-stp.cfg illisible"; continue; fi
    remote="$(echo "$cfg" | awk '/asp asp-to-inter/{f=1} f&&/remote-ip/{print $2; exit}')"
    shut="$(echo "$cfg" | awk '/asp asp-to-inter/{f=1} f&&/shutdown/{print $0; exit}')"
    pc="$(echo "$cfg" | awk '/^ point-code/{print $2; exit}')"
    if [ -n "$HUB_IP" ] && [ "$remote" = "$HUB_IP" ]; then
        if echo "$shut" | grep -q 'no shutdown'; then
            ok "op${i} : ASP → hub inter-STP ${remote} (actif) - le SS7 traverse le WAN, PC ${pc}"
        else
            fail "op${i} : ASP → hub ${remote} mais ${BOLD}shutdown${NC} - aucun SS7 ne passera"
        fi
        # Un hub joignable ou non, ca ne se devine pas depuis la config. Mais
        # le lien est du M3UA sur SCTP : "nc -z" (TCP) rend "Connection
        # refused" sur un hub parfaitement vivant. La preuve, c'est
        # l'association SCTP ETABLIE, vue depuis le noeud lui-meme.
        assoc="$(inside "$i" "ss -an --sctp" | grep -E "ESTAB.*[[:space:]]${remote//./\\.}:2908([[:space:]]|\$)" || true)"
        if [ -n "$assoc" ]; then
            ok "op${i} : association SCTP etablie vers le hub ${remote}:2908"
        elif inside "$i" "ss -an --sctp" | grep -q .; then
            fail "op${i} : aucune association SCTP vers ${remote}:2908 - l'ASP est DOWN"
            echo -e "     ${CYAN}Sur le noeud inter-STP : ./start-interstp.sh --status${NC}"
        else
            skip "op${i} : ss --sctp indisponible - lien vers ${remote}:2908 non verifiable"
        fi
    else
        case "$remote" in
            172.2[0-9].*|127.*)
                ok "op${i} : ASP inter-STP → ${remote} (docker local) - le SS7 reste dans le noeud"
                [ "${#REMOTES[@]}" -ge 1 ] && info "pas de hub commun : voix et SMS traversent, le SS7 non" ;;
            "") skip "op${i} : pas d'ASP inter-STP declare" ;;
            *)  warn "op${i} : ASP inter-STP → ${remote} - M3UA sur un lien non maitrise ? (pas de chiffrement SCTP ici)" ;;
        esac
    fi
done

# 5b - le point code porte-t-il le numero de noeud ?
# Le plan local (1.<op>.<role>) se recalcule a l'identique partout : trois
# noeuds attaches au meme hub y presenteraient trois fois 1.1.2. Le plan WAN
# (1.<noeud><op>.<role>) leve l'ambiguite. On verifie lequel est en place.
if [ "${#REMOTES[@]}" -ge 1 ]; then
    pc_local="$(inside 1 'cat /etc/osmocom/osmo-stp.cfg' | awk '/^ point-code/{print $2; exit}')"
    expected="1.${WAN_NODE_ID}1.2"
    if [ "$pc_local" = "$expected" ]; then
        ok "point code ${pc_local} : le numero de noeud (${WAN_NODE_ID}) y est encode - pas de collision au hub"
    elif [ -n "$HUB_IP" ]; then
        fail "point code ${pc_local} attendu ${expected} : ce noeud entrerait en collision avec ses pairs sur le hub"
        echo -e "     ${CYAN}Reconstruire l'image : ./build-iso.sh --role operator --node ${WAN_NODE_ID}${NC}"
    else
        warn "point code ${pc_local} : plan local, identique sur tous les noeuds"
        echo -e "     ${CYAN}Sans effet tant qu'il n'y a pas de hub commun. Pour du SS7 inter-noeud :${NC}"
        echo -e "     ${CYAN}./build-iso.sh --role interstp   puis   --role operator --node N${NC}"
    fi
fi

# 5c - un port M3UA expose publiquement ?
LOCAL_IP="$(wan_local_ip)"
for port in 2905 2908; do
    if command -v ss >/dev/null 2>&1; then
        # M3UA tourne sur SCTP dans ce depot (osmo-stp.cfg : "listen m3ua ..."),
        # pas sur TCP : "ss -lntp" seul ne verrait RIEN et conclurait a tort
        # "aucun ecouteur". On regarde les deux transports.
        listen="$( { ss -lntp 2>/dev/null; ss -ln --sctp 2>/dev/null; } | grep ":${port} ")"
        if [ -z "$listen" ]; then
            ok "M3UA ${port} : aucun ecouteur sur l'hote"
        elif echo "$listen" | grep -qE '0\.0\.0\.0:'"${port}"'|\[::\]:'"${port}"; then
            warn "M3UA ${port} ecoute sur 0.0.0.0 - expose si le firewall ne le ferme pas"
            echo -e "     ${CYAN}iptables -A INPUT -p sctp --dport ${port} -j DROP${NC} (ou -p tcp selon le transport)"
        else
            ok "M3UA ${port} : ecoute restreinte ($(echo "$listen" | awk '{print $4}' | tr '\n' ' '))"
        fi
    else skip "ss absent : ecouteurs M3UA non verifies"; fi
done

# 5d - SS7 interne toujours sain ? (delegue au check existant)
if [ -x "$SCRIPT_DIR/checks/ss7_check.sh" ] && [ "$MODE" = docker ]; then
    echo ""
    echo -e "  ${CYAN}SS7 interne : checks/ss7_check.sh --quick${NC}"
else
    skip "SS7 interne : checks/ss7_check.sh (docker uniquement)"
fi

# ── 6. iptables ───────────────────────────────────────────────────────────────
banner "6. iptables - chaine WAN"
if iptables -t nat -L OSMO_WAN_MESH -n >/dev/null 2>&1; then
    n=$(iptables -t nat -S OSMO_WAN_MESH 2>/dev/null | grep -c '^-A')
    ok "chaine OSMO_WAN_MESH presente (${n} regle(s))"
    iptables -t nat -S PREROUTING 2>/dev/null | grep -q 'OSMO_WAN_MESH' \
        && ok "chaine branchee sur PREROUTING" \
        || fail "chaine NON branchee sur PREROUTING - rien n'entre"
else
    if iptables -t nat -L OSMO_WAN_INTEROP -n >/dev/null 2>&1; then
        warn "chaine legacy OSMO_WAN_INTEROP active (2 serveurs, prefixe 66)"
    else
        fail "aucune chaine WAN - relancez network/setup-wan-mesh.sh sans --config-only"
    fi
fi

# ── Bilan ─────────────────────────────────────────────────────────────────────
echo ""
printf '─%.0s' $(seq 1 64); echo ""
echo -e "  ${GREEN}${PASS} ok${NC}   ${RED}${FAIL} echec${NC}   ${YELLOW}${WARN} avertissement${NC}   ${SKIP} ignore"
echo ""
[ "$FAIL" -eq 0 ] || exit 1

full_qmd.sh

.sh 596 lignes - 21KB

full_qmd.sh
#!/bin/bash
# full_qmd.sh - Bundle du depot en Quarto : doc + tests + .h + .c + .py + .sh,
# chaque fichier dans un bloc code type, theme "sketchy".
#
# DEUX MODES
#   MODE=single (defaut) : un seul .qmd. Simple, mais Deno/Quarto casse
#                          ("Maximum call stack size exceeded") au-dela de
#                          ~1-2 Mo, surtout avec embed-resources.
#   MODE=site            : projet Quarto multi-pages (recommande au-dela de 1 Mo).
#                          Sections decoupees en pages de SPLIT_KB max,
#                          recherche plein-texte integree, index global.
#
# Usage :
# TOUT EST ECRIT DANS UN SEUL DOSSIER ($OUTDIR, par defaut ./bundle-qmd/) :
# le .qmd, sketchy.css, sk-filter.html, et le rendu quarto. Le depot reste
# propre, et une seconde execution ne rescanne pas sa propre sortie.
#
# Usage :
#   MODE=site ./full_qmd.sh              # -> ./bundle-qmd/ (multi-pages)
#   ./full_qmd.sh                        # -> ./bundle-qmd/<depot>-full.qmd
#   ./full_qmd.sh out.qmd                # chemin explicite (mode single)
#   OUTDIR=/tmp/x ./full_qmd.sh          # ailleurs
#   PL4Y_SECTION=calypso ./full_qmd.sh   # section pl4y.store visee
#   SCOPE=hw/arm/calypso ./full_qmd.sh   # only this subdir
#   EXCLUDE='build|pc-bios' ./full_qmd.sh
#   MAX_KB=256 ./full_qmd.sh             # troncature par fichier
#   SPLIT_KB=300 MODE=site ./full_qmd.sh # taille max d'une page
#   EMBED=true ./full_qmd.sh             # html self-contained (risque de crash)
#   RENDER_MERMAID=0 ./full_qmd.sh       # .mmd en code au lieu de diagramme
#   RENDER=0 MODE=site ./full_qmd.sh     # sources .qmd seules, sans rendu

set -uo pipefail

case "${1:-}" in -h|--help) sed -n '2,30p' "$0"; exit 0;; esac

MODE="${MODE:-single}"
# Dossier unique de sortie, pour les DEUX modes. Le mode single ecrivait avant
# a la racine du depot (calypso-full.qmd + sketchy.css + sk-filter.html, puis
# calypso-full.html et calypso-full_files/ au rendu) : quatre artefacts laches
# dans le depot, que la passe suivante rescannait comme des sources.
# Nom derive du depot (osmo-operator -> ./osmo-operator-qmd) : les bundles de deux
# depots differents peuvent ainsi etre deposes cote a cote sans collision.
OUTDIR="${OUTDIR:-./$(basename "$(cd "$(dirname "$0")" && pwd)")-qmd}"
# Section pl4y.store visee par ce bundle : ecrite dans .pl4y-section a la racine
# du dossier de sortie. build.mjs (dans pl4y) la lit pour savoir sous quelle URL
# publier, plutot que de deviner d'apres le nom du dossier.
PL4Y_SECTION="${PL4Y_SECTION:-}"
OUT="${1:-}"
SCOPE="${SCOPE:-.}"
TITLE="${TITLE:-Calypso QEMU}"
SUBTITLE="${SUBTITLE:-Full source bundle - DSP C54x / osmocom-bb / SHUNT_LEGIT}"
MAX_KB="${MAX_KB:-512}"
SPLIT_KB="${SPLIT_KB:-400}"
RENDER_MERMAID="${RENDER_MERMAID:-1}"
EMBED="${EMBED:-false}"
# En mode site on rend par defaut : le dossier produit est alors DIRECTEMENT
# deposable dans pl4y/bundles/ (il contient _site/, du HTML). Sans rendu, ce
# n'est qu'un projet Quarto, que rien en aval ne sait publier.
# RENDER=0 pour ne generer que les sources .qmd.
if [ "$MODE" = "site" ]; then RENDER="${RENDER:-1}"; else RENDER="${RENDER:-0}"; fi
EXCLUDE_RE="${EXCLUDE:-subprojects|build|pc-bios|tests/functional|tests/qtest|tests/unit|tests/migration|tests/qemu-iotests|node_modules|\.git|\.pytest_cache}"

HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"

# Le dossier existe avant toute chose : les deux modes ecrivent dedans.
mkdir -p "$OUTDIR" || { echo "ERROR: mkdir '$OUTDIR'" >&2; exit 1; }
OUTDIR="$(cd "$OUTDIR" && pwd)"

# Slug de section : explicite via PL4Y_SECTION, sinon derive du depot.
# qemu-calypso est publie sous /calypso/ — d'ou la seule correspondance
# particuliere.
if [ -z "$PL4Y_SECTION" ]; then
    case "$(basename "$HERE")" in
        qemu-calypso) PL4Y_SECTION="calypso" ;;
        *)            PL4Y_SECTION="$(basename "$HERE")" ;;
    esac
fi
printf '%s\n' "$PL4Y_SECTION" > "$OUTDIR/.pl4y-section"

# Sans argument, le .qmd va dans $OUTDIR. Un argument explicite reste
# respecte : `./full_qmd.sh /tmp/x.qmd` ecrit bien la (et son css/js a cote).
if [ -z "$OUT" ]; then
    OUT="$OUTDIR/$(basename "$HERE")-full.qmd"
elif [ -d "$OUT" ]; then
    OUT="${OUT%/}/$(basename "$HERE")-full.qmd"
fi

MAXB=$(( MAX_KB * 1024 ))
SPLITB=$(( SPLIT_KB * 1024 ))
TMPD="$(mktemp -d)"
trap 'rm -rf "$TMPD"' EXIT

GITREV="$(git rev-parse --short HEAD 2>/dev/null || echo 'n/a')"
GITBRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'n/a')"
NOW="$(date -Iseconds)"

# --- cible + auto-exclusion -------------------------------------------------
SELF_FIXED=""      # fichier a exclure du scan (mode single)
SELF_PREFIX=""     # prefixe de dossier a exclure (mode site)

# $OUTDIR est exclu du scan dans LES DEUX modes. Auparavant le mode single
# n'excluait que le .qmd lui-meme : sketchy.css, sk-filter.html et le
# repertoire *_files/ du rendu precedent etaient reavales comme des sources a
# chaque execution, et le bundle grossissait a chaque passe.
SELF_PREFIX="${OUTDIR#"$HERE"/}/"
[ "$SELF_PREFIX" = "$OUTDIR/" ] && SELF_PREFIX=""   # $OUTDIR hors du depot

if [ "$MODE" = "site" ]; then
    DEST=""
else
    mkdir -p "$(dirname "$OUT")" || { echo "ERROR: mkdir pour '$OUT'" >&2; exit 1; }
    : > "$OUT" || { echo "ERROR: cannot write '$OUT'" >&2; exit 1; }
    OUT_ABS="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
    SELF_FIXED="${OUT_ABS#"$HERE"/}"
    DEST="$OUT"
fi

echo "=== full_qmd.sh ==="
echo "Mode     : $MODE"
echo "Scope    : $SCOPE"
echo "Output   : $([ "$MODE" = site ] && echo "$OUTDIR/" || echo "$OUT")"
echo "Exclude  : $EXCLUDE_RE"
echo "Max/file : ${MAX_KB} ko$([ "$MODE" = site ] && echo " | page: ${SPLIT_KB} ko")"
echo

# ------------------------------------------------------------------ helpers --

_lang_for() {
    case "${1##*.}" in
        c|h)        echo c ;;
        cpp|cc|cxx) echo cpp ;;
        py)         echo python ;;
        sh|bash)    echo bash ;;
        md|qmd)     echo markdown ;;
        mmd)        echo mermaid ;;
        *)          echo default ;;
    esac
}

_fence_for() {
    local maxrun n
    maxrun=$(grep -oE '^`{3,}' "$1" 2>/dev/null | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')
    n=3; [ "${maxrun:-0}" -ge 3 ] && n=$(( maxrun + 1 ))
    printf '%*s' "$n" '' | tr ' ' '`'
}

_anchor_for() {
    printf 'f-%s' "$(printf '%s' "$1" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]\+/-/g; s/^-//; s/-$//')"
}

_human() {
    if command -v numfmt >/dev/null 2>&1; then numfmt --to=iec --suffix=B "$1"; else echo "$1 B"; fi
}

_self_filter() {
    { if [ -n "$SELF_FIXED" ]; then grep -vxF "$SELF_FIXED"; else cat; fi; } \
  | { if [ -n "$SELF_PREFIX" ]; then grep -vF "$SELF_PREFIX"; else cat; fi; }
}

_find_into() {
    local excl="$1"; shift
    local args=() p first=1
    for p in "$@"; do
        if [ "$first" -eq 1 ]; then args+=( -name "$p" ); first=0
        else args+=( -o -name "$p" ); fi
    done
    find "$SCOPE" -type f \( "${args[@]}" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" \
        | { if [ -n "$excl" ]; then grep -vE "$excl"; else cat; fi; } \
        | sed 's|^\./||' \
        | _self_filter \
        | sort > "$TMPD/list"
    wc -l < "$TMPD/list"
}

# ------------------------------------------------------- emission d'un fichier

_add() {
    local f="$1" lang fence size nlines anchor ext eff
    ext="${f##*.}"
    lang="$(_lang_for "$f")"
    size=$(wc -c < "$f" 2>/dev/null || echo 0)
    nlines=$(wc -l < "$f" 2>/dev/null || echo 0)
    anchor="$(_anchor_for "$f")"

    {
        printf '\n## `%s` {#%s}\n\n' "$f" "$anchor"
        printf '::: {.file-meta}\n[.%s]{.tag} %s lignes - %s\n:::\n\n' "$ext" "$nlines" "$(_human "$size")"
    } >> "$DEST"

    if [ "$size" -eq 0 ]; then
        printf '::: {.callout-note appearance="simple"}\nFichier vide.\n:::\n' >> "$DEST"
        eff=0
    elif ! grep -Iq . "$f" 2>/dev/null; then
        printf '::: {.callout-warning appearance="simple"}\nFichier binaire - contenu omis.\n:::\n' >> "$DEST"
        eff=0
    else
        fence="$(_fence_for "$f")"
        if [ "$lang" = "mermaid" ] && [ "$RENDER_MERMAID" = "1" ]; then
            printf '%s{mermaid}\n' "$fence" >> "$DEST"
        else
            printf '%s{.%s .numberLines filename="%s"}\n' "$fence" "$lang" "$f" >> "$DEST"
        fi
        if [ "$size" -gt "$MAXB" ]; then
            head -c "$MAXB" "$f" >> "$DEST"
            printf '\n\n... [TRONQUE a %s ko - %s au total] ...\n' "$MAX_KB" "$(_human "$size")" >> "$DEST"
            eff=$MAXB
        else
            cat "$f" >> "$DEST"
            eff=$size
        fi
        printf '\n%s\n' "$fence" >> "$DEST"
    fi

    # manifest : section | page | fichier | ancre | lignes | taille
    printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
        "$SECNUM" "$(basename "${DEST%.qmd}")" "$f" "$anchor" "$nlines" "$(_human "$size")" \
        >> "$TMPD/manifest"

    PAGE_BYTES=$(( PAGE_BYTES + eff + 400 ))
}

# ------------------------------------------------------------- mode site : page

_new_page() {
    local base
    base="$(printf 'sec%s-%02d' "$SECNUM" "$PART")"
    DEST="$OUTDIR/$base.qmd"
    printf '%s\t%s\t%s\t%s\n' "$SECNUM" "$SECTITLE" "$base.qmd" "partie $PART" >> "$TMPD/pages"
    cat > "$DEST" <<EOF
---
title: "$SECTITLE$([ "$PART" -gt 1 ] && printf ' - partie %s' "$PART")"
---

EOF
    PAGE_BYTES=0
}

# ----------------------------------------------------------------- une section

_section() {
    SECNUM="$1"; SECTITLE="$2"
    local extra_excl="$3" n f fsize
    shift 3
    n=$(_find_into "$extra_excl" "$@")
    echo "Section $SECNUM : $n files"

    if [ "$MODE" = "site" ]; then
        PART=1; _new_page
        while IFS= read -r f; do
            [ -n "$f" ] || continue
            fsize=$(wc -c < "$f" 2>/dev/null || echo 0)
            [ "$fsize" -gt "$MAXB" ] && fsize=$MAXB
            if [ "$PAGE_BYTES" -gt 0 ] && [ $(( PAGE_BYTES + fsize )) -gt "$SPLITB" ]; then
                PART=$(( PART + 1 )); _new_page
            fi
            _add "$f"
        done < "$TMPD/list"
    else
        {
            printf '\n\n# %s {#sec-%s}\n\n' "$SECTITLE" "$SECNUM"
            printf '::: {.section-count}\n**%s fichiers**\n:::\n' "$n"
        } >> "$DEST"
        PAGE_BYTES=0
        while IFS= read -r f; do
            [ -n "$f" ] && _add "$f"
        done < "$TMPD/list"
    fi
}

# --------------------------------------------------------------------- le CSS

_write_css() {
    cat > "$1" <<'SKETCHYCSS'
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Architects+Daughter&family=JetBrains+Mono:wght@400;700&display=swap');

:root{
  --sk-ink:#20242e;
  --sk-paper:#fffdf6;
  --sk-red:#e4572e;
  --sk-teal:#17968a;
  --sk-yellow:#ffd469;
  --sk-rough:255px 12px 225px 15px / 15px 225px 15px 255px;
  --sk-rough2:15px 225px 15px 255px / 255px 15px 225px 15px;
}

body{
  background-color:var(--sk-paper);
  background-image:
    linear-gradient(rgba(32,36,46,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(32,36,46,.05) 1px, transparent 1px);
  background-size:28px 28px;
  font-family:'Architects Daughter','Neucha',ui-rounded,system-ui,sans-serif;
  color:var(--sk-ink);
}

h1.title{
  font-family:'Caveat',cursive; font-size:3.6rem; line-height:1.05;
  display:inline-block; padding:.1em .5em; transform:rotate(-1.1deg);
  background:var(--sk-yellow); border:3px solid var(--sk-ink);
  border-radius:var(--sk-rough); box-shadow:6px 6px 0 var(--sk-ink);
}
.subtitle{ font-size:1.2rem; opacity:.75; transform:rotate(.4deg); display:inline-block; }

h1:not(.title){
  font-family:'Caveat',cursive; font-size:2.9rem; margin-top:3.2rem;
  border-bottom:4px solid var(--sk-ink); border-radius:var(--sk-rough2);
  padding-bottom:.15em; transform:rotate(-.35deg);
}
h2{
  font-family:'JetBrains Mono',monospace; font-size:1.05rem; font-weight:700;
  margin-top:2.6rem; padding:.35em .7em; display:inline-block;
  background:#fff; border:2.5px dashed var(--sk-ink);
  border-radius:var(--sk-rough); box-shadow:3px 3px 0 rgba(23,150,138,.35);
}
h2 code{ background:none; color:var(--sk-ink); padding:0; font-size:1em; }

div.sourceCode, pre:not(.sourceCode){
  border:2.5px solid var(--sk-ink);
  border-radius:var(--sk-rough);
  box-shadow:5px 5px 0 rgba(32,36,46,.9), 10px 10px 0 rgba(228,87,46,.18);
  background:#fffef9 !important;
  transform:rotate(-.12deg);
}
code, pre, code span{ font-family:'JetBrains Mono',ui-monospace,monospace !important; }
div.sourceCode{ max-height:70vh; overflow:auto; }
code.sourceCode > span > a:first-child::before{ color:rgba(32,36,46,.28); }

.code-with-filename .code-with-filename-file{
  font-family:'Caveat',cursive !important; font-size:1.2rem; font-weight:700;
  background:var(--sk-yellow) !important; color:var(--sk-ink) !important;
  border:2.5px solid var(--sk-ink); border-bottom:none;
  border-radius:12px 110px 6px 6px / 40px 8px 6px 6px;
  transform:rotate(-.5deg); display:inline-block; padding:.15em .9em;
}
.code-with-filename .code-with-filename-file pre{ border:none; box-shadow:none; background:none; }

.file-meta{ font-size:.9rem; opacity:.8; margin:.1rem 0 .8rem .2rem; }
.file-meta .tag{
  background:var(--sk-teal); color:#fff; padding:.05em .55em; margin-right:.5em;
  border-radius:var(--sk-rough2); font-family:'JetBrains Mono',monospace; font-size:.8rem;
}
.section-count{
  display:inline-block; padding:.1em .8em; background:#fff;
  border:2px solid var(--sk-red); color:var(--sk-red);
  border-radius:var(--sk-rough2); transform:rotate(.7deg); margin-bottom:1rem;
}
.bundle-index table{ font-family:'JetBrains Mono',monospace; font-size:.82rem; width:100%; }
.bundle-index{
  border:2.5px solid var(--sk-ink); border-radius:var(--sk-rough);
  padding:.6rem 1rem; background:#fff; box-shadow:5px 5px 0 rgba(23,150,138,.3);
}
.bundle-index a{ text-decoration:none; }
.bundle-index tr:hover{ background:rgba(255,212,105,.35); }

#TOC, nav[role=doc-toc]{ font-family:'JetBrains Mono',monospace; font-size:.78rem; }
#TOC a.active, nav[role=doc-toc] a.active{ background:var(--sk-yellow); border-radius:var(--sk-rough2); }
#quarto-sidebar{ font-family:'JetBrains Mono',monospace; font-size:.85rem; }

.sk-filter{ position:sticky; top:0; z-index:50; padding:.4rem 0 .8rem; background:var(--sk-paper); }
.sk-filter input{
  width:100%; font-family:'Caveat',cursive; font-size:1.4rem; padding:.25em .8em;
  background:#fff; border:2.5px solid var(--sk-ink); border-radius:var(--sk-rough);
  box-shadow:4px 4px 0 rgba(228,87,46,.35); outline:none;
}
.sk-filter input:focus{ box-shadow:4px 4px 0 var(--sk-teal); }

a{ color:var(--sk-red); text-decoration-style:wavy; text-decoration-thickness:1px; }
blockquote{ border-left:4px solid var(--sk-teal); }
SKETCHYCSS
}

_write_filter_js() {
    cat > "$1" <<'FILTERJS'
<script>
document.addEventListener('DOMContentLoaded', function () {
  var box = document.getElementById('sk-filter');
  if (!box) return;
  var rows = Array.from(document.querySelectorAll('.bundle-index tbody tr'));
  var toc  = Array.from(document.querySelectorAll('#TOC a, nav[role=doc-toc] a'));
  box.addEventListener('input', function () {
    var q = box.value.trim().toLowerCase();
    rows.forEach(function (r) {
      r.style.display = (!q || r.textContent.toLowerCase().includes(q)) ? '' : 'none';
    });
    toc.forEach(function (a) {
      var li = a.closest('li') || a;
      li.style.display = (!q || a.textContent.toLowerCase().includes(q)) ? '' : 'none';
    });
  });
});
</script>
FILTERJS
}

: > "$TMPD/manifest"
: > "$TMPD/pages"

# =========================================================== MODE SINGLE ======

if [ "$MODE" != "site" ]; then

cat >> "$OUT" <<EOF
---
title: "$TITLE"
subtitle: "$SUBTITLE"
date: "$NOW"
date-format: "YYYY-MM-DD HH:mm"
lang: fr
format:
  html:
    theme: [sketchy]
    css: sketchy.css
    toc: true
    toc-depth: 2
    toc-location: left
    toc-title: "Fichiers"
    toc-expand: 1
    number-sections: false
    anchor-sections: true
    smooth-scroll: true
    code-copy: true
    code-overflow: scroll
    code-block-bg: true
    highlight-style: arrow
    embed-resources: $EMBED
    include-after-body: sk-filter.html
    grid:
      sidebar-width: 340px
      body-width: 1150px
      margin-width: 140px
    mermaid:
      theme: neutral
---

::: {.sk-filter}
<input id="sk-filter" type="text" placeholder="filtrer les fichiers (ex: dsp, .c, shunt)..." autocomplete="off">
:::

::: {.callout-note appearance="simple" icon=false}
**Bundle plat du depot** - doc, tests, headers, sources, python, shell.
Scope \`$SCOPE\` - git \`$GITBRANCH@$GITREV\` - genere $NOW
- exclusions \`$EXCLUDE_RE\` - troncature ${MAX_KB} ko/fichier.
:::
EOF

_write_css "$(dirname "$OUT")/sketchy.css"
_write_filter_js "$(dirname "$OUT")/sk-filter.html"

fi

# ============================================================= MODE SITE ======

if [ "$MODE" = "site" ]; then
    _write_css "$OUTDIR/sketchy.css"
    _write_filter_js "$OUTDIR/sk-filter.html"
fi

# ------------------------------------------------------------------ sections

_section 1 "1 - Documentation" ''                                   '*.md' '*.mmd' '*.qmd'
_section 2 "2 - Tests"         ''                                   'test_*.py' 'conftest.py'
_section 3 "3 - Headers"       ''                                   '*.h'
_section 4 "4 - Sources"       ''                                   '*.c' '*.cpp'
_section 5 "5 - Python"        '/tests/|test_.*\.py$|conftest\.py$' '*.py'
_section 6 "6 - Shell"         ''                                   '*.sh' '*.bash'

# ------------------------------------------------------- index + config site --

if [ "$MODE" = "site" ]; then

    # index.qmd : table globale, liens inter-pages
    {
        cat <<EOF
---
title: "$TITLE"
subtitle: "$SUBTITLE"
date: "$NOW"
date-format: "YYYY-MM-DD HH:mm"
---

::: {.callout-note appearance="simple" icon=false}
**Bundle du depot** - doc, tests, headers, sources, python, shell.
Scope \`$SCOPE\` - git \`$GITBRANCH@$GITREV\` - genere $NOW
- exclusions \`$EXCLUDE_RE\` - troncature ${MAX_KB} ko/fichier - pages de ${SPLIT_KB} ko max.
:::

::: {.sk-filter}
<input id="sk-filter" type="text" placeholder="filtrer les fichiers (ex: dsp, .c, shunt)..." autocomplete="off">
:::
EOF
        cursec=""
        while IFS=$'\t' read -r sec page f anchor nlines size; do
            if [ "$sec" != "$cursec" ]; then
                [ -n "$cursec" ] && printf '\n:::\n'
                title="$(awk -F'\t' -v s="$sec" '$1==s {print $2; exit}' "$TMPD/pages")"
                printf '\n# %s\n\n::: {.bundle-index}\n\n' "$title"
                printf '| Fichier | Lignes | Taille |\n|:---|---:|---:|\n'
                cursec="$sec"
            fi
            printf '| [`%s`](%s.html#%s) | %s | %s |\n' "$f" "$page" "$anchor" "$nlines" "$size"
        done < "$TMPD/manifest"
        printf '\n:::\n'
    } > "$OUTDIR/index.qmd"

    # _quarto.yml
    {
        cat <<EOF
project:
  type: website
  output-dir: _site

website:
  title: "$TITLE"
  search:
    location: sidebar
    type: textbox
  sidebar:
    style: floating
    collapse-level: 1
    contents:
      - text: "Index"
        href: index.qmd
EOF
        cursec=""
        while IFS=$'\t' read -r sec title page part; do
            if [ "$sec" != "$cursec" ]; then
                printf '      - section: "%s"\n        contents:\n' "$title"
                cursec="$sec"
            fi
            printf '          - href: %s\n            text: "%s"\n' "$page" "$part"
        done < "$TMPD/pages"

        cat <<EOF

format:
  html:
    theme: [sketchy]
    css: sketchy.css
    toc: true
    toc-depth: 2
    toc-location: right
    toc-title: "Fichiers"
    number-sections: false
    anchor-sections: true
    smooth-scroll: true
    code-copy: true
    code-overflow: scroll
    code-block-bg: true
    highlight-style: arrow
    embed-resources: false
    include-after-body: sk-filter.html
    mermaid:
      theme: neutral
EOF
    } > "$OUTDIR/_quarto.yml"
fi

# -------------------------------------------------------------------- footer

if [ "$MODE" != "site" ]; then
cat >> "$OUT" <<EOF

# Fin du bundle

::: {.section-count}
genere $NOW - \`$(basename "$0")\`
:::
EOF
fi

echo
echo "=== DONE ==="
if [ "$MODE" = "site" ]; then
    echo "Output : $OUTDIR/  ($(ls "$OUTDIR"/*.qmd | wc -l) pages, $(du -sh "$OUTDIR" | cut -f1))"
    echo "Rendu  : (cd \"$OUTDIR\" && quarto render)"
    echo "Preview: (cd \"$OUTDIR\" && quarto preview)"
else
    echo "Output : $OUT ($(du -h "$OUT" | cut -f1), $(wc -l < "$OUT") lignes)"
    echo "         + sketchy.css + sk-filter.html dans $(dirname "$OUT")/"
    echo "Rendu  : quarto render \"$OUT\"   (sortie dans $(dirname "$OUT")/)"
    if [ "$(wc -c < "$OUT")" -gt 1500000 ]; then
        echo
        echo "!! $(du -h "$OUT" | cut -f1) dans un seul document : Deno risque de casser"
        echo "   (RangeError: Maximum call stack size exceeded). Utilise MODE=site."
    fi
fi

if [ "$RENDER" = "1" ]; then
    if command -v quarto >/dev/null 2>&1; then
        if [ "$MODE" = "site" ]; then (cd "$OUTDIR" && quarto render); else quarto render "$OUT"; fi
    else
        echo "quarto introuvable dans le PATH - rendu ignore." >&2
    fi
fi

full_text.sh

.sh 166 lignes - 5,9KB

full_text.sh
#!/bin/bash
# full_text.sh - Concat doc + tests + .h + .c + .py + .sh dans cet ordre
# en un seul .txt brut (separateurs ASCII, sans markdown fences).
# Pour ingestion LLM / archive plate.
#
# Usage :
#   ./full_text.sh                          # -> /tmp/calypso-full.txt
#   ./full_text.sh out.txt                  # custom output
#   SCOPE=hw/arm/calypso ./full_text.sh     # only this subdir
#   EXCLUDE='build|pc-bios' ./full_text.sh  # extra excludes

set -uo pipefail

OUT="${1:-./calypso-full.txt}"
SCOPE="${SCOPE:-.}"
EXCLUDE_RE="${EXCLUDE:-subprojects|build|pc-bios|tests/functional|tests/qtest|tests/unit|tests/migration|tests/qemu-iotests|node_modules|\.git|\.pytest_cache}"

# Si $OUT est un dossier, append le nom de fichier par defaut.
# Evite le cas "./full_text.sh /home/nirvana/qemu-calypso" -> echo >> dossier crash.
if [ -d "$OUT" ]; then
    OUT="${OUT%/}/calypso-full.txt"
fi

HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"

echo "=== full_text.sh ==="
echo "Scope    : $SCOPE"
echo "Output   : $OUT"
echo "Exclude  : $EXCLUDE_RE"
echo

: > "$OUT" || { echo "ERROR: cannot write to '$OUT' (permission ? path invalide ?)" >&2; exit 1; }

# ---- Header ----
cat >> "$OUT" <<EOF
================================================================================
Calypso QEMU - Full text bundle
Generated : $(date -Iseconds)
Scope     : $SCOPE
Sections  : 1.docs  2.tests  3.headers  4.sources  5.python  6.shell
================================================================================

EOF

# Helper : add a file with ASCII separator
_add() {
    local f="$1"
    local rel="${f#./}"
    local size=$(wc -c < "$f" 2>/dev/null || echo 0)
    local nlines=$(wc -l < "$f" 2>/dev/null || echo 0)
    {
        echo ""
        echo "================================================================================"
        echo "FILE: $rel"
        echo "SIZE: $size bytes, $nlines lines"
        echo "================================================================================"
        cat "$f"
        echo ""
    } >> "$OUT"
}

# Find files of a given pattern, filtered by EXCLUDE_RE
_find() {
    find "$SCOPE" -type f \( $1 \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" \
        | sort
}

# ---- 1) Documentation ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 1 : DOCUMENTATION (.md, .mmd, .qmd)"
    echo "################################################################################"
} >> "$OUT"
DOCS=$(find "$SCOPE" -type f \( -name "*.md" -o -name "*.mmd" -o -name "*.qmd" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" | sort)
N=$(echo "$DOCS" | grep -c . || echo 0)
echo "Section 1 : $N files"
echo "Total docs files : $N" >> "$OUT"
for f in $DOCS; do _add "$f"; done

# ---- 2) Tests ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 2 : TESTS (tests/*.py)"
    echo "################################################################################"
} >> "$OUT"
TESTS=$(find "$SCOPE" -type f \( -name "test_*.py" -o -name "conftest.py" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" | sort)
N=$(echo "$TESTS" | grep -c . || echo 0)
echo "Section 2 : $N files"
echo "Total tests files : $N" >> "$OUT"
for f in $TESTS; do _add "$f"; done

# ---- 3) Headers ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 3 : HEADERS (.h)"
    echo "################################################################################"
} >> "$OUT"
HDRS=$(find "$SCOPE" -type f -name "*.h" 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" | sort)
N=$(echo "$HDRS" | grep -c . || echo 0)
echo "Section 3 : $N files"
echo "Total headers files : $N" >> "$OUT"
for f in $HDRS; do _add "$f"; done

# ---- 4) Sources ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 4 : SOURCES (.c, .cpp)"
    echo "################################################################################"
} >> "$OUT"
SRCS=$(find "$SCOPE" -type f \( -name "*.c" -o -name "*.cpp" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" | sort)
N=$(echo "$SRCS" | grep -c . || echo 0)
echo "Section 4 : $N files"
echo "Total sources files : $N" >> "$OUT"
for f in $SRCS; do _add "$f"; done

# ---- 5) Python scripts (hors tests/) ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 5 : PYTHON SCRIPTS (hors tests/)"
    echo "################################################################################"
} >> "$OUT"
PYS=$(find "$SCOPE" -type f -name "*.py" 2>/dev/null \
       | grep -vE "$EXCLUDE_RE|/tests/|test_.*\.py$|conftest\.py$" | sort)
N=$(echo "$PYS" | grep -c . || echo 0)
echo "Section 5 : $N files"
echo "Total python files : $N" >> "$OUT"
for f in $PYS; do _add "$f"; done

# ---- 6) Shell scripts ----
{
    echo ""
    echo "################################################################################"
    echo "# SECTION 6 : SHELL SCRIPTS (.sh)"
    echo "################################################################################"
} >> "$OUT"
SHS=$(find "$SCOPE" -type f \( -name "*.sh" -o -name "*.bash" \) 2>/dev/null \
        | grep -vE "$EXCLUDE_RE" | sort)
N=$(echo "$SHS" | grep -c . || echo 0)
echo "Section 6 : $N files"
echo "Total shell files : $N" >> "$OUT"
for f in $SHS; do _add "$f"; done

# ---- Footer ----
{
    echo ""
    echo "================================================================================"
    echo "END OF BUNDLE - generated $(date -Iseconds)"
    echo "================================================================================"
} >> "$OUT"

echo
echo "=== DONE ==="
echo "Output : $OUT"
echo "Size   : $(du -h "$OUT" | cut -f1)"
echo "Lines  : $(wc -l < "$OUT")"

generate_configs.sh

.sh 962 lignes - 52KB

generate_configs.sh
#!/usr/bin/env bash
# =============================================================================
#  generate_configs.sh - le SEUL endroit ou la configuration du reseau est
#                        decidee, et le seul qui sait substituer les gabarits.
# =============================================================================
#
#  POURQUOI CE FICHIER EXISTE
#  --------------------------
#  La mecanique est inchangee : les fichiers de `configs/` contiennent des
#  marqueurs `__MCC__`, `__ARFCN__`, `__KI__`... qu'une fonction remplace au
#  demarrage. Ce qui change, c'est qu'il n'y a plus DEUX implementations de
#  cette fonction - `start.sh` en portait une copie de 132 lignes et
#  `lib/gabarits.sh` une autre de 70, identiques au formatage pres, qui
#  derivaient donc en silence. Elles vivent ici, une fois.
#
#  Et surtout : les valeurs n'etaient calculees NULLE PART de facon visible.
#  ARFCN, BSIC, IMSI, IMEI, Ki etaient des formules enfouies au milieu de la
#  substitution (`arfcn=$(( 512 + op_id * 2 ))`). Pour changer l'ARFCN il
#  fallait trouver et editer la formule. Desormais ces formules ne sont plus
#  que des DEFAUTS : `globals.conf`, a la racine, les expose toutes et gagne.
#
#  DEUX USAGES
#  -----------
#    ./generate_configs.sh [--force] [--show] [CLE=VALEUR ...]
#         ecrit/complete globals.conf a la racine, puis affiche l'effectif.
#         Sans --force, un globals.conf existant n'est JAMAIS ecrase : c'est
#         ton fichier, on ne reecrit pas par-dessus tes reglages.
#
#    . ./generate_configs.sh
#         charge globals.conf et definit apply_config_templates().
#         C'est ce que font start.sh (hote) et lib/gabarits.sh.
#
#  QUI APPELLE QUOI
#  ----------------
#    start.sh          -> execute ce script (genere globals.conf) puis le source
#    start-direct.sh   -> source globals.conf (docker : la conf est deja la)
#    lib/gabarits.sh   -> source ce script pour apply_config_templates()
#
#  MULTI-OPERATEUR
#  ---------------
#  En mode `virtual` (N_OPERATORS > 1) chaque operateur a besoin de valeurs
#  DISTINCTES. Une valeur posee dans globals.conf s'applique alors a TOUS -
#  ce qui est presque toujours faux pour ARFCN, LAC, IMSI... Laisse-les vides
#  (le defaut) pour garder la derivation par operateur, et ne fixe que ce que
#  tu veux vraiment figer. Le script te previent si tu figes un champ
#  per-operateur avec N_OPERATORS > 1.
#
#  SPDX-License-Identifier: GPL-2.0-or-later
# =============================================================================

_GC_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_GC_FILE="$_GC_HERE/globals.conf"

# ── Les reglages exposes, avec leur defaut historique ────────────────────────
# Format : CLE|VALEUR-PAR-DEFAUT|PER-OP|COMMENTAIRE
#   PER-OP=1 -> la valeur est normalement derivee du numero d'operateur ;
#               la figer n'a de sens qu'avec un seul operateur.
#
#  ATTENTION : le corps de _gc_table est une TABLE, pas du shell. La seule ligne
#  non-donnee admise est « #SECTION|<titre> ». Un « # » ordinaire y devient une
#  entree malformee -- verifie le 27/08, en cassant le module « gabarits » du run.
#  Toute explication se met donc ICI, au-dessus de la fonction.
#
#  [2026-08-27] A5/1 EST LE DEFAUT ASSUME, ET IL A UN COUT CONNU. Le mobile
#  derriere QEMU ne dechiffre pas encore l'A5/1 : le chemin cipher du modele n'est
#  pas exerce (hw/arm/calypso/l1ctl_sock.c, « s'execute donc JAMAIS -- a verifier
#  avant d'activer l'A5/1 »). Comptage des rejets LAPDm dans mobile.log de part et
#  d'autre du CIPHERING MODE COMPLETE : 2 avant, 477 apres, et le LU s'arretait la
#  (T3210). Le side-car osmocom-bb, lui, traite l'A5/1 sans broncher et obtient
#  son LU ACCEPT : c'est le chemin emule qui doit rattraper, pas le reseau qui
#  doit baisser la garde. Repli de travail : ENCRYPTION="a5 0" en ligne de
#  commande, qui traverse jusqu'au fichier pose depuis que 08-gabarits.sh
#  l'exporte.
_gc_table() {
cat <<'TABLE'
#SECTION|Identite du reseau
MCC|001|0|Mobile Country Code (3 chiffres)
MNC|01|0|Mobile Network Code (2 chiffres)
OP_NAME|OsmoQEMU|0|Nom court et long du reseau
#SECTION|Securite et carte SIM
ENCRYPTION|a5 1|0|Chiffrement : "a5 1" (defaut), "a5 0" (aucun), "a5 0 1 3"...
SIM_ALGO|comp128|0|Algorithme d'authentification (comp128, xor...)
KI||1|Ki de la SIM, 16 octets hexa espaces
IMSI||1|IMSI du mobile
IMEI||1|IMEI du mobile
#SECTION|Radio
ARFCN||1|Canal radio (a accorder avec BAND)
BAND|DCS1800|0|Bande : GSM900, DCS1800, PCS1900, GSM850
BSIC||1|Base Station Identity Code, 0..63
LAC||1|Location Area Code
CELL_ID||1|Cell identity
IPA_UNIT_ID||1|Identifiant IPA du BTS
MS_MAX_POWER|15|0|Puissance max autorisee au mobile (0..15)
RXLEV_ACCESS_MIN|0|0|Niveau recu minimal pour acceder a la cellule
CELL_RESEL_HYST|4|0|Hysteresis de reselection de cellule (dB)
T3109|5|0|Timer T3109 du BSC (s) - liberation de canal
#SECTION|GPRS
BVCI||1|BSSGP Virtual Connection Id
NSEI||1|Network Service Entity Id
NSVCI||1|NS Virtual Connection Id
APN|internet|0|Nom du point d'acces
#SECTION|Services
SMS_SC||1|Numero du centre SMS
#SECTION|Dimensionnement
MS_COUNT|2|0|Nombre de mobiles simules
N_OPERATORS|1|0|Nombre d'operateurs (mode "virtual")
HOST_IP|127.0.0.1|0|Adresse de l'hote vue par la pile
TABLE
}

# ── Ecriture de globals.conf ─────────────────────────────────────────────────
_gc_write() {
    local force=$1
    if [ -f "$_GC_FILE" ] && [ "$force" != "1" ]; then
        printf '  globals.conf existe deja - conserve (--force pour regenerer)\n'
        return 0
    fi
    {
        printf '# ═══════════════════════════════════════════════════════════════\n'
        printf '#  globals.conf - la configuration du reseau, en un seul endroit.\n'
        printf '# ═══════════════════════════════════════════════════════════════\n'
        printf '#\n'
        printf '#  Edite ce fichier a la main : il ne sera JAMAIS reecrit par un run.\n'
        printf '#  Seul "./generate_configs.sh --force" le regenere.\n'
        printf '#\n'
        printf '#  CE FICHIER FAIT AUTORITE - SUR LES VARIABLES QU'"'"'IL DECLARE, ET SUR\n'
        printf '#  ELLES SEULES. Il est lu en dernier : les %s reglages ci-dessous\n' "$(_gc_table | grep -vc '^#SECTION')"
        printf '#  ecrasent leur homonyme dans l'"'"'environnement. Tout le reste (CALYPSO_*,\n'
        printf '#  LOG_DIR, MODE...) passe au travers, intact. Deux facons de le modifier :\n'
        printf '#      vim globals.conf\n'
        printf '#      ./generate_configs.sh ARFCN=520\n'
        printf '#\n'
        printf '#  Un champ VIDE = valeur calculee automatiquement. Les champs notes\n'
        printf '#  [auto/operateur] se deduisent du numero d'"'"'operateur : ne les fige\n'
        printf '#  que si N_OPERATORS=1, sinon tous les operateurs se marchent dessus.\n'
        printf '# ═══════════════════════════════════════════════════════════════\n'
        local key def perop comment
        while IFS='|' read -r key def perop comment; do
            [ -n "$key" ] || continue
            if [ "$key" = "#SECTION" ]; then
                printf '\n# ── %s %s\n\n' "$def" \
                    "$(printf '%.0s─' $(seq 1 $((56 - ${#def}))))"
                continue
            fi
            printf '# %s%s\n' "$comment" \
                "$([ "$perop" = 1 ] && printf '  [auto/operateur]')"
            case "$def" in
                *" "*|"") printf '%s="%s"\n' "$key" "$def" ;;
                *)        printf '%s=%s\n'   "$key" "$def" ;;
            esac
        done < <(_gc_table)
    } > "$_GC_FILE"
    printf '  globals.conf ecrit (%s reglages)\n' \
           "$(_gc_table | grep -vc '^#SECTION')"
}

# ── Poser une valeur DANS le fichier, en place, sans casser la mise en forme ──
_gc_set() {
    local key="${1%%=*}" val="${1#*=}"
    _gc_table | grep -q "^${key}|" || {
        printf '  ⚠ reglage inconnu : %s (voir --show)\n' "$key" >&2; return 1; }
    [ -f "$_GC_FILE" ] || _gc_write 1 >/dev/null
    local q; case "$val" in *" "*|"") q="\"$val\"" ;; *) q="$val" ;; esac
    if grep -qE "^${key}=" "$_GC_FILE"; then
        sed -i "s|^${key}=.*|${key}=${q}|" "$_GC_FILE"
    else
        printf '%s=%s\n' "$key" "$q" >> "$_GC_FILE"
    fi
    printf '  %s = %s\n' "$key" "${val:-(auto)}"
}

# ── Chargement ───────────────────────────────────────────────────────────────
# Charge globals.conf, puis COMPLETE avec les defauts de la table.
#
# L'ordre compte : le fichier est lu d'abord et fait autorite sur les variables
# qu'il declare ; ensuite, toute cle encore vide recoit son defaut d'usine.
#
# Ce filet n'est pas decoratif. Sans lui, un globals.conf ABSENT - clone frais,
# fichier volontairement gitignore, suppression accidentelle - laissait BAND,
# SIM_ALGO, APN, T3109... vides, et la substitution les ecrivait tels quels dans
# les configs : "band" sans valeur, "ki  <hexa>" sans algorithme. osmo-bsc
# refuse alors de demarrer, et rien dans le journal ne dit pourquoi. Constate
# en vrai le 2026-08-03. Les defauts appartiennent au SCRIPT ; globals.conf
# n'est qu'une couche de surcharge par-dessus.
gc_load() {
    local key def perop comment
    # ── LA LIGNE DE COMMANDE PRIME SUR globals.conf ──────────────────────────
    # globals.conf affecte SECHEMENT (MCC=001, ENCRYPTION="a5 1", ...). Le sourcer
    # en premier ecrasait donc ce que l'appelant venait de poser dans son
    # environnement : sur une machine qui a un globals.conf,
    #     ENCRYPTION="a5 0" ./run.sh
    # etait avale sans un mot et le run repartait en a5 1. Constate le 2026-08-27,
    # et c'est la meme famille de panne que les deux autres du jour : une valeur
    # qui depend du chemin par lequel on entre, sans que rien ne le signale.
    #
    # On releve donc les cles DEJA presentes dans l'environnement AVANT de sourcer,
    # et on les repose APRES. Precedence retablie, dans l'ordre habituel :
    #     ligne de commande  >  globals.conf  >  defaut de _gc_table
    local -A _gc_env=()
    while IFS='|' read -r key def perop comment; do
        [ -n "$key" ] && [ "$key" != "#SECTION" ] || continue
        if [ -n "${!key:-}" ]; then _gc_env["$key"]="${!key}"; fi
    done < <(_gc_table)

    [ -f "$_GC_FILE" ] && . "$_GC_FILE"

    if [ "${#_gc_env[@]}" -gt 0 ]; then
        for key in "${!_gc_env[@]}"; do printf -v "$key" '%s' "${_gc_env[$key]}"; done
    fi

    while IFS='|' read -r key def perop comment; do
        [ -n "$key" ] && [ "$key" != "#SECTION" ] || continue
        [ -n "${!key:-}" ] || printf -v "$key" '%s' "$def"
    done < <(_gc_table)
    return 0
}

# ── IMEI tire au hasard, avec sa cle de Luhn ────────────────────────────────
# L'IMEI etait derive du numero d'operateur : deux bancs identiques rendaient
# les memes, et deux MS du meme operateur ne se distinguaient plus dans une
# trace. On garde un TAC plausible (35892500) et on tire les six chiffres de
# serie, puis on calcule la 15e position : un IMEI dont la cle est fausse est
# rejete par certains equipements, et surtout il se voit tout de suite comme
# fabrique a la main.
rand_imei() {
    local body d i sum=0 dbl parity
    body="35892500$(printf '%06d' "$(( (RANDOM << 15 | RANDOM) % 1000000 ))")"
    # Luhn sur les 14 chiffres : on double un chiffre sur deux en partant de la
    # droite, en repliant les resultats a deux chiffres.
    for (( i = ${#body} - 1; i >= 0; i-- )); do
        d=${body:i:1}
        parity=$(( (${#body} - 1 - i) % 2 ))
        if [ "$parity" -eq 0 ]; then
            dbl=$(( d * 2 )); [ "$dbl" -gt 9 ] && dbl=$(( dbl - 9 ))
            sum=$(( sum + dbl ))
        else
            sum=$(( sum + d ))
        fi
    done
    printf '%s%d' "$body" "$(( (10 - sum % 10) % 10 ))"
}

# ── LE PLAN DE NUMEROTATION, EN UN SEUL ENDROIT ──────────────────────────────
#
#   MSISDN = <noeud> 00 <operateur> <rang du MS sur 2 chiffres>
#            noeud 1, operateur 1, MS 1  ->  100101
#            noeud 1, operateur 1, MS 2  ->  100102
#            noeud 2, operateur 1, MS 1  ->  200101
#
# POURQUOI LE NOEUD EN TETE. Le plan precedent commencait par un 6 fixe
# (600000 + op*100 + ms) : le numero ne disait donc RIEN de la machine qui le
# porte. Sur un banc a plusieurs noeuds, l'operateur 1 du noeud 1 et l'operateur
# 1 du noeud 2 revendiquaient tous deux 600101 - le meme abonne a deux endroits,
# et un SMS ou un appel qui part chez le premier qui repond. Le premier chiffre
# devient donc l'adresse du noeud, et le routage (SMS comme dialplan) se lit
# directement dans le numero.
#
# CE QUI NE CHANGE PAS, ET C'EST VOULU : l'IMSI (MCC MNC <op sur 4> <ms sur 6>)
# et le Ki (00 11 ... dd <ms> <op>). Ces deux-la sont derives a l'IDENTIQUE dans
# quatre endroits - ici pour mobile.cfg, start-direct.sh pour les mobiles,
# run_modules/21-abonnes-hlr.sh et start.sh pour le HLR - et ils doivent rester
# d'accord au bit pres, sans quoi l'authentification echoue sur un Kc different
# des deux cotes, sans autre trace qu'un rejet de CIPHER MODE. On ne touche donc
# qu'au MSISDN, qui n'entre dans aucun calcul de securite.
osmo_msisdn_pfx() { printf '%s00' "${1:-1}"; }                       # <noeud>00
osmo_msisdn()     { printf '%d' "$(( ${1:-1} * 100000 + ${2:-1} * 100 + ${3:-1} ))"; }

# Le noeud tel que le voit la generation : l'environnement d'abord (c'est ce que
# start.sh passe a un conteneur et ce que start-direct.sh exporte avant un
# --regen), /etc/osmo-role ensuite, 1 par defaut. JAMAIS vide : un plan de
# numerotation sans noeud, c'est le plan de tout le monde.
osmo_node_id() {
    local n="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
    if [ -z "$n" ] && [ -r "${ROLE_FILE:-/etc/osmo-role}" ]; then
        n="$(awk -F= '/^OSMO_WAN_NODE=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' \
             "${ROLE_FILE:-/etc/osmo-role}" 2>/dev/null)"
    fi
    case "$n" in [1-9]) ;; *) n=1 ;; esac
    printf '%s' "$n"
}

# ── La substitution des gabarits - implementation UNIQUE ─────────────────────
#  Signature inchangee, pour rester compatible avec les deux appelants :
#    apply_config_templates DEST CONTAINER_IP GATEWAY_IP OP_ID \
#                           PC_MSC PC_STP PC_BSC MCC MNC OP_NAME \
#                           INTER_STP INTER_STP_SHUTDOWN N_OPERATORS
apply_config_templates() {
    local dest=$1 container_ip=$2 gateway_ip=$3 op_id=$4

    # ── OSMO_NO_REGEN : ne pas refaire ce qui vient d'etre fait ─────────────
    # run.sh (qosmo-grgsm) peut rejouer cette fonction au demarrage. Dans un
    # conteneur, c'est une REGRESSION : les configs viennent d'etre generees par
    # start.sh, avec l'identite du noeud, ses point codes et son inter-STP. Les
    # regenerer depuis les gabarits les remplace par les valeurs par defaut -
    # 1.1.2 pour tout le monde, ASP vers le hub docker, en shutdown - et
    # l'interco meurt sans un mot.
    #
    # Ce commentaire nommait run_modules/08-gabarits.sh comme cet appelant. Ce
    # module N'EXISTE PAS - start-direct.sh le constate deja. Les appelants
    # reels sont start.sh (deux fois : la boucle des operateurs et le mode
    # net-host) et build-iso.sh, qui construit l'arbre de l'image. Decrire un
    # module imaginaire a fait regler plus bas une option sur une configuration
    # que personne ne pose.
    #
    # start-direct.sh pose donc cette variable par defaut, et --regen la leve
    # pour qui veut vraiment repartir des gabarits.
    if [ "${OSMO_NO_REGEN:-0}" = "1" ]; then
        echo "  gabarits : conserves (OSMO_NO_REGEN=1 ; --regen pour regenerer)"
        return 0
    fi
    local pc_msc=$5 pc_stp=$6 pc_bsc=$7 mcc=$8 mnc=$9 op_name=${10}

    # ── Le PLMN, avant d'ecrire quoi que ce soit ────────────────────────────
    # osmo-msc REFUSE "network country code 000" : il abandonne la lecture du
    # fichier a cette ligne, sort, et systemd le relance - trois fois par
    # seconde, indefiniment. Le journal du lanceur, lui, affiche une croix a
    # cote d'osmo-msc puis annonce "Core Osmocom pret" : on cherche la panne
    # dans le SS7 ou dans la BTS pendant que la cause tient en trois chiffres.
    # Un MCC vide donne 000 par printf ; c'est ainsi que c'est arrive.
    # On s'arrete ICI, ou le nom du parametre est encore connu.
    case "$mcc" in
        ''|000|00|0) echo "  ERREUR : MCC invalide ('$mcc') pour l'operateur $op_id - osmo-msc refuserait la config" >&2; return 1 ;;
    esac
    case "$mnc" in
        ''|'-') echo "  ERREUR : MNC vide pour l'operateur $op_id" >&2; return 1 ;;
    esac
    local inter_stp=${11} inter_stp_shutdown=${12} n_operators=${13}

    # Le noeud entre dans le plan de numerotation : MSISDN = <noeud>00<op><ms>.
    # Il n'entre PAS dans l'IMSI ni dans le Ki - ceux-la sont derives a
    # l'identique dans quatre endroits et doivent le rester (voir osmo_msisdn).
    local node msisdn_pfx
    node="$(osmo_node_id)"
    msisdn_pfx="$(osmo_msisdn_pfx "$node")"

    mkdir -p "$dest/osmocom" "$dest/asterisk" "$dest/bb"
    local f bn

    for f in configs/*.cfg; do
        [ "$(basename "$f")" = "osmo-stp-interop.cfg" ] && continue
        cp "$f" "$dest/osmocom/"
    done
    [ -f "configs/osmo-bts-virtual.cfg" ] && cp "configs/osmo-bts-virtual.cfg" "$dest/osmocom/"
    for f in configs/*.conf; do
        bn=$(basename "$f"); [ "$bn" = "sms-routing.conf" ] && continue
        cp "$f" "$dest/asterisk/"
    done
    # scripts/ en entier : c'est le comportement de start.sh, le plus permissif
    # des deux qui coexistaient. lib/gabarits.sh en copiait une liste blanche,
    # qui omettait silencieusement tout script ajoute depuis.
    cp scripts/* "$dest/osmocom/" 2>/dev/null || true
    chmod +x "$dest/osmocom"/*.sh 2>/dev/null || true

    if [ -f "configs/mobile.cfg.template" ]; then
        cp "configs/mobile.cfg.template" "$dest/bb/mobile.cfg"
        cp "configs/mobile.cfg.template" "$dest/bb/mobile_group1.cfg"
    elif [ -f "configs/mobile.cfg" ]; then
        cp "configs/mobile.cfg" "$dest/bb/mobile.cfg"
        cp "configs/mobile.cfg" "$dest/bb/mobile_group1.cfg"
    fi

    local rctx_msc rctx_stp rctx_bsc rctx_inter
    rctx_msc=$(op_rctx_msc "$op_id");   rctx_stp=$(op_rctx_stp "$op_id")
    rctx_bsc=$(op_rctx_bsc "$op_id")
    # RCTX_INTER_OVERRIDE : pose par build-iso.sh quand l'image est un NOEUD DE
    # WAN. Le routing context identifie l'AS aupres du hub ; la formule locale
    # (op*100+50) donne la meme valeur sur tous les noeuds, donc plusieurs AS
    # indiscernables sur le meme hub. Vide = formule d'origine, rien ne change.
    rctx_inter="${RCTX_INTER_OVERRIDE:-$(op_rctx_inter "$op_id")}"

    # ── Les valeurs. globals.conf gagne ; sinon, la formule historique. ──────
    local arfcn="${ARFCN:-$(( 512 + op_id * 2 ))}"
    # ── La SECONDE BTS (le side-car) doit avoir son propre canal ────────────
    # Elle etait figee sur l'ARFCN 516 et l'unit-id 6002 - les valeurs qui vont
    # a l'operateur 1, et a lui seul. Pour l'operateur 2, dont la BTS
    # principale tombe justement sur 516, les DEUX cellules emettaient sur la
    # meme frequence : dans un milieu virtuel ou fake_trx apparie par
    # frequence, leurs bursts se melangent. Constate sur le banc -
    #     TRX 0 of BTS 0 is on ARFCN 516
    #     TRX 0 of BTS 1 is on ARFCN 516
    # - pendant que le mobile envoyait ses RACH sans jamais recevoir
    # d'Immediate Assignment. L'operateur 1 n'y echappait que par coincidence.
    # Le decalage de 100 met les side-cars hors d'atteinte de tout le plan
    # principal (614, 616, 618... contre 514, 516, 518), et reste dans la
    # bande DCS1800 (512-885).
    local arfcn_bts1="${ARFCN_BTS1:-$(( 612 + op_id * 2 ))}"
    # +2 sur le pas de 10 de la BTS principale : 6010/6012, 6020/6022...
    local ipa_unit_id_bts1="${IPA_UNIT_ID_BTS1:-$(( 6000 + op_id * 10 + 2 ))}"
    local bsic="${BSIC:-$(( (op_id * 7) % 64 ))}"
    # Le side-car a son propre BSIC : c'est lui qui departage deux cellules
    # dans les mesures du mobile, et deux cellules du meme operateur qui
    # portent le meme code sont indiscernables l'une de l'autre.
    local bsic_bts1="${BSIC_BTS1:-$(( (bsic + 1) % 64 ))}"
    local lac="${LAC:-0x000${op_id}}"
    local cell_id="${CELL_ID:-$(( 6000 + op_id ))}"
    # Pas 6000+op_id : le gabarit de la SECONDE BTS
    # (configs/osmo-bts-trx-bts1.cfg) porte "ipa unit-id 6002 0" EN DUR. Avec
    # l'ancienne formule, l'operateur 2 recevait lui aussi 6002 : ses deux BTS se
    # presentaient au BSC sous le meme identifiant, il en acceptait une et
    # ejectait l'autre, en boucle. Symptome observe : "BTS failures : 26 OML,
    # 26 RSL", un lien OML qui se reconnecte toutes les deux secondes, et aucun
    # abonne attache - alors que l'operateur 1 (6001) fonctionnait, par simple
    # coincidence de numerotation.
    # Le pas de 10 laisse les valeurs de la seconde BTS hors d'atteinte, quel
    # que soit le nombre d'operateurs.
    local ipa_unit_id="${IPA_UNIT_ID:-$(( 6000 + op_id * 10 ))}"
    local bvci="${BVCI:-$(( op_id * 10 + 2 ))}"
    local nsei="${NSEI:-$(( op_id * 10 ))}"
    local nsvci="${NSVCI:-$(( op_id * 10 ))}"
    # Meme plan d'abonne que le HLR (start.sh, section "HLR subscribers") :
    #    IMSI = MCC MNC <operateur sur 4 chiffres> <index du MS sur 6>
    # Ce gabarit sert le MS #1. L'ancien %010d donnait 0000000002 la ou le HLR
    # provisionne 0002000001 : la fiche existait, l'IMSI presente ne la visait
    # pas, et l'attachement echouait sans que rien ne dise pourquoi.
    local imsi="${IMSI:-${mcc}${mnc}$(printf '%04d%06d' "$op_id" 1)}"
    local imei="${IMEI:-$(rand_imei)}"
    # IMEI : tire au hasard a chaque generation (voir rand_imei).
    # Ki : deux derniers octets = index du MS, puis operateur - l'ordre du HLR
    # (printf '...dd%02x%02x' ms_idx op_id). Le "ff" final ne correspondait a
    # aucune fiche : sans Ki commun, pas de Kc, et le CIPHER MODE est rejete.
    local ki="${KI:-00 11 22 33 44 55 66 77 88 99 aa bb cc dd $(printf '%02x %02x' 1 "$op_id")}"
    local sms_sc="${SMS_SC:-+336661234$(printf '%04d' "$op_id")}"

    local inter_local_ip sip_remote_ip rtp_start rtp_end sip_host_port
    # Idem : sur un noeud de WAN, l'adresse source de l'ASP n'est pas une IP du
    # plan docker mais celle du segment - ou 0.0.0.0 quand elle vient du DHCP.
    inter_local_ip="${INTER_LOCAL_IP_OVERRIDE:-$(op_backbone_ip "$op_id")}"
    # La patte SIP vers Asterisk n'est PAS la dorsale : elle est locale dans les
    # deux topologies, et Asterisk ne l'identifie que sur la boucle (pjsip.conf
    # [gsm_msc-identify] match=127.0.0.1). Elle avait herite d'inter_local_ip -
    # donc de 172.20.0.11, absente de l'ISO - et tout INVITE sortait en
    # "Invalid argument (22)" puis 503. Variable distincte, defaut loopback.
    sip_remote_ip="${SIP_REMOTE_IP_OVERRIDE:-127.0.0.1}"
    rtp_start=$(linphone_rtp_start "$op_id"); rtp_end=$(linphone_rtp_end "$op_id")
    sip_host_port=$(linphone_sip_port "$op_id")

    # ── Le plan radio, ECRIT, a cote des configurations ─────────────────────
    # ARFCN, unit-id, PLMN etaient recalcules ailleurs a partir des memes
    # formules - dans start-direct.sh pour les mobiles, dans un module de
    # qosmo-grgsm pour le side-car. Trois copies d'une meme regle, et la serie de
    # pannes qui va avec : un unit-id 6002 fige, un ARFCN 516 fige, des IMSI
    # sur le PLMN de l'operateur 1. Chaque fois, les valeurs concordaient pour
    # l'operateur 1 et divergeaient pour les suivants.
    # Le generateur est le seul a decider. Il ECRIT donc ce qu'il a decide, et
    # les autres le LISENT au lieu de le refaire.
    {
        printf '# radio-plan.env - ecrit par generate_configs.sh. Ne pas editer.\n'
        printf '# Le plan radio et le PLMN de CET operateur, tels qu ils viennent\n'
        printf '# d etre poses dans les configurations voisines. start-direct.sh le\n'
        printf '# lit pour accorder les mobiles et pour piloter le side-car.\n'
        printf 'OPERATOR_ID=%s\n'      "$op_id"
        printf 'PLAN_NODE=%s\n'        "$node"
        printf 'PLAN_MSISDN_PFX=%s\n'  "$msisdn_pfx"
        printf 'PLAN_MCC=%s\n'         "$mcc"
        printf 'PLAN_MNC=%s\n'         "$mnc"
        printf 'PLAN_ARFCN=%s\n'       "$arfcn"
        printf 'PLAN_ARFCN_BTS1=%s\n'  "$arfcn_bts1"
        printf 'PLAN_UNIT_ID=%s\n'     "$ipa_unit_id"
        printf 'PLAN_UNIT_ID_BTS1=%s\n' "$ipa_unit_id_bts1"
        printf 'PLAN_BSIC_BTS1=%s\n'    "$bsic_bts1"
        printf 'PLAN_LAC=%s\n'         "$lac"
        printf 'PLAN_BSIC=%s\n'        "$bsic"
        printf 'PLAN_CELL_ID=%s\n'     "$cell_id"
    } > "$dest/osmocom/radio-plan.env"

    # extensions.conf porte un #include "annuaire.conf" (les fiches d'abonnes,
    # ecrites par start.sh pour que le nom de l'appelant arrive au combine).
    # Asterisk avertit a chaque chargement quand un include manque, et un banc
    # monte sans start.sh - une VM, start-direct.sh seul - n'en a pas. On pose
    # donc toujours un fichier, quitte a ce qu'il soit vide : le repli _X. de
    # extensions.conf fait le reste, et l'avertissement disparait.
    if [ ! -f "$dest/asterisk/annuaire.conf" ]; then
        {
            printf '; annuaire.conf - fiches d abonnes, ecrites par start.sh.\n'
            printf '; Vide ici : ce banc a ete monte sans annuaire. Les appels\n'
            printf '; passent, ils affichent le numero au lieu du nom.\n'
            printf '[sub-annuaire]\n'
        } > "$dest/asterisk/annuaire.conf"
    fi

    for f in "$dest/osmocom"/*.cfg "$dest/asterisk"/*.conf "$dest/bb"/*.cfg; do
        [ -f "$f" ] || continue
        sed -i \
            -e "s|__ENCRYPTION__|${ENCRYPTION}|g" \
            -e "s|__INTER_NET_GATEWAY__|172.20.0.1|g" \
            -e "s|__CONTAINER_IP__|${container_ip}|g" \
            -e "s|__GATEWAY_IP__|${gateway_ip}|g" \
            -e "s|__HLR_IP__|127.0.0.2|g" \
            -e "s|__INTER_STP_IP__|${inter_stp}|g" \
            -e "s|__INTER_STP_SHUTDOWN__|${inter_stp_shutdown}|g" \
            -e "s|__INTER_LOCAL_IP__|${inter_local_ip}|g" \
            -e "s|__SIP_REMOTE_IP__|${sip_remote_ip}|g" \
            -e "s|__MSISDN_PFX__|${msisdn_pfx}|g" \
            -e "s|__OPERATOR_ID__|${op_id}|g" \
            -e "s|__PC_MSC__|${pc_msc}|g" -e "s|__PC_STP__|${pc_stp}|g" -e "s|__PC_BSC__|${pc_bsc}|g" \
            -e "s|__RCTX_MSC__|${rctx_msc}|g" -e "s|__RCTX_STP__|${rctx_stp}|g" \
            -e "s|__RCTX_BSC__|${rctx_bsc}|g" -e "s|__RCTX_INTER__|${rctx_inter}|g" \
            -e "s|__MCC__|${mcc}|g" -e "s|__MNC__|${mnc}|g" -e "s|__OP_NAME__|${op_name}|g" \
            -e "s|__ARFCN__|${arfcn}|g" -e "s|__IPA_UNIT_ID__|${ipa_unit_id}|g" \
            -e "s|__ARFCN_BTS1__|${arfcn_bts1}|g" \
            -e "s|__IPA_UNIT_ID_BTS1__|${ipa_unit_id_bts1}|g" \
            -e "s|__BSIC_BTS1__|${bsic_bts1}|g" \
            -e "s|__CELL_ID__|${cell_id}|g" -e "s|__BSIC__|${bsic}|g" -e "s|__LAC__|${lac}|g" \
            -e "s|__BVCI__|${bvci}|g" -e "s|__NSEI__|${nsei}|g" -e "s|__NSVCI__|${nsvci}|g" \
            -e "s|__IMSI__|${imsi}|g" -e "s|__IMEI__|${imei}|g" -e "s|__KI__|${ki}|g" \
            -e "s|__SMS_SC__|${sms_sc}|g" -e "s|__HOST_IP__|${HOST_IP}|g" \
            -e "s|__BAND__|${BAND}|g" -e "s|__SIM_ALGO__|${SIM_ALGO}|g" \
            -e "s|__MS_MAX_POWER__|${MS_MAX_POWER}|g" \
            -e "s|__RXLEV_ACCESS_MIN__|${RXLEV_ACCESS_MIN}|g" \
            -e "s|__CELL_RESEL_HYST__|${CELL_RESEL_HYST}|g" \
            -e "s|__T3109__|${T3109}|g" -e "s|__APN__|${APN}|g" \
            -e "s|__SIP_HOST_PORT__|${sip_host_port}|g" \
            -e "s|__ALSA_OUTPUT__|${ALSA_OUTPUT}|g" -e "s|__ALSA_INPUT__|${ALSA_INPUT}|g" \
            -e "s|__RTP_START__|${rtp_start}|g" -e "s|__RTP_END__|${rtp_end}|g" \
            "$f"
    done

    generate_pjsip_interop_trunks     "$op_id" "$n_operators" >> "$dest/asterisk/pjsip.conf"
    generate_extensions_interop_out   "$op_id" "$n_operators" >> "$dest/asterisk/extensions.conf"
    # Routes SMS (fallback = generateur par defaut). Condition d'echec : le
    # MSISDN reel du premier MS absent OU route parasite (op0000/op000).
    # Si non remplie -> regen.
    # Le controle suivait encore "600000 + op*100 + 1" alors que le generateur
    # ecrit desormais <noeud>00<op><ms> : il ne trouvait plus la ligne qu'il
    # venait d'ecrire et regenerait a chaque fois - sans effet visible, mais
    # c'est exactement le genre de test qui finit par masquer une vraie panne.
    _src="$dest/osmocom/sms-routing.conf"
    _generate_sms_routing_conf_fallback "$op_id" "$n_operators" >  "$_src"
    if ! grep -q "^$(osmo_msisdn "$node" "$op_id" 1) = " "$_src" 2>/dev/null \
       || grep -qE "^${op_id}0000? = " "$_src" 2>/dev/null; then
        _generate_sms_routing_conf_fallback "$op_id" "$n_operators" >  "$_src"
    fi

    # ── L'adressage SS7 du noeud : rendu a set-node-id.sh ────────────────────
    # Les gabarits portent les defauts d'une machine SEULE (point-code 1.1.2,
    # ASP vers 127.0.0.1, shutdown). Sur un noeud de WAN ils sont faux - et ils
    # revenaient ici a CHAQUE regeneration : l'adressage pose par set_stp_ip.sh
    # etait efface au demarrage suivant sans que rien ne le signale. L'interco
    # retombait alors que le fichier venait d'etre "regenere proprement".
    #
    # set-node-id.sh (ce que set_stp_ip.sh appelle lui-meme) est le seul endroit
    # qui tienne d'accord les trois fichiers - osmo-stp, osmo-msc, osmo-bsc - et
    # leurs point codes croises. On le rejoue APRES la substitution plutot que
    # de dupliquer ici un sed qui divergerait le jour ou le plan changerait.
    # Son echec est celui de la generation : des configs porteuses du point-code
    # du gabarit ne valent pas mieux que des configs non generees.
    _apply_node_ss7_addressing "$dest" || return 1
}

# Rejoue l'adressage SS7 du noeud sur les configs fraichement generees.
#   $1 = repertoire destination (celui qui contient osmocom/)
# Sans effet - et silencieux - quand la machine n'est pas un noeud de WAN, quand
# le script est absent, ou quand OSMO_NO_STP_IP=1 le demande explicitement.
_apply_node_ss7_addressing() {
    local dest="$1"
    local role_file="${ROLE_FILE:-/etc/osmo-role}"
    local here="${GEN_HERE:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"

    [ "${OSMO_NO_STP_IP:-0}" = "1" ] && return 0
    [ -d "$dest/osmocom" ] || return 0

    # ── L'ENVIRONNEMENT D'ABORD, le fichier de role ensuite ─────────────────
    # Cette fonction s'execute AUSSI dans un conteneur : run.sh (qosmo-grgsm) peut
    # y rejouer apply_config_templates, ce qui regenere tout /etc/osmocom a
    # partir des gabarits - et efface l'identite SS7 que start.sh venait
    # d'ecrire. Le conteneur repartait alors avec le plan du gabarit :
    # point-code 1.1.2 pour TOUS les operateurs, donc deux equipements a la meme
    # adresse SS7 et aucun ASP attache.
    #
    # Or le conteneur n'a pas de /etc/osmo-role - il a des VARIABLES, que
    # start.sh lui passe (OSMO_WAN_NODE, OSMO_HUB_IP). On les lit en premier :
    # c'est la seule source disponible la ou le probleme se produit.
    local role node hub
    node="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
    hub="${OSMO_HUB_IP:-}"
    role="${OSMO_ROLE:-}"
    if [ -r "$role_file" ]; then
        [ -n "$role" ] || role="$(awk -F= '/^OSMO_ROLE=/{gsub(/[ \r\t]/,"",$2);v=$2}    END{print v}' "$role_file")"
        [ -n "$node" ] || node="$(awk -F= '/^OSMO_WAN_NODE=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' "$role_file")"
        [ -n "$hub"  ] || hub="$(awk  -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2);v=$2}  END{print v}' "$role_file")"
    fi
    # UN ROLE EXPLICITE, ET RIEN D'AUTRE.
    # Deduire "operateur" de la simple presence d'un numero de noeud etait faux
    # sur l'hote : start.sh y porte WAN_NODE_ID = le noeud de la MACHINE, alors
    # qu'il genere les configs de CONTENEURS qui sont, eux, d'autres noeuds. Le
    # hook reappliquait donc le noeud de l'hote a chaque operateur : tous se
    # retrouvaient avec le meme point code, et deux equipements a la meme
    # adresse SS7 ne peuvent pas s'attacher tous les deux.
    #
    # Ce rattrapage ne vaut donc QUE pour une machine qui se declare operateur
    # dans /etc/osmo-role (une VM, un noeud natif) ou via OSMO_ROLE. Quand
    # start.sh calcule lui-meme l'identite de chaque conteneur, il n'a besoin
    # de personne.

    # OSMO_WAN_NODE pose dans l'ENVIRONNEMENT vaut declaration lui aussi :
    # c'est ce que start.sh passe au conteneur (-e OSMO_WAN_NODE), et un
    # conteneur n'a pas de /etc/osmo-role, donc pas de OSMO_ROLE. Avec le seul
    # test sur le role, le rattrapage sortait ici sans un mot la ou il est
    # justement necessaire, et le conteneur gardait le point-code du gabarit.
    # WAN_NODE_ID, lui, ne vaut PAS declaration : sur l'hote il designe le noeud
    # de la MACHINE, pas celui du conteneur dont on genere les configs.
    [ "$role" = "operator" ] || [ -n "${OSMO_WAN_NODE:-}" ] || return 0
    [[ "$node" =~ ^[1-9]$ ]] || return 0

    local setid="$here/network/set-node-id.sh"
    [ -r "$setid" ] || return 0

    # Le hub, quelle qu'en soit la source. Sans lui, set-node-id.sh DEMANDE -
    # et comme il est appele ici depuis une boucle de demarrage, il attendait
    # une reponse que personne ne venait donner : le lancement se figeait juste
    # apres la creation du reseau de l'operateur, sans un mot.
    [ -n "$hub" ] || hub="${WAN_STP_TARGET:-${INTER_STP_IP:-}}"

    # --op de set-node-id.sh est l'INDICE de l'operateur dans le point code
    # 1.<noeud><op>.<role>, pas leur nombre. 1 en dur : un noeud de WAN n'en
    # porte qu'un ici. OPS_PER_NODE, lue jusqu'ici, n'est posee nulle part dans
    # le depot - elle valait donc toujours 1, mais par accident, et le jour ou
    # quelqu'un l'aurait exportee l'identite du noeud aurait change sans raison.
    # Ne pas y mettre l'identifiant d'operateur : dans start.sh il vaut l'indice
    # du CONTENEUR, et le noeud 2 passerait de 1.21.2 a 1.22.2.
    local args=(--node "$node" --op 1 --native --conf-dir "$dest/osmocom")
    [ -n "$hub" ] && args+=(--hub-ip "$hub")

    # </dev/null : cette reecriture est automatique, elle ne pose JAMAIS de
    # question. Si une valeur manque, on prefere un echec signale a un blocage.
    if bash "$setid" "${args[@]}" </dev/null >/dev/null 2>&1; then
        echo "  ✓ adressage SS7 du noeud ${node} reapplique (set-node-id.sh)"
    else
        echo "  ⚠ adressage SS7 du noeud ${node} NON reapplique - l'ASP inter-STP" >&2
        echo "    gardera les defauts du gabarit (127.0.0.1, shutdown)." >&2
        # Avertir puis rendre 0, c'etait laisser le demarrage se poursuivre sur
        # des configs dont l'identite SS7 est restee au gabarit : rien en amont
        # ne relit le point code qui vient d'etre ecrit, et la panne ne se
        # voyait qu'a l'ASP qui ne s'attachait jamais.
        return 1
    fi
}

# ── sms-routing.conf : LE ROUTAGE SE LIT DANS LE NUMERO ──────────────────────
# La cle de routage est le NOEUD, pas l'operateur, et c'est le premier chiffre
# du MSISDN (<noeud>00<op><ms>). Avant, [operators] portait une entree par
# operateur pointant sur op_backbone_ip - 172.20.0.11, une adresse du plan
# DOCKER absente de toute VM - et l'operateur 1 du noeud 1 comme celui du noeud
# 2 revendiquaient la meme cle "1". Sur un banc a plusieurs noeuds, un SMS
# partait donc chez le premier qui repondait, quand il partait.
#
# Les adresses viennent de la TABLE WAN (/etc/osmo-wan.conf), la seule source
# qui sache ou joignent reellement les autres noeuds. Sans table - un banc a un
# seul noeud - il ne reste qu'une entree, la notre.
#
#   $1 fichier  $2 noeud  $3 operateur  $4 nb de MS  $5 IP locale du coeur
#   $6 table WAN
_write_sms_routing_native() {
    local out="$1" node="$2" op_id="$3" n_ms="$4" host_ip="$5" wanf="$6"
    local spec="" ent id ip nops n o m
    local -a ids=() 

    # WAN_NODES="id:ip:indicatif[:operateurs] ..." - lu sans sourcer la table :
    # ce fichier pose aussi WAN_NODE_ID et WAN_OPS, qui ecraseraient ceux de
    # l'appelant.
    if [ -r "$wanf" ]; then
        spec="$(sed -n 's/^WAN_NODES=//p' "$wanf" | tr -d '"'"'"'' | tail -1)"
    fi

    # NOTRE adresse, celle par laquelle les autres noeuds nous joignent. La
    # table d'abord (c'est un choix explicite), la route ensuite, la boucle
    # locale en dernier - un banc isole doit quand meme router ses SMS.
    local self_ip=""
    for ent in $spec; do
        IFS=: read -r id ip _ nops <<< "$ent"
        [ "$id" = "$node" ] && self_ip="$ip"
    done
    if [ -z "$self_ip" ]; then
        self_ip="$(ip route get 1.1.1.1 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' | head -1)"
    fi
    [ -n "$self_ip" ] || self_ip="$host_ip"

    {
        printf '# sms-routing.conf - ecrit par generate_configs.sh.\n'
        printf '# Cle de routage = NUMERO DE NOEUD, premier chiffre du MSISDN.\n'
        printf '# MSISDN = <noeud>00<operateur><rang du MS sur 2 chiffres>\n\n'
        printf '[local]\n'
        printf 'operator_id = %s\n' "$node"
        printf 'sc_address  = 1999001%s444\n\n' "$op_id"
        printf '[operators]\n'
        if [ -n "$spec" ]; then
            for ent in $spec; do
                IFS=: read -r id ip _ nops <<< "$ent"
                [ -n "$id" ] || continue
                [ "$id" = "$node" ] && ip="$self_ip"
                printf '%s = %s\n' "$id" "$ip"
            done
        else
            printf '%s = %s\n' "$node" "$self_ip"
        fi
        printf '\n[routes]\n'
        if [ -n "$spec" ]; then
            for ent in $spec; do
                IFS=: read -r id ip _ nops <<< "$ent"
                [ -n "$id" ] || continue
                # nops non renseigne = 3 champs dans l'entree. Le "${nops:-1}"
                # d'un case teste la valeur ETENDUE, pas la variable : il ne
                # corrigeait donc rien et seq recevait une borne vide.
                nops="${nops:-1}"
                case "$nops" in ''|*[!0-9]*) nops=1 ;; esac
                for o in $(seq 1 "$nops"); do
                    for m in $(seq 1 "$n_ms"); do
                        printf '%s = %s\n' "$(osmo_msisdn "$id" "$o" "$m")" "$id"
                    done
                done
            done
        else
            for m in $(seq 1 "$n_ms"); do
                printf '%s = %s\n' "$(osmo_msisdn "$node" "$op_id" "$m")" "$node"
            done
        fi
        printf '\n[relay]\nport = 7890\nconnect_timeout = 10\nretry_count = 3\nretry_delay = 5\n'
    } > "$out"
}

# ── Les retouches NATIVES : la recette de l'ISO, en UN SEUL endroit ──────────
# Ce que build-iso.sh posait a la main APRES apply_config_templates, et que
# personne ne rejouait ensuite. C'est la difference entre "les gabarits sont
# substitues" et "la machine a une configuration qui demarre" :
#
#   sms-routing.conf  le fallback des gabarits enumere les operateurs sur
#                     op_backbone_ip (172.20.0.11) - une adresse du plan
#                     DOCKER, qui n'existe sur aucune VM - et n'y met que deux
#                     routes par operateur. En natif tout boucle sur l'adresse
#                     locale du coeur, et il faut UNE route par MS reellement
#                     embarque, sans quoi le relais rend "No route for
#                     destination" des le troisieme.
#   osmo-sgsn.cfg     "gtp local-ip" est fige a 127.0.0.1 dans le gabarit ; en
#                     natif le GGSN ecoute sur l'adresse du coeur. Le SGSN
#                     annoncait une adresse et le GGSN en servait une autre.
#   osmo-msc.cfg      "hlr remote-ip", meme histoire.
#   run.sh            "trxcon <options>" et "mobile <options>" reduits au seul
#                     binaire : sur l'ISO ce sont les modules qui decident des
#                     options, pas la ligne heritee de l'image docker.
#
# Sans cette fonction, une regeneration rendait des configurations SUBSTITUEES
# mais NATIVEMENT FAUSSES : le SGSN se liait a une adresse qu'il n'a pas, le
# relais SMS visait le plan docker - et rien ne le disait avant le demarrage
# suivant. Elle est donc appelee des DEUX cotes, sur la meme arborescence :
#   build-iso.sh        sur $TEMP_CONFIG puis sur $ROOTFS/etc (construction)
#   start-direct.sh     sur le repertoire temporaire d'un --regen (execution)
# Idempotente : la rejouer sur un arbre deja retouche ne change rien.
#
#   $1  racine qui porte osmocom/ : $TEMP_CONFIG, $ROOTFS/etc, /etc
#   $2  numero d'operateur      (defaut 1)
#   $3  nombre de MS embarques  (defaut 2)
#   $4  adresse locale du coeur (defaut 127.0.0.2)
#   $5  numero de noeud         (defaut : osmo_node_id)
#   $6  table WAN a lire        (defaut /etc/osmo-wan.conf)
apply_native_post_patches() {
    local dest="${1:?apply_native_post_patches : racine manquante}"
    local op_id="${2:-1}" n_ms="${3:-2}" host_ip="${4:-127.0.0.2}"
    local node="${5:-$(osmo_node_id)}" wanf="${6:-${WAN_CONF_FILE:-/etc/osmo-wan.conf}}"
    # ── DEUX ADRESSES DE PLUS, ET POURQUOI ELLES NE SONT PAS host_ip ────────
    # sgsn_gtp_ip  Le SGSN et le GGSN ouvrent tous deux un socket GTP. Leur
    #              donner la meme adresse - ce que faisait ce patch, qui ecrivait
    #              host_ip dans « gtp local-ip » alors que le gabarit du GGSN
    #              met ce meme host_ip dans « gtp bind-ip » - donne un GGSN qui
    #              prend le port et un SGSN qui meurt sur « Address already in
    #              use », relance par systemd sans fin. Le packet attach ne se
    #              termine jamais. Par defaut : la passerelle du segment prive
    #              du noeud, 192.168.<noeud+1>.1, posee par osmo-ip-plan.sh.
    # hlr_ip       « gsup remote-ip » et le « remote-ip » du bloc hlr du MSC
    #              designent OU ECOUTE LE HLR, pas l adresse locale du noeud.
    #              Les aligner sur host_ip ne se voyait pas tant que les deux
    #              valaient 127.0.0.2 ; des que host_ip bouge, le MSC et le SGSN
    #              appellent une adresse ou personne ne repond. On suit donc le
    #              « bind ip » d osmo-hlr.cfg.
    local sgsn_gtp_ip="${7:-}"
    local hlr_ip="${8:-127.0.0.2}"
    local cfg="$dest/osmocom"
    [ -d "$cfg" ] || return 0
    [ "$n_ms" -ge 1 ] 2>/dev/null || n_ms=1
    case "$node" in [1-9]) ;; *) node=1 ;; esac
    # APRES la validation du noeud, jamais avant : un $node vide donnerait
    # 192.168.1.1 - le LAN du banc, ou vivent deja les VM et le hub SS7.
    : "${sgsn_gtp_ip:=192.168.$((node + 1)).1}"

    _write_sms_routing_native "$cfg/sms-routing.conf" \
        "$node" "$op_id" "$n_ms" "$host_ip" "$wanf"

    if [ -f "$cfg/osmo-sgsn.cfg" ]; then
        sed -i \
            -e "s/^\([[:space:]]*gtp[[:space:]]\+local-ip[[:space:]]\+\).*/\1${sgsn_gtp_ip}/" \
            -e "s/^\([[:space:]]*gsup[[:space:]]\+remote-ip[[:space:]]\+\).*/\1${hlr_ip}/" \
            -e "/^[[:space:]]*bind udp local\$/,/^[[:space:]]*!\$/ s/^\([[:space:]]*listen[[:space:]]\+\).*/\1${host_ip} 23000/" \
            "$cfg/osmo-sgsn.cfg"
    fi

    if [ -f "$cfg/osmo-msc.cfg" ]; then
        sed -i \
            -e "/^hlr\$/,/^!\$/ s/^\([[:space:]]*remote-ip[[:space:]]\+\).*/\1${hlr_ip}/" \
            "$cfg/osmo-msc.cfg"
    fi

    # ── osmo-stp.cfg : LE local-ip DE L ASP VERS L INTER-STP ────────────────
    # [2026-08-31] Ce fichier manquait a la liste, et c'est une adresse de
    # CONTENEUR qui restait dans une config d'HOTE.
    #
    # Le gabarit pose  local-ip __INTER_LOCAL_IP__  sous  asp asp-to-inter,
    # substitue par op_backbone_ip(N) = 172.20.0.<10+N>, soit 172.20.0.11 pour
    # l'operateur 1. C'est juste DANS le conteneur, qui porte cette adresse.
    # Sur l'hote elle N'EXISTE PAS - build-iso.sh la retire meme explicitement
    # ("172.20.0.11 est RETIREE : c'est l'adresse du PREMIER CONTENEUR").
    #
    # Un local-ip inbindable ne produit pas d'erreur lisible : l'ASP ne monte
    # simplement jamais, et le diagnostic sort en aval, tres loin de la cause -
    #     as-inter : AS_DOWN
    #     Route par defaut -> inter-STP absente ou non avail
    # ce qui envoie chercher un probleme de hub ou de filtrage alors que la
    # patte locale n'a jamais pu s'ouvrir. Constate le 31/08 sur le banc natif.
    #
    # 127.0.0.1 : la seule adresse dont on soit certain sur un hote, quelle que
    # soit sa topologie reseau. Le remote-ip, lui, est deja resolu par le
    # gabarit (__INTER_STP_IP__) et n'est pas touche ici.
    #
    # awk et pas sed : il faut ne modifier QUE le local-ip qui suit
    # « asp asp-to-inter », pas ceux du bloc d'ecoute juste au-dessus
    # (local-ip 127.0.0.1 / 127.0.0.2), qui sont corrects.
    if [ -f "$cfg/osmo-stp.cfg" ]; then
        awk '
            /^[[:space:]]*asp[[:space:]]+asp-to-inter[[:space:]]/ { in_asp = 1; print; next }
            in_asp && /^[[:space:]]*local-ip[[:space:]]/ {
                sub(/local-ip[[:space:]]+.*/, "local-ip 127.0.0.1"); in_asp = 0; print; next
            }
            /^[[:space:]]*(as|asp|cs7|listen)[[:space:]]/ && !/asp-to-inter/ { in_asp = 0 }
            { print }
        ' "$cfg/osmo-stp.cfg" > "$cfg/osmo-stp.cfg.tmp" \
            && mv -f "$cfg/osmo-stp.cfg.tmp" "$cfg/osmo-stp.cfg"
    fi

    # ── pjsip.conf : LE BLOC [transport-udp] ────────────────────────────────
    # [2026-08-31] Troisieme adresse de conteneur dans une config d'hote, et la
    # plus couteuse. configs/pjsip.conf ne porte AUCUN placeholder : le bloc
    # transport est fige sur le plan docker -
    #     external_media_address=10.2.0.2
    #     external_signaling_address=10.2.0.2
    #     local_net=172.20.0.0/16
    # et generate_configs.sh n'y ajoutait que les trunks interop, jamais le
    # transport. Sur un banc natif, 10.2.0.2 n'existe nulle part.
    #
    # CE QUE CA CASSE. external_* est une option de NAT : pour tout pair HORS
    # local_net, Asterisk REECRIT le Contact et le SDP avec cette adresse. Un
    # Linphone sur le LAN (192.168.x, hors du 172.20.0.0/16 declare) recevait
    # donc "10.2.0.2" et n'avait plus personne a joindre - enregistrement ou
    # audio muet, selon le moment. Le trunk Zadarma le montrait en clair :
    # il nous interrogeait sur sip:474117@127.0.0.1:5060, un Contact
    # inexploitable qu'il avait appris de nous.
    #
    # LE CORRECTIF : sur un hote plat il n'y a pas de frontiere NAT a declarer.
    # On SUPPRIME les external_* - sans eux, pjsip repond avec l'adresse de la
    # socket, c'est-a-dire la bonne, par destination - et on declare local_net
    # sur tout l'espace prive plutot que sur le seul plan docker. Ce jeu est
    # topologie-INDEPENDANT : il reste vrai que l'ISO tourne sur un LAN en
    # 192.168.x, en 10.x, derriere docker, ou avec l'APN GSM.
    if [ -f "$dest/asterisk/pjsip.conf" ]; then
        awk '
            /^\[transport-udp\]/ { in_t = 1; print; next }
            in_t && /^\[/         { in_t = 0 }
            in_t && /^[[:space:]]*external_(media|signaling)_address[[:space:]]*=/ { next }
            in_t && /^[[:space:]]*local_net[[:space:]]*=/ {
                if (!done) {
                    print "local_net=127.0.0.0/8"
                    print "local_net=10.0.0.0/8"
                    print "local_net=172.16.0.0/12"
                    print "local_net=192.168.0.0/16"
                    print "local_net=176.16.32.0/24"
                    done = 1
                }
                next
            }
            { print }
        ' "$dest/asterisk/pjsip.conf" > "$dest/asterisk/pjsip.conf.tmp" \
            && mv -f "$dest/asterisk/pjsip.conf.tmp" "$dest/asterisk/pjsip.conf"
    fi

    # ── mobile.cfg : LA CIBLE GSMTAP ────────────────────────────────────────
    # [2026-08-31] Deuxieme adresse de conteneur dans une config d'hote.
    # Le gabarit pose  remote-host __INTER_NET_GATEWAY__  sous  gsmtap,
    # substitue par 172.20.0.1 - la passerelle du bridge docker. Elle n'existe
    # QUE tant qu'un reseau docker est monte : sur un banc natif, ou des que
    # les conteneurs sont arretes, l'adresse disparait et les paquets GSMTAP
    # partent dans le vide.
    # Personne ne le signale : le mobile continue a fonctionner, l'appel passe,
    # et c'est Wireshark qui reste desesperement vide sur udp/4729 - ce qu'on
    # met naturellement sur le compte de la radio ou du filtre de capture.
    # Les autres composants (osmo-bts-trx, osmo-pcu, osmo-bsc) tapent deja sur
    # 127.0.0.1:4729 ; le mobile etait le seul a viser ailleurs.
    if [ -f "$cfg/mobile.cfg" ]; then
        sed -i \
            -e "/^gsmtap\$/,/^!\$/ s/^\([[:space:]]*remote-host[[:space:]]\+\).*/\1127.0.0.1/" \
            "$cfg/mobile.cfg"
    fi

    if [ -f "$cfg/run.sh" ]; then
        sed -i \
            -e 's#^[[:space:]]*\([^[:space:]]*/\)\?trxcon\([[:space:]].*\)\?$#trxcon#' \
            -e 's#^[[:space:]]*\([^[:space:]]*/\)\?mobile\([[:space:]].*\)\?$#mobile#' \
            "$cfg/run.sh"
    fi
    chmod +x "$cfg"/*.sh 2>/dev/null || true
    return 0
}

# ── Affichage de l'effectif ──────────────────────────────────────────────────
_gc_show() {
    local key def perop comment val src
    printf '\n  %-13s %-26s %s\n' "REGLAGE" "VALEUR EFFECTIVE" "ORIGINE"
    printf '  %-13s %-26s %s\n' "-------" "----------------" "-------"
    while IFS='|' read -r key def perop comment; do
        [ -n "$key" ] && [ "$key" != "#SECTION" ] || continue
        val="${!key:-}"
        if [ -n "$val" ]; then src="globals.conf / env"
        elif [ "$perop" = 1 ]; then src="derive de l'operateur"; val="(auto)"
        else src="defaut"; val="$def"; fi
        printf '  %-13s %-26s %s\n' "$key" "$val" "$src"
    done < <(_gc_table)
    printf '\n'
}

# ── Garde-fou multi-operateur ────────────────────────────────────────────────
_gc_warn_perop() {
    [ "${N_OPERATORS:-1}" -gt 1 ] 2>/dev/null || return 0
    local key def perop comment figes=""
    while IFS='|' read -r key def perop comment; do
        [ "$key" != "#SECTION" ] && [ "$perop" = 1 ] && [ -n "${!key:-}" ] && figes="$figes $key"
    done < <(_gc_table)
    [ -n "$figes" ] || return 0
    printf '\n  ⚠ N_OPERATORS=%s mais ces champs par-operateur sont FIGES :%s\n' \
           "$N_OPERATORS" "$figes"
    printf '    Tous les operateurs partageront la meme valeur - collision garantie\n'
    printf '    sur la radio et/ou les identites. Videz-les dans globals.conf.\n\n'
}

# ── Mode execute ─────────────────────────────────────────────────────────────
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    force=0; show=0; _gc_sets=()
    for a in "$@"; do
        case "$a" in
            --force) force=1 ;;
            --show)  show=1 ;;
            -h|--help)
                sed -n '2,60p' "$0" | sed 's/^#\{0,1\} \{0,1\}//'; exit 0 ;;
            *=*)     _gc_sets+=("$a") ;;
            *) printf 'option inconnue : %s (voir --help)\n' "$a" >&2; exit 2 ;;
        esac
    done
    _gc_write "$force"
    for kv in "${_gc_sets[@]}"; do _gc_set "$kv"; done
    gc_load
    _gc_warn_perop
    [ "$show" = 1 ] && _gc_show
    exit 0
fi

# ── Mode source ──────────────────────────────────────────────────────────────
gc_load

helpers/create_interop.sh

.sh 198 lignes - 7,0KB

helpers/create_interop.sh
#!/bin/bash
# create_interop.sh - Genere la configuration inter-STP (hub SS7 central)
#
# Parametres :
#   $1  n_operators   Nombre d'operateurs (1..24)
#   $2  outfile       Chemin fichier config de sortie (defaut: osmo-stp-interop.cfg)
#
# Point codes ITU 3-8-3 :
#   Inter-STP : 0.0.0
#   Op N STP  : 1.N.2    (cluster = N, 8 bits → max 255, limite a 24)
#   Op N MSC  : 1.N.1
#   Op N BSC  : 1.N.3
#
# Routes statiques avec masque exact 7.255.7 (14 bits) :
#   1.N.1 → as-opN  (MSC)
#   1.N.3 → as-opN  (BSC)

set -e

# ── Mode WAN ─────────────────────────────────────────────────────────────────
#   create_interop.sh --wan <n_noeuds> <ops_par_noeud> <outfile>
#
# Le hub d'origine ne dessert qu'UNE machine : ses point codes 1.<op>.<role> se
# recalculent a l'identique sur chaque noeud d'un WAN, donc trois noeuds relies
# au meme hub presenteraient trois fois 1.1.2. Un point code est une ADRESSE :
# la collision n'est pas un detail de nommage, elle rend le routage SS7 faux.
#
# Le plan WAN encode le noeud DANS le point code :
#   PC = 1.<noeud><op>.<role>     noeud 1 op 1 → 1.11.2 ; noeud 3 op 2 → 1.32.2
#   RCTX = noeud*1000 + op*100 + 50
# Lisible a l'oeil (le premier chiffre est le noeud), unique, et le champ
# central du 3-8-3 tient jusqu'a 9 noeuds × 9 operateurs.
# ── Adresse d'ecoute du hub ──────────────────────────────────────────────────
# Ce fichier est REGENERE a chaque demarrage du hub (start-interstp.sh). Tant
# que l'adresse etait ecrite 0.0.0.0 en dur ici, toute correction posee ailleurs
# - set_stp_ip.sh en particulier - etait effacee au redemarrage suivant, sans
# que rien ne le signale : le hub repartait en multi-homing.
#
# Et 0.0.0.0 n'est pas neutre : SCTP annonce alors TOUTES les adresses de la
# machine dans son INIT (NAT 10.0.2.x, alias 172.20.x, host-only 192.168.56.x).
# Le pair essaie des chemins qui, vus de lui, ne menent nulle part ; l'ASP monte
# puis retombe, en boucle. Une adresse unique et juste vaut mieux qu'un
# multi-homing dont trois branches sur quatre sont mortes.
#
# HUB_LISTEN_IP (ou --listen-ip) impose l'adresse. Vide = 0.0.0.0, l'ancien
# comportement, conserve pour les montages d'une seule machine.
HUB_LISTEN_IP="${HUB_LISTEN_IP:-0.0.0.0}"
_ci_args=()
while [ $# -gt 0 ]; do
    case "$1" in
        --listen-ip)   HUB_LISTEN_IP="${2:-0.0.0.0}"; shift 2 ;;
        --listen-ip=*) HUB_LISTEN_IP="${1#*=}"; shift ;;
        *)             _ci_args+=("$1"); shift ;;
    esac
done
[ "${#_ci_args[@]}" -gt 0 ] && set -- "${_ci_args[@]}"
[ -n "$HUB_LISTEN_IP" ] || HUB_LISTEN_IP="0.0.0.0"

WAN_MODE=0
if [ "${1:-}" = "--wan" ]; then
    WAN_MODE=1
    n_nodes="${2:-3}"
    ops_per_node="${3:-1}"
    outfile="${4:-osmo-stp-interop.cfg}"
    if ! [[ "$n_nodes" =~ ^[1-9]$ ]] || ! [[ "$ops_per_node" =~ ^[1-9]$ ]]; then
        echo "Erreur : --wan <1-9 noeuds> <1-9 operateurs>" >&2; exit 1
    fi
else
    n_operators="${1:-2}"
    outfile="${2:-osmo-stp-interop.cfg}"
    if ! [[ "$n_operators" =~ ^[0-9]+$ ]] || [ "$n_operators" -lt 1 ] || [ "$n_operators" -gt 24 ]; then
        echo "Erreur : n_operators doit etre 1..24" >&2
        exit 1
    fi
fi

cat > "$outfile" <<'EOFCONFIG'
!
! osmo-stp-interop.cfg - Configuration inter-STP centrale
!
! PC 0.0.0 : hub de routage SS7
!

log stderr
 logging filter all 1
 logging color 1
 logging print category-hex 0
 logging print category 1
 logging print extended-timestamp 1
 logging print level 1
 logging print file basename
 logging level lss7 info
 logging level lsccp info
 logging level lm3ua info
 logging level linp notice
!
stats interval 5
!
line vty
 no login
!
cs7 instance 0
 network-indicator international
 point-code 0.0.0
!
 xua rkm routing-key-allocation dynamic-permitted

 listen m3ua 2908
  accept-asp-connections dynamic-permitted
  local-ip __LISTEN_IP__
!
EOFCONFIG

# Heredoc quote : le gabarit sort tel quel, on substitue ensuite. Le marqueur
# n'existe que dans l'entete, la boucle WAN qui suit n'en pose pas.
sed -i "s|__LISTEN_IP__|${HUB_LISTEN_IP}|" "$outfile"

if [ "$WAN_MODE" = "1" ]; then
    for n in $(seq 1 "$n_nodes"); do
        for o in $(seq 1 "$ops_per_node"); do
            rctx_inter=$(( n * 1000 + o * 100 + 50 ))
            pc_stp="1.${n}${o}.2"
            cat >> "$outfile" <<EOF

 as as-n${n}op${o} m3ua
  routing-key ${rctx_inter} ${pc_stp}
  traffic-mode override
EOF
        done
    done
else
# [2026-08-31] DE 1 AU RANG LE PLUS HAUT, pas "1 a n_operators".
# n_operators compte les CONTENEURS ; il ne dit rien du natif, qui est un
# operateur du banc au meme titre. Avec OP_ID_BASE=2 - conteneurs en 2 et 3,
# natif en 1 - la boucle produisait as-op1 et as-op2 pour DEUX conteneurs
# qui sont en realite op2 et op3 : le hub declarait donc un as-op2 en 1.2.2
# que personne ne venait prendre, et AUCUN as-op3. D ou, cote natif,
#     1.2.2/14 INACC ... PROHIB   1.3.2/14 INACC ... PROHIB
# et un banc ou les operateurs sont attaches mais ne se routent pas.
for i in $(seq 1 "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
    rctx_inter=$(( i * 100 + 50 ))
    pc_stp="1.${i}.2"

    cat >> "$outfile" <<EOF

 as as-op${i} m3ua
  routing-key ${rctx_inter} ${pc_stp}
  traffic-mode override
EOF
done
fi

cat >> "$outfile" <<'EOFROUTES'

 route-table system
EOFROUTES

if [ "$WAN_MODE" = "1" ]; then
    for n in $(seq 1 "$n_nodes"); do
        for o in $(seq 1 "$ops_per_node"); do
            cat >> "$outfile" <<EOF
  update route 1.${n}${o}.1 7.255.7 linkset as-n${n}op${o}
  update route 1.${n}${o}.3 7.255.7 linkset as-n${n}op${o}
EOF
        done
    done
else
# [2026-08-31] DE 1 AU RANG LE PLUS HAUT, pas "1 a n_operators".
# n_operators compte les CONTENEURS ; il ne dit rien du natif, qui est un
# operateur du banc au meme titre. Avec OP_ID_BASE=2 - conteneurs en 2 et 3,
# natif en 1 - la boucle produisait as-op1 et as-op2 pour DEUX conteneurs
# qui sont en realite op2 et op3 : le hub declarait donc un as-op2 en 1.2.2
# que personne ne venait prendre, et AUCUN as-op3. D ou, cote natif,
#     1.2.2/14 INACC ... PROHIB   1.3.2/14 INACC ... PROHIB
# et un banc ou les operateurs sont attaches mais ne se routent pas.
for i in $(seq 1 "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
    cat >> "$outfile" <<EOF
  update route 1.${i}.1 7.255.7 linkset as-op${i}
  update route 1.${i}.3 7.255.7 linkset as-op${i}
EOF
done
fi

cat >> "$outfile" <<'EOF'

!
EOF

if [ "$WAN_MODE" = "1" ]; then
    echo "✓ Config inter-STP WAN generee : $outfile" >&2
    echo "  PC hub  : 0.0.0   Noeuds : $n_nodes × $ops_per_node operateur(s)" >&2
    echo "  PC noeuds : 1.<noeud><op>.<role>   RCTX : noeud*1000 + op*100 + 50" >&2
    echo "  Routes  : $(( n_nodes * ops_per_node * 2 )) (masque 7.255.7)" >&2
else
    echo "✓ Config inter-STP generee : $outfile" >&2
    echo "  PC hub     : 0.0.0   Operateurs : $n_operators" >&2
    echo "  Routes     : $((n_operators * 2)) (masque 7.255.7)" >&2
fi

helpers/prepare_host.sh

.sh 122 lignes - 4,5KB

helpers/prepare_host.sh
#!/usr/bin/env bash
# force_stack.sh
# A lancer en root (sudo).
# But :
#  - FORCER (kill + relance) Linphone en user
#  - FORCER (kill + relance) Wireshark en root sur UDP/4729 (GSMTAP)
#  - Restart du service docker
# Affichage:
#   [....] action en cours  -> se transforme en -> [ OK ] action finie (meme ligne)

set -euo pipefail

GSMTAP_PORT="${GSMTAP_PORT:-4729}"

# ---------- helpers ----------
step()    { printf "[....] %s" "$1"; }
step_ok() { printf "\r[ OK ] %s\n" "$1"; }
fail()    { printf "\r[FAIL] %s\n" "$1" >&2; exit 1; }

need() { command -v "$1" >/dev/null 2>&1 || fail "Commande manquante: $1"; }

kill_name_as_user() {
  local user="$1" name="$2"
  pkill -u "$user" -x "$name" 2>/dev/null || true
}

kill_name_root() {
  local name="$1"
  pkill -x "$name" 2>/dev/null || true
}

# ---------- preflight ----------
[[ "${EUID}" -eq 0 ]] || fail "Lance en root: sudo $0"

need sudo
need docker
need ss
need systemctl

# ---------- QUI EST L UTILISATEUR DE LA SESSION ----------
# Ce bloc exigeait SUDO_USER non vide ET different de root, et sortait en [FAIL]
# sinon. Deux situations parfaitement legitimes tombaient dedans :
#
#   1. `sudo -i` puis ./start.sh - le shell est deja root, sudo n a plus rien a
#      transmettre et SUDO_USER est VIDE. C est le mode de travail courant sur
#      ce banc.
#   2. L ISO --desktop : la session graphique EST root. build-iso.sh ecrit
#      AutomaticLogin=root dans /etc/gdm3/custom.conf et decommente le
#      pam_succeed_if qui interdit la connexion de root. "root" est donc ICI la
#      BONNE reponse - la rejeter revenait a refuser la configuration nominale
#      de l image qu on livre.
#
# Le bon critere n est pas le NOM du compte mais la PRESENCE de sa session,
# c est-a-dire de son socket pulse. Meme logique que session_pulse_user() dans
# start.sh, a laquelle ce fichier ne faisait qu ajouter une divergence.
resolve_session_user() {
  local u uid sock
  for u in "${HOST_PULSE_USER:-}" "${SUDO_USER:-}" "$(logname 2>/dev/null || true)"; do
    [[ -n "$u" ]] || continue
    uid="$(id -u "$u" 2>/dev/null)" || continue
    [[ -S "/run/user/${uid}/pulse/native" ]] && { echo "$u"; return 0; }
  done
  # Proprietaire du premier socket pulse trouve : couvre le `sudo -i` et les
  # sessions sans logname (cron, service, terminal detache).
  for sock in /run/user/*/pulse/native; do
    [[ -S "$sock" ]] || continue
    uid="${sock#/run/user/}"; uid="${uid%%/*}"
    u="$(getent passwd "$uid" 2>/dev/null | cut -d: -f1)"
    [[ -n "$u" ]] && { echo "$u"; return 0; }
  done
  # Aucune session pulse vivante : on retombe sur l invocateur, root compris.
  echo "${SUDO_USER:-$(id -un)}"
}
TARGET_USER="$(resolve_session_user)"

id -u "${TARGET_USER}" >/dev/null 2>&1 || fail "User invalide: ${TARGET_USER}"

# ---------- 1) Linphone : kill + relance en user ----------
step "Linphone: kill (user=${TARGET_USER})"
kill_name_as_user "${TARGET_USER}" "linphone"
sleep 0.2
step_ok "Linphone: kill (user=${TARGET_USER})"

step "Linphone: relance (user=${TARGET_USER})"
    # Ne PAS re-resoudre TARGET_USER ici : la ligne qui s y trouvait
    # (${SUDO_USER:-$(logname ...)}) ecrasait le resultat de
    # resolve_session_user() par une valeur moins fiable, juste avant de s en
    # servir. Une seule resolution, en tete de script.
    TARGET_UID=$(id -u "$TARGET_USER")
    # HOME par getent, pas /home/$TARGET_USER en dur : root habite /root. Avec
    # le chemin fige, une session sous root pointait XAUTHORITY sur
    # /home/root/.Xauthority - inexistant - et linphone ne s ouvrait pas.
    TARGET_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)"
    DISPLAY="${DISPLAY:-:0}"
    XAUTHORITY="${XAUTHORITY:-${TARGET_HOME:-/home/$TARGET_USER}/.Xauthority}"

    setsid sudo -u "$TARGET_USER" \
        env DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" \
            DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${TARGET_UID}/bus" \
        linphone </dev/null >/dev/null 2>&1 &
                
step_ok "Linphone: relance (user=${TARGET_USER})"

# ---------- 3) Xterm : kill----------
step "Xterm kill"
kill_name_root "xterm"
sleep 0.2
step_ok "Xterm: kill (root)"

# ---------- 2) Wireshark : kill + relance en root sur GSMTAP/4729 ----------
step "Wireshark: kill (root)"
kill_name_root "wireshark"
sleep 0.2
step_ok "Wireshark: kill (root)"

# ---------- 5) Mini-check (optionnel) ----------
step "Check: listeners UDP/${GSMTAP_PORT}"
ss -lunp | awk -v p=":${GSMTAP_PORT}" '$0 ~ p {print}' >/dev/null 2>&1 || true
step_ok "Check: listeners UDP/${GSMTAP_PORT}"

step "Stack forcee"
step_ok "Stack forcee"

install_modules/00-prereqs.sh

.sh 17 lignes - 852B

install_modules/00-prereqs.sh
# 00-prereqs - la machine peut-elle accueillir cette installation ?
INST_REGISTER prereqs "Prerequis du systeme"
INST_ROOT[prereqs]=0

inst_prereqs_run() {
    have_cmd apt-get || { inst_hint "cette installation vise Debian/Ubuntu ; sur une autre distribution, utilisez Docker : ./tools/make-docker-image.sh"
                          inst_fail "apt-get introuvable"; return $INST_RC_FAIL; }
    have_cmd git || { inst_fail "git absent"; return $INST_RC_FAIL; }
    local free; free=$(df -Pm "${GSM_ROOT%/*}" 2>/dev/null | awk 'NR==2{print $4}')
    if [ -n "$free" ] && [ "$free" -lt 4096 ]; then
        inst_hint "les sources et les objets de compilation occupent ~4 Go"
        inst_fail "espace disque insuffisant : ${free} Mo libres sur $(dirname "$GSM_ROOT")"
        return $INST_RC_FAIL
    fi
    inst_ok
}
inst_prereqs_verify() { inst_ok; }

install_modules/10-deps.sh

.sh 79 lignes - 3,7KB

install_modules/10-deps.sh
# 10-deps - paquets systeme.
# La liste fait autorite dans le Dockerfile : on l'en EXTRAIT au lieu de la
# recopier, pour que les deux ne divergent jamais.
INST_REGISTER deps "Dependances systeme (apt)"
INST_DEPS[deps]="prereqs"

# [2026-08-25] L'extraction reconnaissait "RUN apt-get update && apt-get
# install" EN DUR. Le Dockerfile installe desormais via apt-fast : le motif ne
# capturait plus que le bloc d'amorcage (aria2, curl, et les bouts de la
# commande qui installe apt-fast lui-meme), et l'installation native repartait
# avec 29 jetons de charabia au lieu des paquets. Elle echouait sur des noms
# comme "&&" ou une URL, sans que rien ne designe le Dockerfile comme cause.
#
# Deux durcissements plutot qu'un simple elargissement du motif :
#   1. apt-get ET apt-fast sont acceptes, dans n'importe quel ordre update/install ;
#   2. on ne garde que ce qui RESSEMBLE a un nom de paquet. Un bloc apt peut
#      contenir des `&&`, des URL, des accolades - les laisser passer, c'est
#      transformer une erreur de format en erreur d'apt, illisible.
# Le filtre est la vraie protection : il survit au prochain changement de forme.
_deps_list() {
    # Un paquet, c'est ce qui suit "install -y --no-install-recommends" et
    # PRECEDE le premier operateur shell. Filtrer sur la forme des mots ne
    # suffit pas : le bloc d'amorcage contient "chmod", "printf", "rm",
    # qui ressemblent tous a des noms de paquets. On coupe donc a `&&`, `||`,
    # `{` ou une URL, et on ne garde que ce qu'il y a avant.
    awk '
        /^RUN apt-(get|fast) update && apt-(get|fast) install/ {
            inblk = 1
            line = $0
            sub(/^RUN.*--no-install-recommends/, "", line)
            emit(line)
            next
        }
        inblk { emit($0) }

        function emit(l,   n, a, i, cont) {
            # Le commentaire en PREMIER. Les lignes "# ..." au milieu de la
            # liste nont pas de barre de continuation : Docker les retire avant
            # le shell. Les traiter comme une fin de bloc coupait la liste au
            # premier commentaire - soit trois paquets sur soixante-dix.
            # (Pas dapostrophe dans ce bloc : il vit entre quotes simples.)
            sub(/#.*/, "", l)
            if (l ~ /^[ \t]*$/) return          # commentaire pur : transparent

            cont = (l ~ /\\[ \t]*$/)
            if (l ~ /&&|\|\||[{}]|https?:/) {   # on sort de la liste de paquets
                sub(/(&&|\|\||\{).*/, "", l)
                inblk = 0
            } else if (!cont) {
                inblk = 0                        # derniere ligne du bloc
            }
            sub(/\\[ \t]*$/, "", l)
            n = split(l, a, /[ \t]+/)
            for (i = 1; i <= n; i++)
                if (a[i] ~ /^[a-z0-9][a-z0-9.+*-]*$/) print a[i]
        }
    ' "$INST_TREE/Dockerfile" 2>/dev/null | sort -u | tr '\n' ' '
}

inst_deps_check() {
    have_file "$INST_TREE/Dockerfile" || { inst_fail "Dockerfile introuvable - c'est lui qui porte la liste des paquets"; return $INST_RC_FAIL; }
    inst_ok
}
inst_deps_done() {
    local p; for p in build-essential libtalloc-dev libsctp-dev; do have_pkg "$p" || return 1; done
    return 0
}
inst_deps_run() {
    local pkgs; pkgs="$(_deps_list)"
    [ -z "${pkgs// }" ] && { inst_fail "liste de paquets vide - le format du Dockerfile a change"; return $INST_RC_FAIL; }
    inst_say "paquets : $(echo $pkgs | wc -w)"
    apt-get update -qq && apt-get install -y --no-install-recommends $pkgs
}
inst_deps_verify() {
    local missing="" p
    for p in build-essential libtalloc-dev libsctp-dev libdbi-dev; do have_pkg "$p" || missing="$missing $p"; done
    [ -n "$missing" ] && { inst_fail "paquets absents apres installation :$missing"; return $INST_RC_FAIL; }
    inst_ok
}

install_modules/20-user.sh

.sh 11 lignes - 597B

install_modules/20-user.sh
# 20-user - compte de service, tel que le Dockerfile le cree.
INST_REGISTER user "Utilisateur et groupe osmocom"
INST_DEPS[user]="prereqs"

inst_user_done() { getent passwd osmocom >/dev/null && getent group osmocom >/dev/null; }
inst_user_run() {
    getent group  osmocom >/dev/null || groupadd osmocom || return $INST_RC_FAIL
    getent passwd osmocom >/dev/null || useradd -r -g osmocom -s /sbin/nologin -d /var/lib/osmocom osmocom || return $INST_RC_FAIL
    inst_ok
}
inst_user_verify() { inst_user_done && inst_ok || { inst_fail "le compte osmocom n'existe pas"; return $INST_RC_FAIL; }; }

install_modules/30-sources.sh

.sh 49 lignes - 2,0KB

install_modules/30-sources.sh
# 30-sources - recuperation des depots. Idempotent : un depot deja clone est laisse tel quel
# (on ne tire pas : ce serait modifier le travail en cours de l'utilisateur sans le lui demander).
INST_REGISTER sources "Recuperation des sources Osmocom"
INST_DEPS[sources]="prereqs"

_SRC_REPOS="osmocom-bb|https://gitea.osmocom.org/phone-side/osmocom-bb|
osmo-gapk|https://gitea.osmocom.org/osmocom/gapk|
gsup-smsc-proto|https://gitea.osmocom.org/themwi/gsup-smsc-proto|
libosmo-dsp|https://gitea.osmocom.org/sdr/libosmo-dsp.git|"

inst_sources_done() {
    local l; while IFS='|' read -r dir url br; do
        [ -z "$dir" ] && continue
        have_repo "$GSM_ROOT/$dir" || return 1
    done <<< "$_SRC_REPOS"
    return 0
}
inst_sources_run() {
    mkdir -p "$GSM_ROOT" || { inst_fail "impossible de creer $GSM_ROOT"; return $INST_RC_FAIL; }
    local dir url br n=0
    while IFS='|' read -r dir url br; do
        [ -z "$dir" ] && continue
        if have_repo "$GSM_ROOT/$dir"; then
            if [ "${REINSTALL:-0}" = 1 ]; then
                inst_say "$dir : deja present, mise a jour (git pull --ff-only)"
                git -C "$GSM_ROOT/$dir" pull --ff-only \
                    || inst_say "$dir : mise a jour impossible (modifications locales ?), laisse tel quel"
                n=$((n+1))
            else
                inst_say "$dir : deja present, laisse tel quel"
            fi
            continue
        fi
        inst_say "clonage $dir"
        git clone ${br:+--branch "$br"} "$url" "$GSM_ROOT/$dir" || { inst_fail "echec du clonage de $dir ($url)"; return $INST_RC_FAIL; }
        n=$((n+1))
    done <<< "$_SRC_REPOS"
    [ $n -eq 0 ] && return $INST_RC_DONE
    inst_ok
}
inst_sources_verify() {
    local missing="" dir url br
    while IFS='|' read -r dir url br; do
        [ -z "$dir" ] && continue
        have_repo "$GSM_ROOT/$dir" || missing="$missing $dir"
    done <<< "$_SRC_REPOS"
    [ -n "$missing" ] && { inst_fail "depots absents :$missing"; return $INST_RC_FAIL; }
    inst_ok
}

install_modules/40-patches.sh

.sh 30 lignes - 1,1KB

install_modules/40-patches.sh
# 40-patches - correctifs locaux. `git apply --check` d'abord : un correctif deja
# applique ne doit pas faire echouer l'installation.
INST_REGISTER patches "Application des correctifs"
INST_DEPS[patches]="sources"
INST_REQUIRED[patches]=0

_patch_target() {
    case "$(basename "$1")" in
        osmo-trx-*)   printf '%s\n' "$GSM_ROOT/osmo-trx" ;;
        osmocom-bb-*) printf '%s\n' "$GSM_ROOT/osmocom-bb" ;;
        grgsm-*)      printf '%s\n' "$GSM_ROOT/gr-gsm" ;;
        *)            printf '' ;;
    esac
}
inst_patches_run() {
    local p t n=0 skipped=0
    for p in "$INST_TREE"/patches/*.patch; do
        [ -f "$p" ] || continue
        t="$(_patch_target "$p")"
        [ -z "$t" ] || [ ! -d "$t" ] && { skipped=$((skipped+1)); continue; }
        if git -C "$t" apply --check "$p" 2>/dev/null; then
            git -C "$t" apply "$p" && { inst_say "applique : $(basename "$p")"; n=$((n+1)); }
        else
            inst_say "deja applique ou sans objet : $(basename "$p")"; skipped=$((skipped+1))
        fi
    done
    [ $n -eq 0 ] && return $INST_RC_DONE
    inst_ok
}
inst_patches_verify() { inst_ok; }

install_modules/60-binaires.sh

.sh 30 lignes - 1,2KB

install_modules/60-binaires.sh
# 60-binaires - depot des executables dans /usr/local/bin, comme le fait le Dockerfile.
INST_REGISTER binaires "Installation des binaires"
INST_DEPS[binaires]="build"

_BINS="trxcon:src/host/trxcon/src/trxcon
mobile:src/host/layer23/src/mobile/mobile
virtphy:src/host/virt_phy/src/virtphy
ccch_scan:src/host/layer23/src/misc/ccch_scan"

inst_binaires_done() { have_cmd mobile && have_cmd trxcon; }
inst_binaires_run() {
    local line name path n=0
    while IFS=: read -r name path; do
        [ -z "$name" ] && continue
        if [ -x "$GSM_ROOT/osmocom-bb/$path" ]; then
            install -m755 "$GSM_ROOT/osmocom-bb/$path" "/usr/local/bin/$name" && n=$((n+1))
        else
            inst_say "absent, ignore : $path"
        fi
    done <<< "$_BINS"
    [ $n -eq 0 ] && { inst_fail "aucun binaire trouve a installer - la compilation a-t-elle abouti ?"; return $INST_RC_FAIL; }
    inst_say "$n binaires installes"
    inst_ok
}
inst_binaires_verify() {
    local missing="" b
    for b in mobile trxcon; do have_cmd "$b" || missing="$missing $b"; done
    [ -n "$missing" ] && { inst_fail "binaires absents du PATH :$missing"; return $INST_RC_FAIL; }
    inst_ok
}

install_modules/70-configs.sh

.sh 28 lignes - 1,5KB

install_modules/70-configs.sh
# 70-configs - configuration Osmocom. `cp -n` : on n'ecrase JAMAIS une configuration
# existante, elle a pu etre adaptee a la machine.
INST_REGISTER configs "Configuration (/etc/osmocom)"
INST_DEPS[configs]="prereqs"

inst_configs_done() { have_file /etc/osmocom/osmo-bsc.cfg; }
inst_configs_run() {
    mkdir -p /etc/osmocom /var/log/osmocom /var/run/smsc "${OSMOCOM_HOME:-$HOME/.osmocom}/bb" \
        || { inst_fail "impossible de creer les repertoires de configuration"; return $INST_RC_FAIL; }
    if [ "${REINSTALL:-0}" = 1 ]; then
        # On ecrase, mais jamais sans copie : /etc/osmocom.<date> garde l etat
        # d avant, comme le fait --regen dans start-direct.sh.
        local sav="/etc/osmocom.$(date +%Y%m%d-%H%M%S)"
        cp -a /etc/osmocom "$sav" 2>/dev/null && inst_say "sauvegarde : $sav"
        cp -f "$INST_TREE"/configs/*.cfg /etc/osmocom/ 2>/dev/null
        [ -f "$INST_TREE/configs/mobile.cfg" ] && cp -f "$INST_TREE/configs/mobile.cfg" "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile.cfg"
        inst_say "configurations reecrites (--reinstall)"
    else
        cp -n "$INST_TREE"/configs/*.cfg /etc/osmocom/ 2>/dev/null
        [ -f "$INST_TREE/configs/mobile.cfg" ] && cp -n "$INST_TREE/configs/mobile.cfg" "${OSMOCOM_HOME:-$HOME/.osmocom}/bb/mobile.cfg"
        inst_say "configurations deposees (les fichiers existants ont ete conserves)"
    fi
    inst_ok
}
inst_configs_verify() {
    have_dir /etc/osmocom || { inst_fail "/etc/osmocom absent"; return $INST_RC_FAIL; }
    inst_ok
}

install_modules/80-bureau.sh

.sh 106 lignes - 4,8KB

install_modules/80-bureau.sh
# =============================================================================
#  80-bureau - icones et raccourcis du bureau (install native)
# =============================================================================
#  Pose EXACTEMENT ce que l ISO pose (build-iso.sh, etape 8d) a partir des
#  memes fichiers du depot :
#     data/desktop/osmo-launch.desktop   "Lancer le banc GSM"  -> launch.sh
#     data/*.svg                         les icones
#
#  osmo-multi (antenne, multi-operator) N EST PLUS POSEE ICI : son lanceur
#  start-multi.sh suppose le supplement SS7 (docker + image + topologie), qui
#  n existe qu apres addition.sh. C est addition.sh qui pose l antenne, a la fin
#  d une install SS7 reussie - pas avant, ou elle ne ferait rien au clic.
#  et le lien /usr/local/bin/osmo-start-direct -> start-direct.sh.
#
#  Icon= est reecrit en CHEMIN ABSOLU (/usr/share/osmo-operator/icons/*.svg) :
#  un nom de theme passe par icon-theme.cache, qui ne connait pas une icone
#  arrivee apres lui, et GNOME affiche alors une page blanche sans rien dire
#  (voir update.sh, osmo_reposer_icones). Le cache est quand meme reconstruit
#  pour le menu des applications.
#
#  Les raccourcis sont copies sur le bureau de root et de l utilisateur qui a
#  lance sudo (SUDO_USER), en Bureau/ et Desktop/. L attribut metadata::trusted
#  ne peut pas etre pose ici (il vit dans les metadonnees gvfs de la session) :
#  update.sh le pose au login, et un double-clic + "Autoriser" suffit sinon.
#
#  Ni osmo-tutorial, ni osmo-addition, ni osmo-update : leurs lanceurs sont
#  des enrobages ecrits par build-iso.sh pour l image (terminal + pkexec) et
#  dependent du bureau GNOME de l ISO. Sur une machine deja installee, launch.sh
#  et start-multi.sh se suffisent.
# -----------------------------------------------------------------------------
INST_REGISTER bureau "Bureau : icones et raccourcis"
INST_DEPS[bureau]="configs"
INST_REQUIRED[bureau]=0

_BUREAU_ICONS=/usr/share/osmo-operator/icons
_BUREAU_ENTRIES="osmo-launch"

_bureau_homes() {
    local h
    printf '%s\n' /root
    if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
        h="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
        [ -n "$h" ] && [ -d "$h" ] && printf '%s\n' "$h"
    fi
}

inst_bureau_check() {
    [ -d "$INST_TREE/data/desktop" ] || { inst_fail "data/desktop/ absent du depot"; return $INST_RC_FAIL; }
    [ -x "$INST_TREE/launch.sh" ] || inst_hint "launch.sh n est pas executable : chmod +x $INST_TREE/launch.sh"
    inst_ok
}

inst_bureau_done() {
    local e
    for e in $_BUREAU_ENTRIES; do
        have_file "/usr/share/applications/$e.desktop" || return 1
        have_file "$_BUREAU_ICONS/$e.svg" || return 1
    done
    [ -L /usr/local/bin/osmo-start-direct ] || return 1
    return 0
}

inst_bureau_run() {
    local e h d f
    install -d "$_BUREAU_ICONS" /usr/share/icons/hicolor/scalable/apps /usr/share/applications \
        || { inst_fail "impossible de creer les repertoires du bureau"; return $INST_RC_FAIL; }
    for e in $_BUREAU_ENTRIES; do
        [ -f "$INST_TREE/data/$e.svg" ] && {
            install -m644 "$INST_TREE/data/$e.svg" "$_BUREAU_ICONS/$e.svg"
            install -m644 "$INST_TREE/data/$e.svg" "/usr/share/icons/hicolor/scalable/apps/$e.svg"
        }
        install -m644 "$INST_TREE/data/desktop/$e.desktop" "/usr/share/applications/$e.desktop" \
            || { inst_fail "copie impossible : $e.desktop"; return $INST_RC_FAIL; }
        # Exec= et Icon= suivent l arbre INSTALLE, pas /opt/GSM en dur : ce
        # depot peut vivre ailleurs (GSM_ROOT).
        sed -i -e "s|^Exec=/opt/GSM/osmo-operator/|Exec=$INST_TREE/|" \
               -e "s|^Icon=.*|Icon=$_BUREAU_ICONS/$e.svg|" "/usr/share/applications/$e.desktop"
        inst_say "menu : /usr/share/applications/$e.desktop"
    done
    ln -sf "$INST_TREE/start-direct.sh" /usr/local/bin/osmo-start-direct

    while read -r h; do
        for d in Bureau Desktop; do
            [ -d "$h/$d" ] || continue
            for e in $_BUREAU_ENTRIES; do
                f="$h/$d/$e.desktop"
                cp -f "/usr/share/applications/$e.desktop" "$f" && chmod +x "$f"
                inst_say "bureau : $f"
            done
            [ "$h" != /root ] && chown "$SUDO_USER" "$h/$d"/osmo-*.desktop 2>/dev/null
        done
    done < <(_bureau_homes)

    command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache -f -q /usr/share/icons/hicolor 2>/dev/null
    command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database /usr/share/applications 2>/dev/null
    inst_ok
}

inst_bureau_verify() {
    local e
    for e in $_BUREAU_ENTRIES; do
        grep -q "^Exec=$INST_TREE/" "/usr/share/applications/$e.desktop" \
            || { inst_fail "$e.desktop ne pointe pas sur $INST_TREE"; return $INST_RC_FAIL; }
    done
    inst_ok
}

install_modules/90-verify.sh

.sh 17 lignes - 734B

install_modules/90-verify.sh
# 90-verify - controle d'ensemble. Distinct des `verify` de chaque etape :
# ici on regarde si la machine est utilisable, pas si une etape a reussi.
INST_REGISTER verify "Verification de l'installation"
INST_DEPS[verify]="binaires configs"
INST_ROOT[verify]=0

inst_verify_run() {
    local missing="" b
    for b in osmo-stp osmo-hlr osmo-msc osmo-mgw osmo-bsc; do have_cmd "$b" || missing="$missing $b"; done
    if [ -n "$missing" ]; then
        inst_hint "les demons Osmocom ne sont pas dans les depots Ubuntu par defaut : ajoutez le depot Osmocom, ou utilisez Docker (./tools/make-docker-image.sh)"
        inst_fail "demons absents :$missing"
        return $INST_RC_FAIL
    fi
    inst_ok
}
inst_verify_verify() { inst_ok; }

install_modules/_lib/inst.sh

.sh 60 lignes - 2,5KB

install_modules/_lib/inst.sh
# =============================================================================
#  install_modules/_lib/inst.sh - contrat des modules d'installation
# =============================================================================
#
#  Meme principe que run_modules/_lib/mod.sh, mais pour l'installation. La
#  difference tient a la nature des etapes : on n'y "demarre" rien, on
#  transforme la machine - donc on veut savoir, pour chacune, si elle est DEJA
#  faite (idempotence) et si elle a REUSSI (verification).
#
#  Quatre fonctions par module, `run` seule obligatoire :
#     inst_<p>_check    prerequis, LECTURE SEULE          0 ok · 1 manque · 4 sans objet
#     inst_<p>_done     deja installe ?                   0 oui · 1 non
#     inst_<p>_run      fait le travail                   0 ok · 1 echec · 3 deja fait
#     inst_<p>_verify   controle APRES coup               0 ok · 1 echec
#
#  `verify` est le pendant de la barriere `wait` cote execution : installer
#  n'est pas avoir installe. Un `apt-get` qui rend 0 mais laisse un binaire
#  absent doit etre vu ici, pas trois etapes plus loin.
# -----------------------------------------------------------------------------

readonly INST_RC_OK=0
readonly INST_RC_FAIL=1
readonly INST_RC_DONE=3
readonly INST_RC_NA=4

declare -a INST_ORDER=()
declare -A INST_DESC=()      # description affichee
declare -A INST_DEPS=()      # etapes prerequises
declare -A INST_REQUIRED=()  # 1 = un echec arrete tout
declare -A INST_ROOT=()      # 1 = exige les droits root
declare -A INST_TIMEOUT=()   # secondes, pour les etapes longues

INST_REGISTER() {
    local slug="$1" desc="$2"
    INST_ORDER+=("$slug")
    INST_DESC[$slug]="$desc"
    INST_DEPS[$slug]=""
    INST_REQUIRED[$slug]=1
    INST_ROOT[$slug]=1
    INST_TIMEOUT[$slug]=1800
}

inst_prefix() { printf 'inst_%s' "${1//-/_}"; }

# --- protocole de message (identique a run_modules) ---------------------------
_INST_REASON=""
_INST_HINT=""
inst_ok()   { _INST_REASON=""   ; return $INST_RC_OK; }
inst_fail() { _INST_REASON="$*" ; return $INST_RC_FAIL; }
inst_done() { _INST_REASON="$*" ; return $INST_RC_DONE; }
inst_na()   { _INST_REASON="$*" ; return $INST_RC_NA; }
inst_hint() { _INST_HINT="$*"; }
inst_say()  { printf '%s\n' "$*"; }

# --- sondes -------------------------------------------------------------------
have_cmd()  { command -v "$1" >/dev/null 2>&1; }
have_dir()  { [ -d "$1" ]; }
have_file() { [ -f "$1" ]; }
have_pkg()  { dpkg -s "$1" >/dev/null 2>&1; }
have_repo() { [ -d "$1/.git" ]; }

install.sh

.sh 192 lignes - 8,4KB

install.sh
#!/bin/bash
# =============================================================================
#  install.sh - installation native, par modules
# =============================================================================
#
#  Meme architecture que run.sh : ce fichier n'est qu'un moteur. Chaque etape
#  vit dans install_modules/NN-<slug>.sh et respecte le contrat decrit dans
#  install_modules/_lib/inst.sh.
#
#      sudo ./install.sh                tout
#      ./install.sh --list              les etapes, sans rien faire
#      ./install.sh --dry-run           deroule sans effet de bord
#      sudo ./install.sh --only deps    une seule etape
#      sudo ./install.sh --skip build   tout sauf celle-la
#      ./install.sh --check             ce qui est deja installe
#      sudo ./install.sh --reinstall    rejoue TOUT, meme ce qui est deja la
#                                       (sources mises a jour, configs et
#                                       raccourcis reecrits, binaires refaits)
#
#  Ou : GSM_ROOT (defaut /opt/GSM) recoit sources et binaires.
#  Le bureau (icones, raccourcis "Lancer le banc GSM" et "multi-operator") est
#  l etape `bureau` : elle pose les MEMES fichiers que l ISO (data/desktop/).
#
#  SI VOUS DECOUVREZ LE PROJET, PREFEREZ DOCKER : `./tools/make-docker-image.sh` produit une image
#  complete et reproductible. Cette installation native s'adresse a ceux qui
#  veulent travailler sur les sources.
#
#  La liste des paquets n'est pas dupliquee ici : elle est EXTRAITE du
#  Dockerfile, qui reste la reference. Les deux ne peuvent donc pas diverger.
# -----------------------------------------------------------------------------
set -uo pipefail

INST_TREE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODDIR="$INST_TREE/install_modules"
: "${GSM_ROOT:=/opt/GSM}"
: "${OSMOCOM_HOME:=$HOME/.osmocom}"
export INST_TREE GSM_ROOT OSMOCOM_HOME

DRY=0 ONLY="" SKIP="" ACTION=install REINSTALL=0
while [ $# -gt 0 ]; do
    case "$1" in
        --list)    ACTION=list ;;
        --check)   ACTION=check ;;
        --dry-run) DRY=1 ;;
        --reinstall) REINSTALL=1 ;;
        --only)    ONLY="${2:-}"; shift ;;
        --skip)    SKIP="${2:-}"; shift ;;
        -h|--help) sed -n '3,24p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'; exit 0 ;;
        *) printf 'option inconnue : %s\n' "$1" >&2; exit 2 ;;
    esac
    shift
done

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    TTY=1; C_OK=$'\033[32m'; C_KO=$'\033[31m'; C_SK=$'\033[33m'; C_DIM=$'\033[2m'; C_Z=$'\033[0m'
else
    TTY=0; C_OK=""; C_KO=""; C_SK=""; C_DIM=""; C_Z=""
fi
begin() { if [ $TTY -eq 1 ]; then printf '[ %s..%s ] %s' "$C_DIM" "$C_Z" "$1"; else printf '[ .. ] %s\n' "$1"; fi; }
end()   { [ $TTY -eq 1 ] && printf '\r\033[K'; printf '[%s%s%s] %s' "$2" "$1" "$C_Z" "$3"
          [ -n "${4:-}" ] && printf ' %s(%s)%s' "$C_DIM" "$4" "$C_Z"; printf '\n'; }

LOGDIR="${LOG_DIR:-/tmp/osmo-nitb-install}"; mkdir -p "$LOGDIR" 2>/dev/null || true
export REINSTALL

[ -d "$MODDIR" ] || { printf 'install_modules/ introuvable\n' >&2; exit 2; }
. "$MODDIR/_lib/inst.sh"
shopt -s nullglob; for f in "$MODDIR"/[0-9][0-9]-*.sh; do . "$f"; done; shopt -u nullglob
[ ${#INST_ORDER[@]} -eq 0 ] && { printf 'aucun module d installation\n' >&2; exit 2; }

in_csv() { case ",$1," in *",$2,"*) return 0;; esac; return 1; }
selected=()
for s in "${INST_ORDER[@]}"; do
    [ -n "$ONLY" ] && { in_csv "$ONLY" "$s" || continue; }
    [ -n "$SKIP" ] && { in_csv "$SKIP" "$s" && continue; }
    selected+=("$s")
done

if [ "$ACTION" = list ]; then
    printf 'Etapes - %d, dans cet ordre\n\n' "${#selected[@]}"
    for s in "${selected[@]}"; do
        printf '  %-10s %-38s %s%s\n' "$s" "${INST_DESC[$s]}" \
            "$([ "${INST_REQUIRED[$s]}" = 1 ] && echo obligatoire || echo optionnelle)" \
            "$([ -n "${INST_DEPS[$s]}" ] && echo "  apres: ${INST_DEPS[$s]}")"
    done
    printf '\nGSM_ROOT = %s\n' "$GSM_ROOT"
    exit 0
fi

if [ "$ACTION" = check ]; then
    rc=0
    for s in "${selected[@]}"; do
        p="$(inst_prefix "$s")"
        if declare -F "${p}_done" >/dev/null; then
            if "${p}_done" >/dev/null 2>&1; then end " OK " "$C_OK" "${INST_DESC[$s]}" "deja installe"
            else end "----" "$C_SK" "${INST_DESC[$s]}" "a faire"; rc=1; fi
        else
            end " ?  " "$C_DIM" "${INST_DESC[$s]}" "pas de controle"
        fi
    done
    exit $rc
fi

# root : exige seulement si une etape retenue le demande
need_root=0
for s in "${selected[@]}"; do [ "${INST_ROOT[$s]}" = 1 ] && need_root=1; done
if [ $DRY -eq 0 ] && [ $need_root -eq 1 ] && [ "$(id -u)" -ne 0 ]; then
    printf 'Cette installation ecrit dans %s, /usr/local et /etc/osmocom.\n' "$GSM_ROOT" >&2
    printf 'Relancez avec sudo, ou inspectez sans rien modifier :  ./install.sh --dry-run\n' >&2
    exit 2
fi

declare -A STATE=()
nb_ok=0 nb_skip=0 nb_fail=0
for s in "${selected[@]}"; do
    p="$(inst_prefix "$s")"; log="$LOGDIR/$s.log"
    _INST_REASON=""; _INST_HINT=""

    # Une etape prerequise que l appelant a ECARTEE (--only, --skip) n est pas
    # un echec : c est son choix. Seule une etape jouee et RATEE bloque. Sans
    # ca, "--only deps" sautait deps parce que prereqs n etait pas dans la
    # selection - l exemple de l en-tete ne marchait pas.
    dep_ko=""
    for d in ${INST_DEPS[$s]}; do
        in_csv "$(IFS=,; echo "${selected[*]}")" "$d" || continue
        case "${STATE[$d]:-absent}" in ok|skip) ;; *) dep_ko="$d"; break;; esac
    done
    if [ -n "$dep_ko" ]; then
        end "SKIP" "$C_SK" "${INST_DESC[$s]}" "depend de $dep_ko, non satisfaite"
        STATE[$s]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    begin "${INST_DESC[$s]}"

    # --reinstall : on ne demande plus « deja fait ? », on refait. Les modules
    # lisent REINSTALL pour ecraser ce qu ils conservent d ordinaire (cp -n,
    # depot laisse tel quel).
    if [ "$REINSTALL" -eq 0 ] && declare -F "${p}_done" >/dev/null && "${p}_done" >>"$log" 2>&1; then
        end "SKIP" "$C_SK" "${INST_DESC[$s]}" "deja installe"
        STATE[$s]=skip; nb_skip=$((nb_skip+1)); continue
    fi

    if declare -F "${p}_check" >/dev/null; then
        "${p}_check" >>"$log" 2>&1; rc=$?
        if [ $rc -eq $INST_RC_FAIL ]; then
            end "FAIL" "$C_KO" "${INST_DESC[$s]}" "${_INST_REASON:-prerequis non satisfait}"
            [ -n "$_INST_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_INST_HINT" "$C_Z"
            printf '       %sjournal : %s%s\n' "$C_DIM" "$log" "$C_Z"
            nb_fail=$((nb_fail+1)); STATE[$s]=fail
            [ "${INST_REQUIRED[$s]}" = 1 ] && { printf '\ninstallation interrompue\n'; exit 1; }
            continue
        fi
    fi

    if [ $DRY -eq 1 ]; then
        end " -- " "$C_DIM" "${INST_DESC[$s]}" "simule"; STATE[$s]=ok; continue
    fi

    "${p}_run" >>"$log" 2>&1; rc=$?
    case $rc in
        $INST_RC_DONE) end "SKIP" "$C_SK" "${INST_DESC[$s]}" "${_INST_REASON:-rien a faire}"
                       STATE[$s]=skip; nb_skip=$((nb_skip+1)); continue ;;
        $INST_RC_FAIL) end "FAIL" "$C_KO" "${INST_DESC[$s]}" "${_INST_REASON:-echec}"
                       [ -n "$_INST_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_INST_HINT" "$C_Z"
                       printf '       %sjournal : %s%s\n' "$C_DIM" "$log" "$C_Z"
                       nb_fail=$((nb_fail+1)); STATE[$s]=fail
                       [ "${INST_REQUIRED[$s]}" = 1 ] && { printf '\ninstallation interrompue\n'; exit 1; }
                       continue ;;
    esac

    # installer n'est pas avoir installe : on controle apres coup
    if declare -F "${p}_verify" >/dev/null; then
        if ! "${p}_verify" >>"$log" 2>&1; then
            end "FAIL" "$C_KO" "${INST_DESC[$s]}" "faite, mais le controle echoue : ${_INST_REASON:-}"
            [ -n "$_INST_HINT" ] && printf '       %s→ %s%s\n' "$C_DIM" "$_INST_HINT" "$C_Z"
            printf '       %sjournal : %s%s\n' "$C_DIM" "$log" "$C_Z"
            nb_fail=$((nb_fail+1)); STATE[$s]=fail
            [ "${INST_REQUIRED[$s]}" = 1 ] && { printf '\ninstallation interrompue\n'; exit 1; }
            continue
        fi
    fi

    end " OK " "$C_OK" "${INST_DESC[$s]}"; STATE[$s]=ok; nb_ok=$((nb_ok+1))
done

printf '\n%d ok · %d ignores · %d echecs\n' "$nb_ok" "$nb_skip" "$nb_fail"
if [ $nb_fail -eq 0 ] && [ $DRY -eq 0 ]; then
    printf '\n  %sverifier%s   ./install.sh --check\n'          "$C_DIM" "$C_Z"
    printf '  %slancer%s     ./start-direct.sh --list   puis   sudo ./start-direct.sh\n\n' "$C_DIM" "$C_Z"
fi
[ $nb_fail -gt 0 ] && exit 1
exit 0

launch/osmo-launch.sh

.sh 283 lignes - 11KB

launch/osmo-launch.sh
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# /opt/osmo-launch.sh - Orchestrateur osmo-operator (lab + web dashboard)
#
# Usage :
#   sudo /opt/osmo-launch.sh           # lance tout (interactif)
#   sudo /opt/osmo-launch.sh --auto    # lance avec les defauts (2 ops, 8 MS)
#   sudo /opt/osmo-launch.sh stop      # arrete tout
#   sudo /opt/osmo-launch.sh status    # etat des services
#   sudo /opt/osmo-launch.sh web-only  # lance uniquement le dashboard
# ══════════════════════════════════════════════════════════════════════════════
set -euo pipefail

LAB_DIR="/opt/GSM/osmo-operator"
WEB_DIR="/opt/GSM/osmo-egprs-web"
WEB_SERVICE="osmo-egprs-web"
LOG_DIR="/var/log/osmocom"

RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'
YELLOW='\033[1;33m'; NC='\033[0m'; BOLD='\033[1m'

[ "$(id -u)" -ne 0 ] && { echo -e "${RED}Root requis.${NC}"; exit 1; }

mkdir -p "$LOG_DIR"

# ══════════════════════════════════════════════════════════════════════════════
banner() {
    echo -e "${CYAN}${BOLD}"
    echo "╔══════════════════════════════════════════════════════════════╗"
    echo "║  osmo-operator - GSM/EGPRS Multi-PLMN + Web Dashboard          ║"
    echo "╚══════════════════════════════════════════════════════════════╝"
    echo -e "${NC}"
}

# ══════════════════════════════════════════════════════════════════════════════
wait_docker() {
    echo -ne "  Docker daemon"
    local i=0
    while ! docker info &>/dev/null; do
        sleep 1; echo -n "."; i=$((i+1))
        [ $i -ge 30 ] && { echo -e " ${RED}TIMEOUT${NC}"; return 1; }
    done
    echo -e " ${GREEN}OK${NC}"
}

wait_operators() {
    local timeout="${1:-120}"
    echo -ne "  Containers operateurs"
    local i=0
    while [ "$(docker ps --filter 'name=osmo-operator-' --format '{{.Names}}' 2>/dev/null | wc -l)" -eq 0 ]; do
        sleep 2; echo -n "."; i=$((i+2))
        [ $i -ge "$timeout" ] && { echo -e " ${RED}TIMEOUT${NC}"; return 1; }
    done
    local n=$(docker ps --filter 'name=osmo-operator-' --format '{{.Names}}' | wc -l)
    echo -e " ${GREEN}${n} operateur(s) detectes${NC}"
}

# ══════════════════════════════════════════════════════════════════════════════
start_web() {
    echo -e "${GREEN}[web] Demarrage dashboard...${NC}"

    if [ -f "$WEB_DIR/server.js" ] && command -v node &>/dev/null; then
        # Mode natif (Node.js local)
        systemctl restart "$WEB_SERVICE" 2>/dev/null && {
            echo -e "  ${GREEN}✓${NC} systemd: $WEB_SERVICE actif (port 8080)"
            return 0
        }
        # Fallback si le service systemd n'existe pas
        echo -e "  ${YELLOW}Service systemd absent, lancement direct...${NC}"
        cd "$WEB_DIR"
        node server.js --verbose >> "$LOG_DIR/web-dashboard.log" 2>&1 &
        echo $! > /var/run/osmo-egprs-web.pid
        echo -e "  ${GREEN}✓${NC} Dashboard lance (PID $(cat /var/run/osmo-egprs-web.pid))"

    elif [ -f "$WEB_DIR/Dockerfile" ] || [ -f "$WEB_DIR/start-web.sh" ]; then
        # Mode Docker
        if [ -f "$WEB_DIR/start-web.sh" ]; then
            bash "$WEB_DIR/start-web.sh"
        else
            docker rm -f osmo-egprs-web 2>/dev/null || true
            docker build -t osmo-egprs-web "$WEB_DIR"
            docker run -d \
                --name osmo-egprs-web \
                --network host \
                --cap-add=NET_RAW --cap-add=NET_ADMIN \
                -v /var/run/docker.sock:/var/run/docker.sock:ro \
                -e CONTAINER_PREFIX=osmo-operator- \
                --restart unless-stopped \
                osmo-egprs-web
        fi
        echo -e "  ${GREEN}✓${NC} Dashboard Docker lance (port 80)"
    else
        echo -e "  ${RED}✗${NC} Dashboard introuvable dans $WEB_DIR"
        return 1
    fi
}

stop_web() {
    echo -e "${YELLOW}[web] Arret dashboard...${NC}"
    systemctl stop "$WEB_SERVICE" 2>/dev/null || true
    [ -f /var/run/osmo-egprs-web.pid ] && {
        kill "$(cat /var/run/osmo-egprs-web.pid)" 2>/dev/null || true
        rm -f /var/run/osmo-egprs-web.pid
    }
    docker rm -f osmo-egprs-web 2>/dev/null || true
    echo -e "  ${GREEN}✓${NC} Dashboard arrete"
}

# ══════════════════════════════════════════════════════════════════════════════
start_lab() {
    echo -e "${GREEN}[lab] Demarrage osmo-operator...${NC}"

    if [ ! -f "$LAB_DIR/start.sh" ]; then
        echo -e "  ${RED}✗${NC} $LAB_DIR/start.sh introuvable"
        return 1
    fi

    cd "$LAB_DIR"
    bash ./start.sh
}

stop_lab() {
    echo -e "${YELLOW}[lab] Arret osmo-operator...${NC}"
    if [ -f "$LAB_DIR/start.sh" ]; then
        cd "$LAB_DIR"
        bash ./start.sh stop
    fi
    # Nettoyage complet si start.sh stop ne suffit pas
    docker ps -a --filter "name=osmo-" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true
    echo -e "  ${GREEN}✓${NC} Lab arrete"
}

# ══════════════════════════════════════════════════════════════════════════════
show_status() {
    echo -e "${CYAN}${BOLD}── Etat des services ──${NC}"
    echo ""

    # Docker
    if docker info &>/dev/null; then
        echo -e "  Docker       : ${GREEN}●${NC} actif"
    else
        echo -e "  Docker       : ${RED}●${NC} arrete"
    fi

    # Containers
    local ops=$(docker ps --filter 'name=osmo-operator-' --format '{{.Names}}' 2>/dev/null | sort)
    local n_ops=$(echo "$ops" | grep -c . 2>/dev/null || echo 0)
    if [ "$n_ops" -gt 0 ]; then
        echo -e "  Operateurs   : ${GREEN}●${NC} ${n_ops} actif(s)"
        echo "$ops" | while read -r c; do
            echo -e "                 ${CYAN}└─${NC} $c"
        done
    else
        echo -e "  Operateurs   : ${RED}●${NC} aucun"
    fi

    # Inter-STP
    if docker ps --format '{{.Names}}' 2>/dev/null | grep -q osmo-inter-stp; then
        echo -e "  Inter-STP    : ${GREEN}●${NC} actif"
    else
        echo -e "  Inter-STP    : ${RED}●${NC} arrete"
    fi

    # Web dashboard
    if systemctl is-active --quiet "$WEB_SERVICE" 2>/dev/null; then
        echo -e "  Dashboard    : ${GREEN}●${NC} systemd (port 8080)"
    elif [ -f /var/run/osmo-egprs-web.pid ] && kill -0 "$(cat /var/run/osmo-egprs-web.pid 2>/dev/null)" 2>/dev/null; then
        echo -e "  Dashboard    : ${GREEN}●${NC} PID $(cat /var/run/osmo-egprs-web.pid) (port 8080)"
    elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q osmo-egprs-web; then
        echo -e "  Dashboard    : ${GREEN}●${NC} Docker (port 80)"
    else
        echo -e "  Dashboard    : ${RED}●${NC} arrete"
    fi

    # IPs
    echo ""
    local host_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
    [ -n "$host_ip" ] && echo -e "  Host IP      : ${CYAN}${host_ip}${NC}"
    echo -e "  Dashboard    : ${CYAN}http://${host_ip:-localhost}:8080${NC}"
    echo ""
}

# ══════════════════════════════════════════════════════════════════════════════
do_start() {
    banner

    # 1. Docker
    echo -e "${GREEN}[1/3] Verification Docker...${NC}"
    systemctl start docker 2>/dev/null || true
    wait_docker || { echo -e "${RED}Docker necessaire.${NC}"; exit 1; }

    # 2. Lab
    echo ""
    echo -e "${GREEN}[2/3] Lab multi-operateurs...${NC}"
    start_lab

    # 3. Web (attendre que les containers soient up)
    echo ""
    echo -e "${GREEN}[3/3] Dashboard web...${NC}"
    wait_operators 180 || true
    start_web || true

    # Resume
    echo ""
    show_status
    echo -e "${GREEN}${BOLD}Pret !${NC}"
}

do_auto() {
    banner
    echo -e "${YELLOW}Mode automatique (defauts)${NC}"
    echo ""

    systemctl start docker 2>/dev/null || true
    wait_docker || exit 1

    # Lancer start.sh en mode non-interactif avec des defauts
    # On pipe les reponses au script interactif
    cd "$LAB_DIR"
    {
        echo "2"       # bridge mode
        echo "2"       # 2 operateurs
        echo "o"       # valeurs par defaut
        echo "8"       # 8 MS
        echo "N"       # pas de WAN
        echo "1"       # faketrx
        echo "0"       # A5/0
    } | bash ./start.sh

    wait_operators 180 || true
    start_web || true

    echo ""
    show_status
}

# ══════════════════════════════════════════════════════════════════════════════
# Main
# ══════════════════════════════════════════════════════════════════════════════
CMD="${1:-}"

case "$CMD" in
    stop)
        banner
        stop_web
        stop_lab
        echo -e "\n${GREEN}Tout arrete.${NC}"
        ;;
    status)
        banner
        show_status
        ;;
    web-only)
        banner
        systemctl start docker 2>/dev/null || true
        wait_docker || exit 1
        start_web
        ;;
    --auto)
        do_auto
        ;;
    restart)
        banner
        stop_web
        stop_lab
        sleep 3
        do_start
        ;;
    help|-h|--help)
        echo "Usage: $0 [commande]"
        echo ""
        echo "  (rien)    Lancement interactif (lab + web)"
        echo "  --auto    Lancement automatique (2 ops, 8 MS, defauts)"
        echo "  stop      Arrete tout (lab + web + containers)"
        echo "  restart   Redemarre tout"
        echo "  status    Etat des services"
        echo "  web-only  Lance uniquement le dashboard web"
        echo "  help      Ce message"
        ;;
    *)
        do_start
        ;;
esac

launch.sh

.sh 344 lignes - 19KB

launch.sh
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# launch.sh - Point d entree DOUBLE-CLIC du banc osmo-operator.
#
# Monte le poste de travail complet en un geste :
#     1. wireshark    sur udp/4729 (GSMTAP)        - root
#     2. linphone     le softphone SIP             - utilisateur de la session
#     3. firefox      sur http://127.0.0.1:8080    - des que le dashboard repond
#     4. start-direct.sh                           - le banc, au PREMIER PLAN
#
# Les trois premiers partent en arriere-plan et ne peuvent pas faire echouer le
# lancement : un outil absent se signale et on continue. start-direct.sh, lui,
# tient le terminal - c est sa sortie qu on veut lire.
#
# Trois choses separent un script qui marche au terminal d un script qui marche
# au double-clic, et elles sont traitees en tete :
#   1. PAS DE TERMINAL. Un .desktop lance sans tty : start-direct.sh ecrirait
#      dans le vide. On se relance dans un emulateur de terminal.
#   2. PAS DE ROOT. Le banc a besoin des privileges (netns, systemd, captures).
#      Depuis une icone : pkexec (fenetre graphique), repli sudo.
#   3. LA FENETRE SE FERME AVANT QU ON LISE L ERREUR. On la retient a la fin.
#
# Usage au terminal (aucun de ces detours ne s applique) :
#     sudo ./launch.sh [arguments passes a start-direct.sh]
# ══════════════════════════════════════════════════════════════════════════════
set -uo pipefail

DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
TARGET="$DIR/start-direct.sh"
GSMTAP_PORT="${GSMTAP_PORT:-4729}"
DASH_URL="${OSMO_DASH_URL:-http://127.0.0.1:8080}"
DASH_PORT="${DASH_URL##*:}"
# Le tutoriel s ouvre en DEUXIEME ONGLET, a cote du tableau de bord. Servi par
# le dashboard (et non en file://) : Firefox est un snap, son bac a sable ne lui
# donne acces ni a /root ni aux repertoires caches - un file:// rendrait
# "L acces au fichier a ete refuse". Voir /usr/local/bin/osmo-tutorial.
TUTO_URL="${OSMO_TUTORIAL_URL:-${DASH_URL%/}/tutorial.html}"

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m'

[ -x "$TARGET" ] || { echo -e "${RED}start-direct.sh introuvable ou non executable dans $DIR${NC}" >&2; exit 1; }

# ── 1. Un terminal, si on n en a pas — OU s il n est pas a nous ─────────────
# On teste la presence d un TTY, pas $DISPLAY : c est la difference reelle entre
# « lance a la main » et « lance par une icone ». OSMO_LAUNCH_TERM marque le
# tour de relance pour ne pas boucler.
#
# [2026-08-31] DEUX DEFAUTS, LE MEME MALENTENDU : la garde demandait « y a-t-il
# UN terminal ? », jamais « est-ce LE MIEN ? ».
#   1. stdin SEUL. `./launch.sh < fichier` depuis un vrai terminal se croyait
#      lance par une icone et ouvrait une fenetre en trop, dont la sortie
#      n allait pas a l appelant. On teste donc stdin ET stdout.
#   2. UN TTY HERITE N EST PAS LE NOTRE. start-direct.sh TIENT le terminal
#      jusqu a la fin (cf. l en-tete de ce fichier). Appele par un autre
#      lanceur — start-multi.sh, qui monte le natif AVANT ses conteneurs — il
#      heritait du terminal de celui-ci, ecrivait par-dessus sa sortie et ne
#      lui rendait la main qu a la fin du natif. OSMO_TERM_TAKEN=1 dit « ce
#      terminal est deja occupe » : on ouvre alors LA NOTRE, tty ou pas.
_lt_besoin=0
{ [ ! -t 0 ] || [ ! -t 1 ]; } && _lt_besoin=1
[ "${OSMO_TERM_TAKEN:-0}" = "1" ] && _lt_besoin=1
if [ "$_lt_besoin" = "1" ] && [ "${OSMO_LAUNCH_TERM:-0}" != "1" ]; then
    export OSMO_LAUNCH_TERM=1
    unset OSMO_TERM_TAKEN            # la fenetre qu on ouvre ici EST la notre
    for _t in gnome-terminal xfce4-terminal konsole xterm; do
        command -v "$_t" >/dev/null 2>&1 || continue
        case "$_t" in
            gnome-terminal) exec "$_t" -- "$0" "$@" ;;
            *)              exec "$_t" -e "$0" "$@" ;;
        esac
    done
    # AUCUN emulateur installe. On ne reprend pas le terminal de l appelant -
    # ce serait refaire exactement la panne que cette garde empeche. On se
    # detache, et la sortie va dans un journal plutot que dans le vide.
    _lt_log="${OSMO_LAUNCH_LOG:-/var/log/osmo-launch.log}"
    echo -e "${YELLOW}Aucun emulateur de terminal - lancement detache, journal : ${_lt_log}${NC}" >&2
    exec setsid "$0" "$@" </dev/null >>"$_lt_log" 2>&1
fi

# Le drapeau a joue son role. On le RETIRE de l environnement : exporte, il
# descendait dans start-direct.sh et tout ce qui suit, ou il ferait croire a
# n importe quel descendant rappelant ce script qu il est deja dans sa fenetre
# - et plus aucune ne s ouvrirait. La valeur reste ici, dans une variable de
# shell, la ou on en a besoin (retenir la fenetre a la fin).
_own_window=0
[ "${OSMO_LAUNCH_TERM:-0}" = "1" ] && _own_window=1
unset OSMO_LAUNCH_TERM

# ── 2. Les privileges ───────────────────────────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
    if command -v pkexec >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
        # pkexec lave l environnement : DISPLAY et XAUTHORITY sont repasses a la
        # main, sinon rien de graphique ne peut s ouvrir ensuite.
        exec pkexec env DISPLAY="${DISPLAY:-}" XAUTHORITY="${XAUTHORITY:-}" \
             OSMO_LAUNCH_TERM="$_own_window" "$0" "$@"
    fi
    command -v sudo >/dev/null 2>&1 && exec sudo -E "$0" "$@"
    echo -e "${RED}Root requis et ni pkexec ni sudo disponibles.${NC}" >&2
    exit 1
fi

# ── L UTILISATEUR DE LA SESSION GRAPHIQUE ───────────────────────────────────
# Meme critere que helpers/prepare_host.sh et start.sh : le NOM du compte ne
# dit rien, la presence de son socket pulse si. Sur l ISO --desktop la session
# est ouverte sous root (gdm3 AutomaticLogin=root) - « root » est donc ici une
# reponse valide, pas une erreur.
session_user() {
    local u uid sock
    for u in "${HOST_PULSE_USER:-}" "${SUDO_USER:-}" "$(logname 2>/dev/null || true)"; do
        [ -n "$u" ] || continue
        uid="$(id -u "$u" 2>/dev/null)" || continue
        [ -S "/run/user/${uid}/pulse/native" ] && { echo "$u"; return 0; }
    done
    for sock in /run/user/*/pulse/native; do
        [ -S "$sock" ] || continue
        uid="${sock#/run/user/}"; uid="${uid%%/*}"
        u="$(getent passwd "$uid" 2>/dev/null | cut -d: -f1)"
        [ -n "$u" ] && { echo "$u"; return 0; }
    done
    echo "${SUDO_USER:-$(id -un)}"
}
GUI_USER="$(session_user)"
GUI_UID="$(id -u "$GUI_USER" 2>/dev/null || echo 0)"
GUI_HOME="$(getent passwd "$GUI_USER" 2>/dev/null | cut -d: -f6)"
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-${GUI_HOME:-/root}/.Xauthority}"

# Lance une application graphique sous le compte de la session. `setsid` la
# detache : elle survit a la fermeture de ce terminal, et son code de retour ne
# peut pas faire tomber launch.sh (set -e n est pas arme ici, mais on garde le
# || true pour l intention).
gui_run() {
    setsid sudo -u "$GUI_USER" \
        env DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" \
            DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${GUI_UID}/bus" \
        "$@" </dev/null >/dev/null 2>&1 &
    disown 2>/dev/null || true
}

# ── LE PAVAGE 2x2 ───────────────────────────────────────────────────────────
# Chaque action prend UN QUART de l ecran :
#
#        +---------------------+---------------------+
#        |  terminal du banc   |  firefox / dashboard|
#        |  (start-direct.sh)  |  127.0.0.1:8080     |
#        +---------------------+---------------------+
#        |  wireshark          |  linphone           |
#        |  udp/4729 GSMTAP    |  postes 100 / 200   |
#        +---------------------+---------------------+
#
# Pourquoi wmctrl et pas les raccourcis GNOME : le pavage au clavier est une
# action d utilisateur, il n existe pas en ligne de commande. wmctrl parle a
# l EWMH, ce que Mutter expose EN X11 - et l ISO force X11 (build-iso.sh ecrit
# WaylandEnable=false dans gdm3/custom.conf, la session Wayland tombant sur
# llvmpipe en machine virtuelle). Sous Wayland, rien de tout ceci ne repondrait
# et le pavage serait simplement ignore, sans casser le lancement.
#
# ⚠️ Retirer les etats maximise AVANT de poser la geometrie : une fenetre
# maximisee accepte l ordre et n en tient aucun compte - elle reste plein ecran
# et l on croit wmctrl casse.
tile_windows() {
    command -v wmctrl  >/dev/null 2>&1 || { echo -e "  ${YELLOW}○${NC} wmctrl absent - fenetres non pavees"; return 0; }
    command -v xdpyinfo >/dev/null 2>&1 || { echo -e "  ${YELLOW}○${NC} xdpyinfo absent - fenetres non pavees"; return 0; }

    local sw sh qw qh
    sw="$(xdpyinfo 2>/dev/null | awk '/dimensions:/{split($2,a,"x"); print a[1]; exit}')"
    sh="$(xdpyinfo 2>/dev/null | awk '/dimensions:/{split($2,a,"x"); print a[2]; exit}')"
    [ -n "$sw" ] && [ -n "$sh" ] || return 0
    qw=$(( sw / 2 )); qh=$(( sh / 2 ))

    # $1 = motif WM_CLASS (insensible a la casse), $2 = x, $3 = y
    place() {
        local pat="$1" x="$2" y="$3" i id
        for i in $(seq 1 90); do
            id="$(wmctrl -l -x 2>/dev/null | grep -iE "$pat" | awk '{print $1; exit}')"
            [ -n "$id" ] && break
            sleep 1
        done
        [ -n "$id" ] || return 0
        wmctrl -i -r "$id" -b remove,maximized_vert,maximized_horz 2>/dev/null || true
        wmctrl -i -r "$id" -e "0,${x},${y},${qw},${qh}" 2>/dev/null || true
    }

    place 'gnome-terminal|xfce4-terminal|konsole|xterm'  0    0
    place 'firefox|navigator'                          "$qw" 0
    place 'wireshark'                                   0   "$qh"
    place 'linphone'                                   "$qw" "$qh"
}

# ── TOUT ARRETER AVANT DE RELANCER ──────────────────────────────────────────
# [2026-08-31] Un clic = un banc NEUF. Sans ca, un lancement par-dessus un banc
# deja debout ne relance presque rien : les demons Osmocom ne relisent PAS leur
# configuration a chaud. On l a paye plusieurs fois - un osmo-stp realigne sur
# le bon point code gardait l ancien en memoire, et `kill -HUP` n y change rien
# (verifie : config a jour sur le disque, ASP toujours ASP_DOWN sur l ancienne
# adresse). Le seul geste qui compte est l arret complet.
#
# On arrete le NATIF (il delegue a run.sh --stop, qui demonte proprement radio
# et coeur). Rien ici ne doit faire echouer le lancement : ce qui est deja
# arrete l est tres bien, d ou les || true.
tout_arreter() {
    echo -e "  ${CYAN}→${NC} arret du banc en place (un clic = un banc neuf)"
    if [ -x "$DIR/start-direct.sh" ]; then
        timeout 120 "$DIR/start-direct.sh" --stop >/dev/null 2>&1 || true
        echo -e "    ${GREEN}✓${NC} pile native arretee"
    fi
    # Les conteneurs ne sont PAS touches ici : start.sh fait deja
    # `docker rm -f $(docker ps -aq --filter name=osmo-)` en tete de course.
    # Le refaire serait redondant, et surtout destructeur depuis launch.sh -
    # un clic sur le telephone rouge, qui ne monte que le natif, balayerait
    # les conteneurs multi-operateur d a cote.
    # Les ponts audio survivent aux conteneurs (setsid) : sans ca on empile un
    # relais de plus a chaque relance, et le son se dedouble.
    pkill -f 'paplay --server=tcp:' 2>/dev/null || true
}

tout_arreter

# ── LES OUTILS D EXPLOITATION SONT-ILS DEMANDES ? ───────────────────────────
# [2026-08-31] OSMO_LAUNCH_APPS=0 : ni wireshark, ni linphone. C est ce que
# demande start-multi.sh quand il declenche ce lanceur pour remonter le natif -
# il veut la PILE, pas une seconde instance des outils.
#
# ⚠️ FIREFOX N EST PAS CONCERNE, et c est deliberé : le navigateur porte le
# dashboard du natif et le tutoriel, et start-multi.sh y AJOUTE les onglets des
# dashboards conteneurs. Le couper ici priverait un clic sur l antenne bleue de
# la base sur laquelle ces onglets viennent se greffer - on n aurait plus que
# les deux consoles conteneurs, sans celle du natif ni le tutoriel.
# Firefox, lui, sait ne pas se dedoubler : une seconde invocation ajoute des
# onglets a la fenetre existante.
# Par defaut a 1 : le double-clic sur le telephone rouge ne change pas.
OSMO_LAUNCH_APPS="${OSMO_LAUNCH_APPS:-1}"
if [ "$OSMO_LAUNCH_APPS" != "1" ]; then
    echo -e "  ${CYAN}i${NC} mode banc seul : wireshark et linphone non lances (firefox garde ses onglets)"
fi

# ── 1. WIRESHARK sur udp/4729 ───────────────────────────────────────────────
# En ROOT, et pas sous $GUI_USER : la capture sur `any` demande CAP_NET_RAW.
# -k demarre la capture tout de suite (sinon on ouvre sur un ecran de choix
# d interface, et rien n est capture tant qu on n a pas clique).
# [2026-08-31] LES DEUX RAISONS DE NE PAS LANCER SONT DISTINGUEES.
# Le test cumulait "demande ?" et "installe ?" dans un seul if, et le else
# annoncait toujours "absent". En mode banc seul (OSMO_LAUNCH_APPS=0, pose par
# start-multi.sh), on lisait donc "wireshark absent" pour un wireshark
# parfaitement installe - un message qui envoie verifier un paquet present.
if [ "$OSMO_LAUNCH_APPS" != "1" ]; then
    echo -e "  ${CYAN}○${NC} wireshark : non demande (mode banc seul)"
elif command -v wireshark >/dev/null 2>&1; then
    setsid wireshark -k -i any -f "udp port ${GSMTAP_PORT}" \
        </dev/null >/dev/null 2>&1 &
    disown 2>/dev/null || true
    echo -e "  ${GREEN}✓${NC} wireshark : capture GSMTAP sur ${CYAN}udp/${GSMTAP_PORT}${NC}"
else
    echo -e "  ${YELLOW}○${NC} wireshark absent - capture GSMTAP ignoree"
fi

# ── 2. LINPHONE ─────────────────────────────────────────────────────────────
# Le compte SIP est pre-provisionne (configs/linphonerc, pose par build-iso.sh).
# ⚠️ Linphone ne relit PAS sa config a chaud : s il tourne deja, il garde son
# etat en memoire. On ne le relance donc pas de force ici - on le laisse tel
# quel plutot que de couper un appel en cours.
if [ "$OSMO_LAUNCH_APPS" != "1" ]; then
    echo -e "  ${CYAN}○${NC} linphone : non demande (mode banc seul)"
elif command -v linphone >/dev/null 2>&1; then
    if pgrep -x linphone >/dev/null 2>&1; then
        echo -e "  ${GREEN}✓${NC} linphone : deja lance (laisse en place)"
    else
        gui_run linphone
        echo -e "  ${GREEN}✓${NC} linphone : lance (user=${CYAN}${GUI_USER}${NC})"
    fi
else
    echo -e "  ${YELLOW}○${NC} linphone absent"
fi

# ── 3. FIREFOX sur le dashboard, QUAND IL REPOND ────────────────────────────
# Ouvrir l URL tout de suite donnerait une page d erreur : le dashboard n est
# pas encore la, et une fois l onglet en echec l utilisateur doit recharger a la
# main. On attend donc que le port reponde, en tache de fond, jusqu a 180 s.
if command -v firefox >/dev/null 2>&1; then
    (
        for _i in $(seq 1 180); do
            if (exec 3<>"/dev/tcp/127.0.0.1/${DASH_PORT}") 2>/dev/null; then
                exec 3>&- 2>/dev/null
                # Deux URL sur la meme ligne de commande = deux ONGLETS, dans
                # la meme fenetre. Un second `firefox <url>` marcherait aussi
                # (le premier processus recupere l argument), mais il faudrait
                # le cadencer : lance trop tot, il fait la course avec le
                # demarrage du navigateur et ouvre parfois une 2e fenetre.
                gui_run firefox "$DASH_URL" "$TUTO_URL"
                exit 0
            fi
            sleep 1
        done
    ) &
    disown 2>/dev/null || true
    echo -e "  ${GREEN}✓${NC} firefox : ouvrira ${CYAN}${DASH_URL}${NC} des que le dashboard repond"
else
    echo -e "  ${YELLOW}○${NC} firefox absent - dashboard a ouvrir a la main sur ${DASH_URL}"
fi

# Le pavage part en tache de fond : chaque fenetre est placee des qu elle
# apparait, sans retarder le demarrage du banc.
tile_windows &
disown 2>/dev/null || true

# ── 4. LE BANC, AU PREMIER PLAN ─────────────────────────────────────────────
echo
cd "$DIR" || exit 1

# ── AUCUNE SAISIE ───────────────────────────────────────────────────────────
# [2026-08-31] start-direct.sh ouvre menu_interactif() : Profil, Noeud SS7,
# Operateur, Inter-STP - quatre questions whiptail avant que quoi que ce soit
# ne demarre. Il les saute quand PERSONNE ne peut repondre, et il teste ca sur
# la presence d un terminal ([ -t 0 ]) - or launch.sh vient justement d en
# ouvrir un pour afficher la sortie. La garde ne se declenchait donc jamais
# ici, et un double-clic sur le telephone rouge s arretait sur un formulaire
# au lieu de lancer le banc.
#
# NO_MENU=1 est le drapeau prevu pour ca (start-direct.sh l.460 et l.1263) :
# les menus rendent la main sans rien demander et les valeurs par defaut
# s appliquent - noeud 1, operateur 1, hub local. C est exactement ce que
# start.sh pose deja devant ses conteneurs.
#
# OSMO_LAUNCH_MENU=1 ./launch.sh redonne les questions quand on les veut.
if [ "${OSMO_LAUNCH_MENU:-0}" = "1" ]; then
    "$TARGET" "$@"
else
    NO_MENU=1 "$TARGET" "$@"
fi
rc=$?

echo
if [ "$rc" -eq 0 ]; then
    echo -e "${GREEN}=== start-direct.sh termine (code 0) ===${NC}"
else
    echo -e "${RED}=== start-direct.sh a ECHOUE (code $rc) ===${NC}"
fi
# Uniquement dans une fenetre ouverte par nos soins : au terminal, rendre la
# main tout de suite est le comportement attendu.
if [ "$_own_window" = "1" ]; then
    echo "Fenetre maintenue ouverte - Entree pour fermer."
    read -r _ || true
fi
exit "$rc"