Files
projet-vxlan-automation/utilities/Create_Fabric/fabric_creator.py
D. Arnodo c8daee6c11 Feat/leaf template (#7)
* fix(template) : Change `Ethernet3`configuration
* fix(Template): Missing Underlay configuration
* Fix(template) : error 'ipam.models.ip.IPAddress DoesNotExist
* fix(routing) : ! IP routing not enabled
* fix(leaves template): adding ip routing and multi-agent model
2025-03-28 16:52:37 +01:00

414 lines
16 KiB
Python

"""
Classe principale pour la création de fabric VXLAN dans NetBox
"""
import logging
from typing import Dict, List, Tuple, Optional
from config import FabricConfig, DeviceRoles, IPRoles, InterfaceTypes
from exceptions import DeviceCreationError, IPAllocationError, CablingError, FabricError
from helpers.netbox_backend import NetBoxBackend
logger = logging.getLogger(__name__)
class VXLANFabricCreator:
"""Gère la création d'une fabric VXLAN complète"""
def __init__(self, netbox: NetBoxBackend):
"""
Initialise le créateur de fabric
Args:
netbox: Instance de NetBoxBackend
"""
self.nb = netbox
self.config: Optional[FabricConfig] = None
self._roles: Dict = {}
self._ip_roles: Dict = {}
def validate_prerequisites(self) -> bool:
"""
Valide tous les prérequis nécessaires
Returns:
bool: True si tous les prérequis sont validés
Raises:
DeviceCreationError: Si un rôle d'équipement est manquant
IPAllocationError: Si un rôle IP est manquant
"""
try:
# Validation des rôles d'équipements
for role in DeviceRoles:
role_obj = self.nb.get_device_role(role.value)
if not role_obj:
raise DeviceCreationError(f"Role {role.value} not found")
self._roles[role.value] = role_obj
# Validation des rôles IP
for role in IPRoles:
ip_role = self.nb.nb.ipam.roles.get(slug=role.value)
if not ip_role:
raise IPAllocationError(f"IP role {role.value} not found")
self._ip_roles[role.value] = ip_role
# Validation des types d'équipements
device_types = [
self.config.spine_type,
self.config.leaf_type,
self.config.access_type
]
for dev_type in device_types:
if not self.nb.get_device_type_by_slug(dev_type):
raise DeviceCreationError(f"Device type {dev_type} not found")
return True
except Exception as e:
logger.error(f"Validation failed: {str(e)}")
return False
def get_or_create_site(self) -> dict:
"""
Sélectionne un site existant ou en crée un nouveau
Returns:
dict: Objet site
Raises:
DeviceCreationError: Si la création/sélection échoue
"""
existing_sites = self.nb.get_sites()
if not existing_sites:
raise DeviceCreationError("No sites found in NetBox")
print("\nExisting Sites:")
for idx, site in enumerate(existing_sites, start=1):
print(f" {idx}. {site.name} (slug={site.slug})")
choice = input("Choose site number or 'new': ").strip().lower()
if choice == "new":
name = input("Site name: ").strip()
code = input("Site code: ").strip().upper()
if not all([name, code]):
raise DeviceCreationError("Site name and code required")
return self.nb.create_site(name, code.lower())
try:
return existing_sites[int(choice) - 1]
except (ValueError, IndexError):
raise DeviceCreationError("Invalid site selection")
def create_spines(self, site) -> List[dict]:
"""
Crée les switches spine
Args:
site: Objet site
Returns:
List[dict]: Liste des spines créés
Raises:
DeviceCreationError: Si la création échoue
"""
spines = []
for i in range(1, 3): # Toujours 2 spines
name = f"{self.config.site_code.lower()}dc_sp{i}_00"
spine = self.nb.create_device(
name=name,
device_type_slug=self.config.spine_type,
role_id=self._roles[DeviceRoles.SPINE.value].id,
site_id=site.id
)
if not spine:
raise DeviceCreationError(f"Failed to create spine {name}")
spines.append(spine)
logger.info(f"Created spine: {spine.name}")
return spines
def create_building_pair(self, site, building_num) -> Tuple[dict, dict]:
"""
Crée une paire leaf/access pour un bâtiment
Args:
site: Objet site
building_num: Numéro du bâtiment
Returns:
Tuple[dict, dict]: Paire (leaf, access)
Raises:
DeviceCreationError: Si la création échoue
"""
try:
# Création du location
location_name = f"{self.config.site_code}{building_num}"
location = self.nb.nb.dcim.locations.create(
name=location_name,
slug=location_name.lower(),
site=site.id
)
# Création Leaf
leaf_name = f"{self.config.site_code.lower()}{str(building_num).zfill(2)}_lf1_00"
leaf = self.nb.create_device(
name=leaf_name,
device_type_slug=self.config.leaf_type,
role_id=self._roles[DeviceRoles.LEAF.value].id,
site_id=site.id,
location_id=location.id
)
if not leaf:
raise DeviceCreationError(f"Failed to create leaf {leaf_name}")
# Création Access
access_name = f"{self.config.site_code.lower()}{str(building_num).zfill(2)}_sw1_00"
access = self.nb.create_device(
name=access_name,
device_type_slug=self.config.access_type,
role_id=self._roles[DeviceRoles.ACCESS.value].id,
site_id=site.id,
location_id=location.id
)
if not access:
raise DeviceCreationError(f"Failed to create access switch {access_name}")
logger.info(f"Created leaf/access pair: {leaf_name} / {access_name}")
return leaf, access
except Exception as e:
raise DeviceCreationError(f"Failed to create building pair: {str(e)}")
def setup_cabling(self, leaf, spines, access) -> Tuple[dict, dict, dict, dict]:
"""
Configure le câblage pour un ensemble leaf/spines/access
Topologie:
- Spine1.EthX -> LeafY.Eth1 (où X = numéro du leaf)
- Spine2.EthX -> LeafY.Eth2 (où X = numéro du leaf)
- LeafY.Eth3 -> AccessY.Eth1
Args:
leaf: Objet leaf
spines: Liste des spines
access: Objet access switch
Returns:
Tuple[dict, dict, dict, dict]: Interfaces connectées (leaf_if1, leaf_if2, spine1_if, spine2_if)
Raises:
CablingError: Si le câblage échoue
"""
try:
# Extraire le numéro du leaf (ex: pa01_lf1_00 -> 1)
leaf_number = int(leaf.name.split('_')[0][-2:])
# Leaf -> Spine1 (Leaf.Eth1 -> Spine1.EthX)
leaf_if1 = self.nb.get_or_create_interface(
leaf.id, "Ethernet1", InterfaceTypes.QSFPP.value
)
spine1_if = self.nb.get_or_create_interface(
spines[0].id, f"Ethernet{leaf_number}", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if1, spine1_if):
raise CablingError(f"Failed to cable {leaf_if1.name} to {spine1_if.name}")
logger.info(f"Connected {leaf.name}.Eth1 to {spines[0].name}.Eth{leaf_number}")
# Leaf -> Spine2 (Leaf.Eth2 -> Spine2.EthX)
leaf_if2 = self.nb.get_or_create_interface(
leaf.id, "Ethernet2", InterfaceTypes.QSFPP.value
)
spine2_if = self.nb.get_or_create_interface(
spines[1].id, f"Ethernet{leaf_number}", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if2, spine2_if):
raise CablingError(f"Failed to cable {leaf_if2.name} to {spine2_if.name}")
logger.info(f"Connected {leaf.name}.Eth2 to {spines[1].name}.Eth{leaf_number}")
# Leaf -> Access (Leaf.Eth3 -> Access.Eth1)
leaf_if3 = self.nb.get_or_create_interface(
leaf.id, "Ethernet3", InterfaceTypes.QSFPP.value
)
access_if = self.nb.get_or_create_interface(
access.id, "Ethernet1", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if3, access_if):
raise CablingError(f"Failed to cable {leaf_if3.name} to {access_if.name}")
logger.info(f"Connected {leaf.name}.Eth3 to {access.name}.Eth1")
return leaf_if1, leaf_if2, spine1_if, spine2_if
except ValueError as e:
raise CablingError(f"Invalid device naming format: {str(e)}")
except Exception as e:
raise CablingError(f"Cabling failed: {str(e)}")
def setup_ip_addressing(self, site, interfaces, devices) -> None:
"""
Configure l'adressage IP pour les interfaces et les loopbacks
Args:
site: Objet site
interfaces: Liste de tuples d'interfaces à connecter
devices: Liste des équipements nécessitant un loopback
Raises:
IPAllocationError: Si l'allocation IP échoue
"""
try:
# Récupération des prefixes parents
underlay_pfxs = self.nb.nb.ipam.prefixes.filter(
role_id=self._ip_roles[IPRoles.UNDERLAY.value].id,
scope_id=site.id
)
loopback_pfxs = self.nb.nb.ipam.prefixes.filter(
role_id=self._ip_roles[IPRoles.LOOPBACK.value].id,
scope_id=site.id
)
if not underlay_pfxs or not loopback_pfxs:
raise IPAllocationError("Missing required prefix pools")
parent_prefix = list(underlay_pfxs)[0]
loopback_prefix = list(loopback_pfxs)[0]
# Attribution des /31 pour les liens
for if_a, if_b in interfaces:
child_prefix = self.nb.allocate_prefix(
parent_prefix, 31, site.id,
self._ip_roles[IPRoles.UNDERLAY.value].id,
self.config.tenant_id
)
if not child_prefix:
raise IPAllocationError(f"Failed to allocate /31 for {if_a.name}-{if_b.name}")
ips = self.nb.get_available_ips_in_prefix(child_prefix)
if len(ips) < 2:
raise IPAllocationError(f"Not enough IPs in /31 for {if_a.name}-{if_b.name}")
self.nb.assign_ip_to_interface(if_a, ips[0].address)
self.nb.assign_ip_to_interface(if_b, ips[1].address)
logger.info(f"Assigned IPs to {if_a.name}-{if_b.name}")
# Attribution des loopbacks
for device in devices:
# Création de l'interface loopback
loopback = self.nb.get_or_create_interface(
device.id,
"Loopback0",
InterfaceTypes.VIRTUAL.value
)
if not loopback:
raise IPAllocationError(f"Failed to create Loopback0 for {device.name}")
# Allocation du /32
loopback_32 = self.nb.allocate_prefix(
loopback_prefix, 32, site.id,
self._ip_roles[IPRoles.LOOPBACK.value].id,
self.config.tenant_id
)
if not loopback_32:
raise IPAllocationError(f"Failed to allocate /32 for {device.name}")
ips = self.nb.get_available_ips_in_prefix(loopback_32)
if not ips:
raise IPAllocationError(f"No IPs available in /32 for {device.name}")
ip = self.nb.assign_ip_to_interface(loopback, ips[0].address)
if ip:
logger.info(f"Assigned {ip.address} to {device.name} Loopback0")
else:
raise IPAllocationError(f"Failed to assign IP to {device.name} Loopback0")
except Exception as e:
raise IPAllocationError(f"IP addressing failed: {str(e)}")
def assign_asns(self, spines: List[dict], leaves: List[dict]) -> None:
"""
Attribue les ASN aux équipements
Args:
spines: Liste des spines
leaves: Liste des leaves
Raises:
DeviceCreationError: Si l'attribution des ASN échoue
"""
try:
# ASNs pour les spines
for i, spine in enumerate(spines):
asn = self.config.base_spine_asn + i
if not self.nb.save_custom_fields(spine, {"ASN": asn}):
raise DeviceCreationError(f"Failed to assign ASN {asn} to {spine.name}")
logger.info(f"Assigned ASN {asn} to {spine.name}")
# ASNs pour les leaves
for i, leaf in enumerate(leaves):
asn = self.config.base_leaf_asn + i
if not self.nb.save_custom_fields(leaf, {"ASN": asn}):
raise DeviceCreationError(f"Failed to assign ASN {asn} to {leaf.name}")
logger.info(f"Assigned ASN {asn} to {leaf.name}")
except Exception as e:
raise DeviceCreationError(f"ASN assignment failed: {str(e)}")
def create_fabric(self) -> Dict:
"""
Processus principal de création de la fabric
Returns:
Dict: Résultat de la création
Raises:
FabricError: Si la création échoue
"""
try:
if not self.validate_prerequisites():
raise FabricError("Prerequisites validation failed")
# 1. Création/sélection du site
site = self.get_or_create_site()
self.config.site_code = site.name[:2].upper()
# 2. Création des équipements
spines = self.create_spines(site)
leaves = []
access_switches = []
interface_pairs = []
# 3. Création des paires leaf/access par bâtiment
for building in range(1, self.config.num_buildings + 1):
leaf, access = self.create_building_pair(site, building)
leaves.append(leaf)
access_switches.append(access)
# 4. Câblage
interfaces = self.setup_cabling(leaf, spines, access)
interface_pairs.extend([
(interfaces[0], interfaces[2]), # Leaf-Spine1
(interfaces[1], interfaces[3]) # Leaf-Spine2
])
# 5. Configuration IP
self.setup_ip_addressing(
site=site,
interfaces=interface_pairs,
devices=spines + leaves # Loopbacks uniquement pour spines et leaves
)
# 6. Attribution des ASN
self.assign_asns(spines, leaves)
logger.info("Fabric creation completed successfully")
return {
'site': site,
'spines': spines,
'leaves': leaves,
'access_switches': access_switches
}
except Exception as e:
logger.error(f"Fabric creation failed: {str(e)}")
raise FabricError(f"Fabric creation failed: {str(e)}")