6 - Shell
addition.sh
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 0checks/annuaire.sh
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 0checks/check_all.sh
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
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 0checks/diag-stp-operator.sh
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 0checks/global_check.sh
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
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
fichecks/operator_summary.sh
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
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
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
fichecks/wan_ss7_check.sh
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 1full_qmd.sh
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
fifull_text.sh
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
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_loadhelpers/create_interop.sh
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
fihelpers/prepare_host.sh
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
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
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
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
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
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
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
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
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
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
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
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 0launch/osmo-launch.sh
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
;;
esaclaunch.sh
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"