Skip to content

#26 — dba-daemon

PLANIFIÉ

Priorité: 🔴 CRITIQUE · Type: TYPE B+C · Conteneurs: rgz-api + rgz-beat · Code: app/services/dba.py + app/tasks/qos.py

Dépendances: #6 rgz-radius · #4 rgz-db · #10 rgz-beat


Description

Le DBA Daemon (Dynamic Bandwidth Allocation) est le moteur d'allocation dynamique de bande passante du réseau ACCESS. Il recalcule toutes les 5 minutes le MIR (Maximum Information Rate) de chaque revendeur actif en fonction du trafic réseau réel mesuré, puis applique les nouveaux paramètres de débit sur les CPE via le protocole CoA (Change of Authorization) RFC 5176.

L'objectif du DBA est double : garantir une expérience équitable entre revendeurs en période de congestion, et permettre le burst (consommation au-delà du CIR contractuel) lorsque la capacité réseau est disponible. Ce mécanisme est transparent pour l'abonné final — il bénéficie automatiquement de la bande passante disponible sans intervention manuelle.

Le DBA opère en deux couches complémentaires. La couche service (app/services/dba.py) contient la logique de calcul MIR, l'envoi CoA via FreeRADIUS, et la mise en cache Redis. La couche tâche Celery (app/tasks/qos.py) orchestre le déclenchement périodique via rgz-beat toutes les 5 minutes, avec gestion des erreurs et retry exponentiel.

L'ordre strict DB → Redis → CoA (LL#26) est imposé : les nouvelles valeurs MIR sont d'abord persistées en base, puis mise en cache Redis, et enfin envoyées au CPE. En cas d'échec CoA, le CPE conserve sa dernière valeur MIR valide — l'état DB reste la source de vérité.

Architecture Interne

rgz-beat (every 5min)


app/tasks/qos.py :: recalculate_dba()

    ├──► Lecture sessions actives : radius_sessions WHERE is_active=True
    │    GROUP BY reseller_id → {session_count, bytes_in_rate, bytes_out_rate}

    ├──► Calcul charge réseau globale (sum bytes / QOS_TOTAL_MBPS)
    │    → load_percent : 0-100%

    ├──► Pour chaque revendeur :
    │    app/services/dba.py :: compute_mir(reseller_id, load_percent)
    │    ┌─────────────────────────────────────────────────────────┐
    │    │  CIR = forfait.cir_mbps (ex: 2 Mbps contractuel)        │
    │    │  Si load < 50%  → MIR = CIR × 3  (burst max)           │
    │    │  Si load 50-80% → MIR = CIR × 1.5 (burst modéré)       │
    │    │  Si load > 80%  → MIR = CIR × 1   (congestion mode)    │
    │    └─────────────────────────────────────────────────────────┘

    ├──► LL#26 : WRITE DB FIRST
    │    UPDATE resellers SET current_mir_mbps=X, mir_updated_at=NOW()

    ├──► THEN Redis cache
    │    HSET rgz:dba:mir:{reseller_id} mir_mbps X updated_at T session_count N
    │    EXPIRE rgz:dba:mir:{reseller_id} 600

    └──► THEN CoA RFC 5176 → FreeRADIUS → CPE
         radclient -x {radius_host}:3799 coa {radius_secret}
         Bandwidth-Max-Up   = MIR × 1000000 (bits/s)
         Bandwidth-Max-Down = MIR × 1000000 (bits/s)
         NAS-Identifier     = nas_id du revendeur

Configuration

Variables d'environnement

VariableValeur exempleDescription
RADIUS_SECRETsecret_radius_rgzSecret CoA (SEC-05 : env var uniquement)
RADIUS_HOSTrgz-radiusHostname FreeRADIUS
RADIUS_COA_PORT3799Port CoA RFC 5176
DBA_INTERVAL_SEC300Intervalle recalcul (5min)
DBA_BURST_LOW3.0Multiplicateur MIR si charge <50%
DBA_BURST_MED1.5Multiplicateur MIR si charge 50-80%
DBA_CONGESTION_THRESHOLD80Seuil % charge → congestion mode
QOS_TOTAL_MBPS100Capacité WAN totale pour calcul load%

Celery Task

python
# app/tasks/qos.py
from app.celery_app import celery_app

@celery_app.task(
    name="rgz.dba.recalculate",
    queue="rgz.qos",
    autoretry_for=(Exception,),
    retry_backoff=True,
    max_retries=3
)
def recalculate_dba():
    """
    Tâche Celery Beat — every 5min — queue rgz.qos
    Recalcule le MIR de chaque revendeur actif et envoie CoA.
    """
    from app.services.dba import DBAService
    service = DBAService()
    return service.run_full_cycle()
python
# app/celery_app.py — beat schedule entry
"rgz.dba.recalculate": {
    "task": "rgz.dba.recalculate",
    "schedule": 300.0,   # 5 minutes
    "options": {"queue": "rgz.qos"},
},

Endpoints API

MéthodeEndpointAuthDescription
GET/api/v1/qos/dba/{reseller_id}Bearer JWTLire MIR actuel du revendeur
POST/api/v1/qos/dba/{reseller_id}/forceBearer JWT (admin)Forcer recalcul immédiat
GET/api/v1/qos/dbaBearer JWT (admin)Liste MIR tous revendeurs

Exemple — GET /api/v1/qos/dba/

json
// Requête
GET /api/v1/qos/dba/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJ...

// Réponse 200
{
  "reseller_id": "550e8400-e29b-41d4-a716-446655440000",
  "cir_mbps": 2,
  "mir_mbps": 6,
  "load_percent": 35.2,
  "multiplier": 3.0,
  "session_count": 4,
  "updated_at": "2026-02-21T14:30:00Z",
  "coa_sent": true,
  "coa_sent_at": "2026-02-21T14:30:01Z"
}

Exemple — POST /api/v1/qos/dba/{reseller_id}/force

json
// Requête (admin uniquement)
POST /api/v1/qos/dba/550e8400-e29b-41d4-a716-446655440000/force
Authorization: Bearer eyJ... (role=admin)

// Réponse 202
{
  "message": "DBA recalculation triggered",
  "task_id": "abc123-celery-task-id",
  "reseller_id": "550e8400-e29b-41d4-a716-446655440000"
}

Redis Keys

Clé RedisTypeTTLContenu
rgz:dba:mir:{reseller_id}Hash600smir_mbps, cir_mbps, load_percent, updated_at, session_count
rgz:dba:lock:cycleString60sMutex anti-doublon run Celery
python
# Structure Hash Redis
{
    "mir_mbps": "6",
    "cir_mbps": "2",
    "load_percent": "35.2",
    "updated_at": "2026-02-21T14:30:00Z",
    "session_count": "4",
    "coa_sent": "true"
}

Celery Tasks

TâcheQueueIntervalleDescription
rgz.dba.recalculatergz.qos5 minRecalcul MIR + envoi CoA tous revendeurs

Commandes Utiles

bash
# Vérifier le MIR actuel d'un revendeur via Redis
docker exec rgz-redis redis-cli HGETALL "rgz:dba:mir:550e8400-e29b-41d4-a716-446655440000"

# Lancer manuellement la tâche DBA (debug)
docker exec rgz-beat celery -A app.celery_app call rgz.dba.recalculate

# Voir les logs de la tâche DBA
docker logs rgz-beat --tail=50 | grep "dba"

# Vérifier qu'un CoA est bien envoyé
docker exec rgz-radius radclient -x rgz-radius:3799 coa secret \
  "NAS-Identifier=access_kossou,Bandwidth-Max-Down=6000000,Bandwidth-Max-Up=6000000"

# Inspecter la queue Celery qos
docker exec rgz-redis redis-cli LLEN "rgz.qos"

# Forcer recalcul via API (admin)
curl -X POST https://api-rgz.duckdns.org/api/v1/qos/dba/{reseller_id}/force \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# Vérifier état DB MIR
docker exec rgz-db psql -U rgz -d rgzdb -c \
  "SELECT id, slug, current_mir_mbps, mir_updated_at FROM resellers WHERE status='active';"

Sécurité

RègleApplication
SEC-05RADIUS_SECRET uniquement via variable d'environnement, jamais codé en dur dans dba.py
LL#26Ordre strict : écriture DB → mise à jour Redis → envoi CoA. Si CoA échoue : log + retry, pas de rollback DB
SEC-01Endpoint GET /qos/dba/{reseller_id} vérifie que current_user.reseller_id == reseller_id sauf si role=admin
SEC-07Redis protégé par requirepass (variable REDIS_PASSWORD), ACL limitée au service api

Gestion des Erreurs CoA

python
# app/services/dba.py — gestion échec CoA
try:
    coa_result = send_coa(nas_id, mir_mbps)
    logger.info(f"CoA sent to {nas_id}: MIR={mir_mbps}Mbps")
except CoAException as e:
    # CPE garde son ancienne valeur MIR — pas critique
    logger.warning(f"CoA failed for {nas_id}: {e}. DB value preserved.")
    # NE PAS lever d'exception — la tâche Celery ne doit pas échouer pour un CoA raté

Implémentation TODO

  • [ ] Créer app/services/dba.py avec classe DBAService : compute_mir(), send_coa(), run_full_cycle()
  • [ ] Créer app/tasks/qos.py avec tâche rgz.dba.recalculate
  • [ ] Ajouter colonnes current_mir_mbps INTEGER, mir_updated_at TIMESTAMPTZ à table resellers
  • [ ] Ajouter colonne cir_mbps INTEGER NOT NULL DEFAULT 2 à table resellers
  • [ ] Implémenter endpoint GET /api/v1/qos/dba/{reseller_id} (lecture Redis → fallback DB)
  • [ ] Implémenter endpoint POST /api/v1/qos/dba/{reseller_id}/force (admin only)
  • [ ] Configurer beat schedule dans app/celery_app.py (rgz.dba.recalculate every 300s)
  • [ ] Tests unitaires compute_mir() pour les 3 seuils de charge
  • [ ] Tests intégration CoA avec FreeRADIUS mock
  • [ ] Vérifier SEC-05 : aucun hardcode RADIUS_SECRET dans le code

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

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