Skip to content

#39 — snmp-poller

PLANIFIÉ

Priorité: 🔴 CRITIQUE · Type: TYPE C · Conteneur: rgz-beat · Code: app/tasks/monitoring.py

Dépendances: #10 rgz-beat, #04 rgz-db


Description

snmp-poller est la tâche Celery responsable de la collecte périodique des métriques réseau sur tous les CPE (Customer Premises Equipment) déployés par RGZ — principalement des LiteBeam M5 et LiteBeam AC de Ubiquiti. Toutes les 5 minutes, la tâche interroge chaque point d'accès via SNMPv3 (mode authPriv, SHA-256, AES-128) et stocke les métriques collectées dans une hypertable TimescaleDB pour analyse historique.

Le choix de SNMPv3 avec chiffrement est imposé par les recommandations de sécurité du projet (SEC-07, SEC-08) : les clés SNMP transitent sur le réseau RGZ et doivent être protégées. Chaque CPE est enregistré dans la base de données avec son IP, son NAS-ID et son VLAN, permettant au poller de construire dynamiquement la liste des cibles à interroger au démarrage de chaque cycle.

Les données collectées alimentent trois outils critiques : le dashboard RF Monitoring (#41, heatmap RSSI), les rapports SLA (#43, calcul uptime), et les dashboards Grafana (#37). Elles permettent également de détecter les CPE dégradés (RSSI < -80dBm) et de déclencher des alertes Prometheus avant qu'un site soit complètement hors service.

L'outil utilise la librairie Python pysnmp ou easysnmp pour les requêtes SNMP. Les OIDs LiteBeam sont issus de la MIB propriétaire Ubiquiti (UBNT-MIB) disponible sur le site Ubiquiti, complétée par les OIDs standard IF-MIB pour les compteurs d'octets.

Architecture Interne

Celery Beat (rgz.snmp.poll, every 5min)


app/tasks/monitoring.py :: snmp_poll_all_cpe()

        ├── 1. Charger liste CPE actifs depuis DB
        │      SELECT ap_ip, nas_id, reseller_id FROM reseller_sites WHERE active=true

        ├── 2. Pour chaque CPE (en parallèle, asyncio):
        │      SNMPv3 GET(OIDs LiteBeam)
        │      OIDs cibles:
        │        ubntAirMaxRssi      → RSSI (dBm)
        │        ubntAirMaxSnr       → SNR (dB)
        │        ubntAirMaxCcq       → CCQ (%)
        │        ubntAirMaxTxRate    → TX Rate (Kbps)
        │        ubntAirMaxRxRate    → RX Rate (Kbps)
        │        hrProcessorLoad     → CPU Load (%)
        │        sysUpTime           → Uptime (TimeTicks)
        │        ifInOctets          → Bytes In
        │        ifOutOctets         → Bytes Out

        ├── 3. Normaliser et valider les valeurs
        │      (RSSI doit être négatif, CCQ 0-100, etc.)

        └── 4. Bulk INSERT dans TimescaleDB
               hypertable: snmp_metrics
               partition: par jour sur champ timestamp

Modèle de données

sql
-- Hypertable TimescaleDB (créée par #4 rgz-db)
CREATE TABLE snmp_metrics (
    id            UUID DEFAULT gen_random_uuid(),
    cpe_id        UUID NOT NULL REFERENCES reseller_sites(id),
    nas_id        TEXT NOT NULL,
    metric_name   TEXT NOT NULL CHECK (metric_name IN (
                    'rssi_dbm', 'snr_db', 'ccq_percent',
                    'tx_rate_kbps', 'rx_rate_kbps',
                    'cpu_load_percent', 'uptime_seconds',
                    'bytes_in', 'bytes_out'
                  )),
    value         DOUBLE PRECISION NOT NULL,
    timestamp     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY   (id, timestamp)
);

SELECT create_hypertable('snmp_metrics', 'timestamp', chunk_time_interval => INTERVAL '1 day');
CREATE INDEX ON snmp_metrics (nas_id, metric_name, timestamp DESC);

Celery Tasks

TaskScheduleQueueDescription
rgz.snmp.pollevery 5minrgz.monitoringPolling SNMPv3 tous CPE actifs
python
# app/tasks/monitoring.py — skeleton
from app.celery_app import celery_app
from pysnmp.hlapi import *

@celery_app.task(name="rgz.snmp.poll", queue="rgz.monitoring")
def snmp_poll_all_cpe():
    """Polling SNMPv3 de tous les CPE actifs toutes les 5 minutes."""
    # 1. Récupérer liste CPE depuis DB
    # 2. Pool asyncio pour requêtes parallèles
    # 3. Bulk insert dans TimescaleDB
    pass

def snmp_get_cpe_metrics(cpe_ip: str, nas_id: str) -> dict:
    """Interroge un CPE LiteBeam via SNMPv3."""
    oids = [
        ObjectType(ObjectIdentity('UBNT-MIB', 'ubntAirMaxRssi', 0)),
        ObjectType(ObjectIdentity('UBNT-MIB', 'ubntAirMaxSnr', 0)),
        ObjectType(ObjectIdentity('UBNT-MIB', 'ubntAirMaxCcq', 0)),
        ObjectType(ObjectIdentity('IF-MIB', 'ifInOctets', 1)),
        ObjectType(ObjectIdentity('IF-MIB', 'ifOutOctets', 1)),
    ]
    # SNMPv3 authPriv SHA-256 AES-128
    # ...
    pass

Configuration

Variables d'environnement

VariableExempleDescription
SNMP_AUTH_KEYsnmp-auth-key-sha256Clé d'authentification SNMPv3 (SHA-256)
SNMP_PRIV_KEYsnmp-priv-key-aes128Clé de chiffrement SNMPv3 (AES-128)
SNMP_USERNAMErgzmonitorNom d'utilisateur SNMPv3
SNMP_TIMEOUT5Timeout par requête SNMP (secondes)
SNMP_RETRIES2Nombre de retries SNMP
SNMP_POLL_CONCURRENCY10CPE interrogés en parallèle max

Endpoints API

MéthodeRouteCode HTTPDescription
GET/api/v1/monitoring/cpe200Liste CPE avec dernier RSSI
GET/api/v1/monitoring/cpe/{cpe_id}/metrics200Métriques historiques d'un CPE
GET/api/v1/monitoring/cpe/{cpe_id}/metrics/latest200Dernière mesure uniquement

Exemple de réponse — liste CPE

json
GET /api/v1/monitoring/cpe

{
  "items": [
    {
      "cpe_id": "550e8400-e29b-41d4-a716-446655440000",
      "nas_id": "access_kossou",
      "ap_ip": "10.142.0.2",
      "city": "Cotonou",
      "last_rssi_dbm": -68.5,
      "last_snr_db": 22,
      "last_ccq_percent": 94,
      "last_poll": "2026-02-21T10:30:00Z",
      "status": "ok"
    }
  ],
  "total": 47,
  "page": 1,
  "pages": 1
}

Exemple de réponse — historique métriques

json
GET /api/v1/monitoring/cpe/{cpe_id}/metrics?from=2026-02-20T00:00:00Z&to=2026-02-21T00:00:00Z&metric=rssi_dbm

{
  "items": [
    {"timestamp": "2026-02-20T00:00:00Z", "metric_name": "rssi_dbm", "value": -67.0},
    {"timestamp": "2026-02-20T00:05:00Z", "metric_name": "rssi_dbm", "value": -68.5}
  ],
  "total": 288,
  "page": 1,
  "pages": 6
}

Redis Keys

CléTypeTTLUsage
rgz:metrics:realtime:{nas_id}Hash300sDernière mesure SNMP du CPE
python
# Cache Redis pour les métriques temps réel (TTL = durée entre 2 polls)
redis_client.hset(
    f"rgz:metrics:realtime:{nas_id}",
    mapping={
        "rssi_dbm": str(rssi),
        "snr_db": str(snr),
        "ccq_percent": str(ccq),
        "last_poll": datetime.utcnow().isoformat(),
    }
)
redis_client.expire(f"rgz:metrics:realtime:{nas_id}", 300)

Commandes Utiles

bash
# Vérifier que la tâche SNMP est schedulée dans Celery Beat
docker exec rgz-beat celery -A app.celery_app inspect scheduled | grep snmp

# Déclencher manuellement un poll SNMP
docker exec rgz-beat celery -A app.celery_app call rgz.snmp.poll

# Tester SNMP sur un CPE manuellement (depuis l'hôte)
snmpwalk -v3 -l authPriv -u rgzmonitor \
  -a SHA -A "$SNMP_AUTH_KEY" \
  -x AES -X "$SNMP_PRIV_KEY" \
  10.142.0.2 1.3.6.1.4.1.41112

# Vérifier les métriques dans TimescaleDB
docker exec rgz-db psql -U rgz -c "
  SELECT nas_id, metric_name, value, timestamp
  FROM snmp_metrics
  WHERE timestamp > NOW() - INTERVAL '5 minutes'
  ORDER BY timestamp DESC
  LIMIT 20;
"

# Logs Celery Beat (pour voir le snmp poll)
docker logs rgz-beat -f --tail=100 | grep snmp

# Vérifier Redis cache métriques
docker exec rgz-redis redis-cli HGETALL "rgz:metrics:realtime:access_kossou"

Sécurité

RègleImplémentation
SEC-07Clés SNMPv3 (SNMP_AUTH_KEY, SNMP_PRIV_KEY) via env vars uniquement, jamais en dur
SEC-08Connexion TimescaleDB via rôle rw (pas superuser)
LL#42Healthcheck Celery beat via /proc : grep -q celery /proc/1/cmdline
LL#27Logs Docker max-size: 10m max-file: 3
LL#33restart: unless-stopped
SNMPv3authPriv obligatoire (SHA-256 auth + AES-128 chiffrement) — jamais v1/v2c

Implémentation TODO

  • [ ] app/tasks/monitoring.py — tâche snmp_poll_all_cpe() complète
  • [ ] Fonction snmp_get_cpe_metrics() avec pysnmp authPriv SHA-256/AES-128
  • [ ] Gestion des timeouts et erreurs SNMP (CPE injoignable → log warning, pas exception)
  • [ ] Bulk INSERT TimescaleDB (eviter N insertions individuelles)
  • [ ] Cache Redis rgz:metrics:realtime:{nas_id} TTL=300s
  • [ ] Endpoints GET /api/v1/monitoring/cpe et .../metrics
  • [ ] Chargement MIB UBNT (UBNT-MIB) dans l'image Docker
  • [ ] Index TimescaleDB optimisé pour requêtes Grafana
  • [ ] Tests unitaires avec CPE simulé (mock SNMP)
  • [ ] Alerte Prometheus si CPE inaccessible après 3 polls consécutifs

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone