#31 — Allocation VLAN
PLANIFIÉ
Priorité: 🔴 CRITIQUE · Type: TYPE B · Conteneur: rgz-api · Code: app/services/vlan_manager.pyDépendances: #1 rgz-api, #30 kea-dhcp
Description
Allocation automatique de VLAN lors de l'onboarding revendeur. Chaque revendeur V1 reçoit un VLAN unique entre 100 et 499. Le calcul du subnet est automatique : 10.[vlan].0.0/24 avec gateway 10.[vlan].0.1. Pour les revendeurs multi-sites (V2/V3), un seul VLAN est partagé entre tous les sites du même revendeur. Cette approche simplifie la gestion de la QoS et du DBA qui opèrent par VLAN.
Le NAS-ID (identifiant réseau RADIUS) suit la convention : access_[slug] en mono-site, access_[slug]_s[N] en multi-site. Le slug est le nom du revendeur en minuscules, sans accents, max 20 caractères. Cette allocation est stockée dans la table reseller_sites et synchronisée avec Kea DHCP pour la génération dynamique des pools d'adressage.
Le service expose des endpoints pour lire les allocations existantes, simuler une allocation avant confirmation, et supporter la migration de VLAN en cas de besoin. Les validations incluent : vérification que le VLAN demandé est libre, calcul du prochain VLAN disponible si auto-allocation, vérification du format du slug, et génération automatique du subnet.
Architecture Interne
1. Inscription revendeur via #56 onboarding
└─> POST /api/v1/network/vlans/allocate
└─> VlanManager.allocate_vlan(reseller_id, classif, slug, sites=[])
├─> Requête DB: SELECT COUNT(*) FROM reseller_sites WHERE reseller_id = ?
├─> Calcul: vlan_id = 100 + (hash(reseller_id) % 400) [recherche du prochain libre]
├─> INSERT reseller_sites(reseller_id, site_number, nas_id, ssid, vlan_id, subnet, gateway)
├─> Redis: SET rgz:vlan:{reseller_id} {vlan_id, subnet, gateway} TTL=3600s
└─> Webhook → #30 Kea DHCP pour créer les pools DHCP
2. Multi-site (V2/V3):
└─> Même VLAN, NAS-ID différents par site
├─> Site 1: access_slug_s1 → VLAN 150, Subnet 10.150.0.0/24
├─> Site 2: access_slug_s2 → VLAN 150 (partagé), Subnet 10.150.0.0/24
├─> IPs: 10.150.0.2 (AP Site 1), 10.150.0.3 (AP Site 2), etc.
└─> Broadcast domain unique pour IPv4 (VLAN trunk vers tous les sites)
3. DBA recalcul (every 5min via #26):
└─> RADIUS → rgz-api → SELECT bytes FROM radius_sessions WHERE nas_id = ?
└─> Applique MIR par VLAN via CoA (Change-of-Authorization)
└─> Impacts: #27 HTB QoS, #28 DSCP marking
4. Validation Subnet:
└─> Vérification CIDR 10.[100-499].0.0/24
└─> Gateway: n.0.1
└─> Range DHCP: n.0.10 - n.0.254Configuration
Variables d'environnement
VLAN_MIN=100 # VLAN minimum alloué
VLAN_MAX=499 # VLAN maximum alloué
SUBNET_PREFIX_IPV4=10 # Préfixe réseau (toujours 10.vlan.0.0/24)
KEAVERN_API=http://rgz-kea:8000 # API Kea pour synchro pools DHCP
KEAVERN_SECRET=kea_sync_secret # Secret HMAC pour webhook Kea
VLAN_AUTO_ALLOCATION=true # Activation allocation auto
VLAN_SLUG_MAX_LENGTH=20 # Longueur max du slugModel SQLAlchemy
# app/models/reseller.py
class ResellerSites(Base):
__tablename__ = "reseller_sites"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
reseller_id = Column(UUID(as_uuid=True), ForeignKey("resellers.id"), nullable=False)
site_number = Column(Integer, nullable=False) # 1-indexed, V2/V3 uniquement
nas_id = Column(String(50), unique=True, nullable=False) # access_slug[_sN]
ssid = Column(String(32), nullable=False) # "ACCESS [ResellerName] [Site]" ou "ACCESS [Name]"
vlan_id = Column(Integer, nullable=False,
CheckConstraint("vlan_id BETWEEN 100 AND 499"))
subnet_cidr = Column(String(18), nullable=False) # "10.150.0.0/24"
gateway_ip = Column(String(15), nullable=False) # "10.150.0.1"
latitude = Column(Float) # Pour #68 site survey
longitude = Column(Float)
city = Column(String(50))
quarter = Column(String(50))
ap_ip = Column(String(15), nullable=True) # IP d'accès SSH au CPE
created_at = Column(DateTime, default=utcnow, nullable=False)
updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)
__table_args__ = (
UniqueConstraint("reseller_id", "site_number", name="uq_reseller_site_number"),
UniqueConstraint("nas_id", name="uq_nas_id"),
)Endpoints API
| Méthode | Route | Réponse | Auth |
|---|---|---|---|
| POST | /api/v1/network/vlans/allocate | 201 {vlan_id, subnet_cidr, gateway_ip, nas_id} | Revendeur/Admin |
| GET | /api/v1/network/vlans/{reseller_id} | 200 {items: [{vlan_id, nas_id, site_number, ...}], total} | Revendeur/Admin |
| GET | /api/v1/network/vlans | 200 {items: [...], total, page} | Admin |
| GET | /api/v1/network/vlans/simulate?slug=X&classif=V1&sites=1 | 200 {vlan_id, subnet_cidr} | Admin |
| DELETE | /api/v1/network/vlans/{reseller_id} | 204 | Admin |
Schémas Pydantic
# app/schemas/network.py
class VlanAllocateRequest(BaseModel):
reseller_id: UUID
classif: Literal["V1", "V2", "V3"] # Classification revendeur
slug: str # Slug revendeur: min 3, max 20, ^[a-z0-9_]+$
sites: List[SiteInput] = [] # V2/V3 uniquement
class SiteInput(BaseModel):
site_number: int # 1-indexed
ssid: str # "ACCESS [ResellerName] [SiteName]"
latitude: Optional[float] = None
longitude: Optional[float] = None
city: Optional[str] = None
ap_ip: Optional[str] = None # Pour SSH CPE
class VlanAllocateResponse(BaseModel):
vlan_id: int
subnet_cidr: str # "10.150.0.0/24"
gateway_ip: str
nas_id: str
sites: List[SiteAllocation] # Multi-site
class SiteAllocation(BaseModel):
site_number: int
nas_id: str
ap_ip: Optional[str]
ssid: strCommandes Utiles
# Tester allocation VLAN
curl -X POST http://localhost:8000/api/v1/network/vlans/allocate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"reseller_id": "550e8400-e29b-41d4-a716-446655440000",
"classif": "V1",
"slug": "test_reseller",
"sites": []
}'
# Vérifier allocation existante
curl -X GET http://localhost:8000/api/v1/network/vlans/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $TOKEN"
# Simuler allocation V2 (3 sites)
curl -X GET 'http://localhost:8000/api/v1/network/vlans/simulate?slug=tech_connect&classif=V2&sites=3' \
-H "Authorization: Bearer $TOKEN"
# Requête DB pour voir allocations
psql -h rgz-db -U rgz_admin -d rgz_db -c \
"SELECT reseller_id, vlan_id, nas_id, site_number FROM reseller_sites ORDER BY vlan_id;"Implémentation TODO
- [ ] Implémenter
VlanManager.allocate_vlan()avec allocation auto (hash modulo) - [ ] Implémenter
VlanManager.get_next_free_vlan()(parcours 100-499) - [ ] Validation slug: regex
^[a-z0-9_]{3,20}$(pas d'accents) - [ ] Endpoint POST allocate avec transactionnel DB + Redis cache
- [ ] Endpoint GET {reseller_id} pour afficher VLAN + sites
- [ ] Endpoint GET (admin) pour afficher tous les VLAN avec taux d'utilisation
- [ ] Simulation d'allocation avant confirmation
- [ ] Webhook vers Kea DHCP pour sync pools après insertion DB
- [ ] Validation CIDR: toujours 10.[100-499].0.0/24
- [ ] Suppression en cascade si revendeur supprimé
- [ ] Tests: allocation auto, validation slug, multi-site, webhook Kea
- [ ] Intégration avec #56 onboarding (appel allocate_vlan à l'étape 2)
Dernière mise à jour: 2026-02-21