""" Datacenter Generator for Infrahub ================================== This generator creates a complete datacenter fabric topology including: - Spine switches (Layer 3 core) - Leaf switches (Aggregation with VXLAN) - Border leaf switches (DCI gateway capable) - Access switches (Rack ToR) - MLAG domains for leaf and border pairs - IP prefixes and addresses - BGP configuration - Interfaces with proper connectivity Architecture: - Spine-Leaf topology with MLAG leaf pairs - eBGP underlay (spine ASN vs leaf pair ASNs) - VXLAN/EVPN overlay on leafs - Optional DCI connectivity via border leafs (eth12 shutdown by default) """ import math from typing import Any, Dict, List from infrahub_sdk.generator import InfrahubGenerator class DatacenterGenerator(InfrahubGenerator): """ Generates complete datacenter fabric topology from InfraDatacenter object. """ async def generate(self, data: dict) -> None: """ Main generator entry point. Args: data: GraphQL query response with datacenter details """ # Get datacenter object from query response dc = self.nodes[0] self.log.info(f"🚀 Starting datacenter generation for: {dc.name.value}") self.log.info(f" DC ID: {dc.dc_id.value}") self.log.info(f" Number of bays: {dc.number_of_bays.value}") self.log.info(f" Spine count: {dc.spine_count.value}") # Step 1: Calculate derived values self.log.info("📊 Calculating topology parameters...") topology = self._calculate_topology(dc) # Step 2: Create IP prefixes self.log.info("🌐 Creating IP prefixes...") await self._create_ip_prefixes(dc, topology) # Step 3: Create spine switches self.log.info("🔴 Creating spine switches...") spines = await self._create_spines(dc, topology) # Step 4: Create leaf switches and MLAG pairs self.log.info("🔵 Creating leaf switches and MLAG pairs...") leaf_pairs = await self._create_leaf_pairs(dc, topology) # Step 5: Create border leaf switches (if enabled) border_pair = None if dc.has_border_leafs.value: self.log.info("🟣 Creating border leaf switches...") border_pair = await self._create_border_pair(dc, topology) # Step 6: Create access switches self.log.info("🟡 Creating access switches...") access_switches = await self._create_access_switches(dc, topology, leaf_pairs) # Step 7: Create spine-to-leaf interfaces and BGP sessions self.log.info("🔗 Creating spine-to-leaf connectivity...") await self._create_spine_leaf_connectivity( dc, spines, leaf_pairs, border_pair, topology ) # Step 8: Create leaf-to-access connectivity self.log.info("🔗 Creating leaf-to-access connectivity...") await self._create_leaf_access_connectivity( dc, leaf_pairs, access_switches, topology ) # Step 9: Update datacenter computed attributes self.log.info("✍️ Updating datacenter computed attributes...") await self._update_datacenter_computed_fields(dc, topology) # Summary total_devices = ( len(spines) + sum(len(pair["devices"]) for pair in leaf_pairs) + len(access_switches) ) if border_pair: total_devices += len(border_pair["devices"]) self.log.info("=" * 60) self.log.info(f"✅ Datacenter '{dc.name.value}' generation complete!") self.log.info(f" Total devices: {total_devices}") self.log.info(f" - Spines: {len(spines)}") self.log.info( f" - Leaf pairs: {len(leaf_pairs)} ({sum(len(pair['devices']) for pair in leaf_pairs)} devices)" ) if border_pair: self.log.info(f" - Border leafs: {len(border_pair['devices'])}") self.log.info(f" - Access switches: {len(access_switches)}") self.log.info("=" * 60) def _calculate_topology(self, dc) -> Dict[str, Any]: """ Calculate topology parameters based on datacenter configuration. Returns: Dictionary with calculated values: - leaf_pair_count: Number of MLAG leaf pairs needed - total_leaf_count: Total number of leaf switches - total_access_count: Total number of access switches - spine_asn: ASN for spine switches - base_leaf_asn: Starting ASN for leaf pairs """ dc_id = dc.dc_id.value number_of_bays = dc.number_of_bays.value bgp_base_asn = dc.bgp_base_asn.value # Calculate leaf pairs: ceil(bays / 2) # Each pair serves 2 bays leaf_pair_count = math.ceil(number_of_bays / 2) total_leaf_count = leaf_pair_count * 2 total_access_count = number_of_bays # Calculate ASNs # Spine ASN: base + (dc_id * 100) # Example: DC1 → 65000 + 100 = 65100 spine_asn = ( dc.spine_asn.value if dc.spine_asn.value else bgp_base_asn + (dc_id * 100) ) # Leaf pair ASNs: spine_asn + pair_number # Example: DC1 Pair 1 → 65100 + 1 = 65101 base_leaf_asn = spine_asn + 1 # Border leaf ASN: base_leaf_asn + leaf_pair_count border_asn = base_leaf_asn + leaf_pair_count topology = { "leaf_pair_count": leaf_pair_count, "total_leaf_count": total_leaf_count, "total_access_count": total_access_count, "spine_asn": spine_asn, "base_leaf_asn": base_leaf_asn, "border_asn": border_asn, } self.log.debug(f"Topology calculated: {topology}") return topology async def _create_ip_prefixes(self, dc, topology: Dict[str, Any]) -> None: """ Create IP prefixes for the datacenter. Prefix allocation scheme: - 10.{dc_id}.0.0/24 - Loopback0 (router IDs) - 10.{dc_id}.1.0/24 - Loopback1 (VTEP addresses) - 10.{dc_id}.10.0/24 - Spine-Leaf P2P links - 10.{dc_id}.20.0/24 - Leaf-Access P2P links - 10.{dc_id}.255.0/24 - MLAG peer links - 10.255.0.0/24 - Management IPs """ dc_id = dc.dc_id.value dc_name = dc.name.value # TODO: Get or create namespace # For now, assume default namespace prefixes = [ { "prefix": f"10.{dc_id}.0.0/24", "description": f"Loopback0 addresses for {dc_name}", "prefix_type": "loopback", }, { "prefix": f"10.{dc_id}.1.0/24", "description": f"VTEP Loopback1 addresses for {dc_name}", "prefix_type": "vtep", }, { "prefix": f"10.{dc_id}.10.0/24", "description": f"Spine-Leaf P2P links for {dc_name}", "prefix_type": "p2p", }, { "prefix": f"10.{dc_id}.20.0/24", "description": f"Leaf-Access P2P links for {dc_name}", "prefix_type": "p2p", }, { "prefix": f"10.{dc_id}.255.0/24", "description": f"MLAG peer links for {dc_name}", "prefix_type": "mlag", }, ] for prefix_data in prefixes: prefix_obj = await self.client.create( kind="IpamIPPrefix", data={ "prefix": prefix_data["prefix"], "description": prefix_data["description"], "prefix_type": prefix_data["prefix_type"], "status": "active", "datacenter": dc.id, }, ) await prefix_obj.save(allow_upsert=True) self.log.debug( f" Created prefix: {prefix_data['prefix']} ({prefix_data['prefix_type']})" ) async def _create_spines(self, dc, topology: Dict[str, Any]) -> List[Any]: """ Create spine switches. Naming: spine{1..N}-{DC_NAME} IPs: 10.{dc_id}.0.{10+spine_num}/32 """ dc_id = dc.dc_id.value dc_name = dc.name.value spine_count = dc.spine_count.value spine_asn = topology["spine_asn"] spines = [] for spine_num in range(1, spine_count + 1): hostname = f"spine{spine_num}-{dc_name}" loopback0_ip = f"10.{dc_id}.0.{10 + spine_num}" mgmt_ip = f"10.255.0.{10 + spine_num}" # Create device device = await self.client.create( kind="NetworkDevice", data={ "hostname": hostname, "description": f"Spine switch {spine_num} in {dc_name}", "role": "spine", "platform": "cEOS", "datacenter": dc.id, "site": dc.site.node.id, "status": "active", "spine_id": spine_num, "management_ip_template": mgmt_ip, }, ) await device.save(allow_upsert=True) # Create Loopback0 interface lo0 = await self.client.create( kind="NetworkInterface", data={ "name": "Loopback0", "description": f"Router ID for {hostname}", "interface_type": "loopback", "enabled": True, "device": device.id, "loopback_id": 0, "loopback_purpose": "router_id", }, ) await lo0.save(allow_upsert=True) # Create IP address for Loopback0 lo0_ip = await self.client.create( kind="IpamIPAddress", data={ "address": f"{loopback0_ip}/32", "description": f"Loopback0 for {hostname}", "status": "active", "interface": lo0.id, }, ) await lo0_ip.save(allow_upsert=True) # Create BGP configuration bgp_config = await self.client.create( kind="NetworkBGPConfig", data={ "asn": spine_asn, "router_id": loopback0_ip, "maximum_paths": 64, "distance_external": 20, "distance_internal": 200, "ebgp_admin_distance": 200, "default_ipv4_unicast": False, "device": device.id, }, ) await bgp_config.save(allow_upsert=True) spines.append( { "device": device, "loopback0": lo0, "loopback0_ip": loopback0_ip, "bgp_config": bgp_config, } ) self.log.info( f" ✅ Created spine: {hostname} (ASN: {spine_asn}, Loopback0: {loopback0_ip})" ) return spines async def _create_leaf_pairs( self, dc, topology: Dict[str, Any] ) -> List[Dict[str, Any]]: """ Create leaf switch pairs with MLAG configuration. Naming: leaf{1..N}-{DC_NAME} IPs: - Loopback0: 10.{dc_id}.0.{20+leaf_num}/32 - Loopback1: 10.{dc_id}.1.{20+pair_num}/32 (shared) """ dc_id = dc.dc_id.value dc_name = dc.name.value leaf_pair_count = topology["leaf_pair_count"] base_leaf_asn = topology["base_leaf_asn"] leaf_pairs = [] for pair_num in range(1, leaf_pair_count + 1): pair_asn = base_leaf_asn + (pair_num - 1) vtep_ip = f"10.{dc_id}.1.{20 + pair_num}" # Create MLAG domain mlag_domain = await self.client.create( kind="NetworkMLAGDomain", data={ "domain_id": f"MLAG-leaf{pair_num * 2 - 1}-{pair_num * 2}-{dc_name}", "local_interface": "Vlan4094", "peer_interface": "Vlan4094", "peer_address": f"10.{dc_id}.255.{pair_num * 4 - 2}", # Will be set properly per device "status": "active", }, ) await mlag_domain.save(allow_upsert=True) pair_devices = [] # Create 2 leafs in the pair (odd and even) for side_num in range(2): leaf_num = pair_num * 2 - 1 + side_num # 1, 2, 3, 4... hostname = f"leaf{leaf_num}-{dc_name}" loopback0_ip = f"10.{dc_id}.0.{20 + leaf_num}" mgmt_ip = f"10.255.0.{20 + leaf_num}" mlag_side = "left" if side_num == 0 else "right" # Create device device = await self.client.create( kind="NetworkDevice", data={ "hostname": hostname, "description": f"Leaf switch {leaf_num} in {dc_name} (Pair {pair_num})", "role": "leaf", "platform": "cEOS", "datacenter": dc.id, "site": dc.site.node.id, "status": "active", "leaf_id": leaf_num, "mlag_side": mlag_side, "mlag_domain": mlag_domain.id, "management_ip_template": mgmt_ip, }, ) await device.save(allow_upsert=True) # Create Loopback0 lo0 = await self.client.create( kind="NetworkInterface", data={ "name": "Loopback0", "description": f"Router ID for {hostname}", "interface_type": "loopback", "enabled": True, "device": device.id, "loopback_id": 0, "loopback_purpose": "router_id", }, ) await lo0.save(allow_upsert=True) lo0_ip = await self.client.create( kind="IpamIPAddress", data={ "address": f"{loopback0_ip}/32", "description": f"Loopback0 for {hostname}", "status": "active", "interface": lo0.id, }, ) await lo0_ip.save(allow_upsert=True) # Create Loopback1 (VTEP) - shared IP lo1 = await self.client.create( kind="NetworkInterface", data={ "name": "Loopback1", "description": f"VTEP for {hostname} (shared with pair)", "interface_type": "loopback", "enabled": True, "device": device.id, "loopback_id": 1, "loopback_purpose": "vtep", }, ) await lo1.save(allow_upsert=True) lo1_ip = await self.client.create( kind="IpamIPAddress", data={ "address": f"{vtep_ip}/32", "description": f"VTEP shared for leaf pair {pair_num}", "status": "active", "interface": lo1.id, }, ) await lo1_ip.save(allow_upsert=True) # Create BGP configuration bgp_config = await self.client.create( kind="NetworkBGPConfig", data={ "asn": pair_asn, "router_id": loopback0_ip, "maximum_paths": 64, "distance_external": 20, "distance_internal": 200, "ebgp_admin_distance": 200, "default_ipv4_unicast": False, "device": device.id, }, ) await bgp_config.save(allow_upsert=True) # Create VXLAN interface vxlan1 = await self.client.create( kind="NetworkVXLANTunnel", data={ "name": "Vxlan1", "source_ip": vtep_ip, "udp_port": 4789, "device": device.id, }, ) await vxlan1.save(allow_upsert=True) # Create EVPN config evpn = await self.client.create( kind="NetworkEVPNConfig", data={ "vni_auto": True, "device": device.id, }, ) await evpn.save(allow_upsert=True) pair_devices.append( { "device": device, "loopback0": lo0, "loopback0_ip": loopback0_ip, "loopback1": lo1, "vtep_ip": vtep_ip, "bgp_config": bgp_config, } ) self.log.info( f" ✅ Created leaf: {hostname} (ASN: {pair_asn}, VTEP: {vtep_ip})" ) leaf_pairs.append( { "pair_num": pair_num, "asn": pair_asn, "mlag_domain": mlag_domain, "devices": pair_devices, "vtep_ip": vtep_ip, } ) return leaf_pairs async def _create_border_pair(self, dc, topology: Dict[str, Any]) -> Dict[str, Any]: """ Create border leaf pair for DCI connectivity. Naming: borderleaf{1,2}-{DC_NAME} IPs: 10.{dc_id}.0.{30+num}/32 eth12: Created but shutdown by default """ dc_id = dc.dc_id.value dc_name = dc.name.value border_asn = topology["border_asn"] border_count = dc.border_leaf_count.value # Create MLAG domain mlag_domain = await self.client.create( kind="NetworkMLAGDomain", data={ "domain_id": f"MLAG-border-{dc_name}", "local_interface": "Vlan4094", "peer_interface": "Vlan4094", "peer_address": f"10.{dc_id}.255.254", "status": "active", }, ) await mlag_domain.save(allow_upsert=True) border_devices = [] for border_num in range(1, border_count + 1): hostname = f"borderleaf{border_num}-{dc_name}" loopback0_ip = f"10.{dc_id}.0.{30 + border_num}" mgmt_ip = f"10.255.0.{30 + border_num}" mlag_side = "left" if border_num == 1 else "right" # Create device device = await self.client.create( kind="NetworkDevice", data={ "hostname": hostname, "description": f"Border leaf {border_num} in {dc_name} (DCI capable)", "role": "borderleaf", "platform": "cEOS", "datacenter": dc.id, "site": dc.site.node.id, "status": "active", "mlag_side": mlag_side, "mlag_domain": mlag_domain.id, "management_ip_template": mgmt_ip, }, ) await device.save(allow_upsert=True) # Create Loopback0 lo0 = await self.client.create( kind="NetworkInterface", data={ "name": "Loopback0", "description": f"Router ID for {hostname}", "interface_type": "loopback", "enabled": True, "device": device.id, "loopback_id": 0, "loopback_purpose": "router_id", }, ) await lo0.save(allow_upsert=True) lo0_ip = await self.client.create( kind="IpamIPAddress", data={ "address": f"{loopback0_ip}/32", "description": f"Loopback0 for {hostname}", "status": "active", "interface": lo0.id, }, ) await lo0_ip.save(allow_upsert=True) # Create eth12 for DCI (shutdown by default) eth12 = await self.client.create( kind="NetworkInterface", data={ "name": "Ethernet12", "description": "DCI interface to DCI switch (shutdown unless dci_enabled=true)", "interface_type": "ethernet", "enabled": dc.dci_enabled.value if dc.dci_enabled.value else False, "mtu": dc.mtu.value, "device": device.id, }, ) await eth12.save(allow_upsert=True) # Create BGP configuration bgp_config = await self.client.create( kind="NetworkBGPConfig", data={ "asn": border_asn, "router_id": loopback0_ip, "maximum_paths": 64, "distance_external": 20, "distance_internal": 200, "ebgp_admin_distance": 200, "default_ipv4_unicast": False, "device": device.id, }, ) await bgp_config.save(allow_upsert=True) border_devices.append( { "device": device, "loopback0": lo0, "loopback0_ip": loopback0_ip, "bgp_config": bgp_config, "eth12": eth12, } ) self.log.info( f" ✅ Created border leaf: {hostname} (ASN: {border_asn}, eth12: {'enabled' if dc.dci_enabled.value else 'shutdown'})" ) return { "asn": border_asn, "mlag_domain": mlag_domain, "devices": border_devices, } async def _create_access_switches( self, dc, topology: Dict[str, Any], leaf_pairs: List[Dict[str, Any]] ) -> List[Any]: """ Create access switches and assign to leaf pairs. Assignment: Round-robin to leaf pairs (2 access per pair) Naming: access{bay_num}-{DC_NAME} """ dc_name = dc.name.value number_of_bays = dc.number_of_bays.value access_switches = [] for bay_num in range(1, number_of_bays + 1): hostname = f"access{bay_num}-{dc_name}" mgmt_ip = f"10.255.0.{100 + bay_num}" # Assign to leaf pair (round-robin) assigned_pair_idx = (bay_num - 1) // 2 assigned_pair = leaf_pairs[assigned_pair_idx] # Create device device = await self.client.create( kind="NetworkDevice", data={ "hostname": hostname, "description": f"Access switch for Bay {bay_num} in {dc_name}", "role": "access", "platform": "cEOS", "datacenter": dc.id, "site": dc.site.node.id, "status": "active", "management_ip_template": mgmt_ip, }, ) await device.save(allow_upsert=True) access_switches.append( { "device": device, "bay_num": bay_num, "assigned_pair": assigned_pair, } ) self.log.info( f" ✅ Created access: {hostname} (Bay {bay_num} → Leaf Pair {assigned_pair['pair_num']})" ) return access_switches async def _create_spine_leaf_connectivity( self, dc, spines: List[Any], leaf_pairs: List[Dict[str, Any]], border_pair: Dict[str, Any], topology: Dict[str, Any], ) -> None: """ Create interfaces and BGP sessions between spines and leafs (including borders). Each leaf connects to ALL spines. Interface assignment: - Spine side: Ethernet{1..N} per leaf - Leaf side: Ethernet{1..3} for 3 spines """ dc_id = dc.dc_id.value # P2P IP allocator (starting from 10.{dc_id}.10.0/31) p2p_counter = 0 # Collect all leaf devices (regular + border) all_leaf_devices = [] for pair in leaf_pairs: all_leaf_devices.extend(pair["devices"]) if border_pair: all_leaf_devices.extend(border_pair["devices"]) # For each spine for spine_idx, spine in enumerate(spines): spine_device = spine["device"] # Connect to each leaf for leaf_idx, leaf in enumerate(all_leaf_devices): leaf_device = leaf["device"] # Allocate P2P subnet spine_ip = f"10.{dc_id}.10.{p2p_counter * 2}" leaf_ip = f"10.{dc_id}.10.{p2p_counter * 2 + 1}" p2p_counter += 1 # Spine interface spine_eth = f"Ethernet{leaf_idx + 1}" spine_int = await self.client.create( kind="NetworkInterface", data={ "name": spine_eth, "description": f"To {leaf_device.hostname.value}", "interface_type": "ethernet", "enabled": True, "mtu": dc.mtu.value, "device": spine_device.id, }, ) await spine_int.save(allow_upsert=True) spine_ip_obj = await self.client.create( kind="IpamIPAddress", data={ "address": f"{spine_ip}/31", "description": f"{spine_device.hostname.value} to {leaf_device.hostname.value}", "status": "active", "interface": spine_int.id, }, ) await spine_ip_obj.save(allow_upsert=True) # Leaf interface leaf_eth = f"Ethernet{spine_idx + 1}" leaf_int = await self.client.create( kind="NetworkInterface", data={ "name": leaf_eth, "description": f"To {spine_device.hostname.value}", "interface_type": "ethernet", "enabled": True, "mtu": dc.mtu.value, "device": leaf_device.id, }, ) await leaf_int.save(allow_upsert=True) leaf_ip_obj = await self.client.create( kind="IpamIPAddress", data={ "address": f"{leaf_ip}/31", "description": f"{leaf_device.hostname.value} to {spine_device.hostname.value}", "status": "active", "interface": leaf_int.id, }, ) await leaf_ip_obj.save(allow_upsert=True) # Create BGP neighbor on spine spine_bgp = spine["bgp_config"] spine_neighbor = await self.client.create( kind="NetworkBGPNeighbor", data={ "neighbor_ip": leaf_ip, "description": f"To {leaf_device.hostname.value}", "enabled": True, "peer_type": "ebgp", "bgp_config": spine_bgp.id, "local_interface": spine_int.id, "remote_device": leaf_device.id, }, ) await spine_neighbor.save(allow_upsert=True) # Create BGP neighbor on leaf leaf_bgp = leaf["bgp_config"] leaf_neighbor = await self.client.create( kind="NetworkBGPNeighbor", data={ "neighbor_ip": spine_ip, "description": f"To {spine_device.hostname.value}", "enabled": True, "peer_type": "ebgp", "bgp_config": leaf_bgp.id, "local_interface": leaf_int.id, "remote_device": spine_device.id, }, ) await leaf_neighbor.save(allow_upsert=True) self.log.debug(f" Created {p2p_counter} spine-leaf P2P links") async def _create_leaf_access_connectivity( self, dc, leaf_pairs: List[Dict[str, Any]], access_switches: List[Any], topology: Dict[str, Any], ) -> None: """ Create dual-homed connectivity from access switches to leaf pairs. Each access connects to both leafs in assigned pair: - access eth1 → leaf_left eth7 - access eth2 → leaf_right eth7 """ dc_id = dc.dc_id.value # P2P IP allocator (starting from 10.{dc_id}.20.0/31) p2p_counter = 0 for access in access_switches: access_device = access["device"] assigned_pair = access["assigned_pair"] # Connect to both leafs in the pair for link_num, leaf_data in enumerate(assigned_pair["devices"], start=1): leaf_device = leaf_data["device"] # Allocate P2P subnet leaf_ip = f"10.{dc_id}.20.{p2p_counter * 2}" access_ip = f"10.{dc_id}.20.{p2p_counter * 2 + 1}" p2p_counter += 1 # Leaf interface (eth7 for access connectivity) leaf_int = await self.client.create( kind="NetworkInterface", data={ "name": "Ethernet7", "description": f"To {access_device.hostname.value}", "interface_type": "ethernet", "enabled": True, "mtu": dc.mtu.value, "device": leaf_device.id, }, ) await leaf_int.save(allow_upsert=True) leaf_ip_obj = await self.client.create( kind="IpamIPAddress", data={ "address": f"{leaf_ip}/31", "description": f"{leaf_device.hostname.value} to {access_device.hostname.value}", "status": "active", "interface": leaf_int.id, }, ) await leaf_ip_obj.save(allow_upsert=True) # Access interface access_eth = f"Ethernet{link_num}" access_int = await self.client.create( kind="NetworkInterface", data={ "name": access_eth, "description": f"To {leaf_device.hostname.value}", "interface_type": "ethernet", "enabled": True, "mtu": dc.mtu.value, "device": access_device.id, }, ) await access_int.save(allow_upsert=True) access_ip_obj = await self.client.create( kind="IpamIPAddress", data={ "address": f"{access_ip}/31", "description": f"{access_device.hostname.value} to {leaf_device.hostname.value}", "status": "active", "interface": access_int.id, }, ) await access_ip_obj.save(allow_upsert=True) self.log.debug(f" Created {p2p_counter} leaf-access P2P links") async def _update_datacenter_computed_fields( self, dc, topology: Dict[str, Any] ) -> None: """ Update the datacenter object with computed values. """ # Update computed attributes dc.leaf_pair_count.value = topology["leaf_pair_count"] dc.total_leaf_count.value = topology["total_leaf_count"] dc.total_access_count.value = topology["total_access_count"] await dc.save() self.log.debug( f" Updated computed fields: leaf_pair_count={topology['leaf_pair_count']}, " f"total_leaf_count={topology['total_leaf_count']}, " f"total_access_count={topology['total_access_count']}" )