#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 timestampModèle de données
-- 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
| Task | Schedule | Queue | Description |
|---|---|---|---|
rgz.snmp.poll | every 5min | rgz.monitoring | Polling SNMPv3 tous CPE actifs |
# 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
# ...
passConfiguration
Variables d'environnement
| Variable | Exemple | Description |
|---|---|---|
SNMP_AUTH_KEY | snmp-auth-key-sha256 | Clé d'authentification SNMPv3 (SHA-256) |
SNMP_PRIV_KEY | snmp-priv-key-aes128 | Clé de chiffrement SNMPv3 (AES-128) |
SNMP_USERNAME | rgzmonitor | Nom d'utilisateur SNMPv3 |
SNMP_TIMEOUT | 5 | Timeout par requête SNMP (secondes) |
SNMP_RETRIES | 2 | Nombre de retries SNMP |
SNMP_POLL_CONCURRENCY | 10 | CPE interrogés en parallèle max |
Endpoints API
| Méthode | Route | Code HTTP | Description |
|---|---|---|---|
GET | /api/v1/monitoring/cpe | 200 | Liste CPE avec dernier RSSI |
GET | /api/v1/monitoring/cpe/{cpe_id}/metrics | 200 | Métriques historiques d'un CPE |
GET | /api/v1/monitoring/cpe/{cpe_id}/metrics/latest | 200 | Dernière mesure uniquement |
Exemple de réponse — liste CPE
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
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é | Type | TTL | Usage |
|---|---|---|---|
rgz:metrics:realtime:{nas_id} | Hash | 300s | Dernière mesure SNMP du CPE |
# 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
# 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ègle | Implémentation |
|---|---|
| SEC-07 | Clés SNMPv3 (SNMP_AUTH_KEY, SNMP_PRIV_KEY) via env vars uniquement, jamais en dur |
| SEC-08 | Connexion TimescaleDB via rôle rw (pas superuser) |
| LL#42 | Healthcheck Celery beat via /proc : grep -q celery /proc/1/cmdline |
| LL#27 | Logs Docker max-size: 10m max-file: 3 |
| LL#33 | restart: unless-stopped |
| SNMPv3 | authPriv obligatoire (SHA-256 auth + AES-128 chiffrement) — jamais v1/v2c |
Implémentation TODO
- [ ]
app/tasks/monitoring.py— tâchesnmp_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/cpeet.../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