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
This commit is contained in:
414
utilities/Create_Fabric/fabric_creator.py
Normal file
414
utilities/Create_Fabric/fabric_creator.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
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)}")
|
||||
Reference in New Issue
Block a user