diff --git a/infrahub/.infrahub.yml b/infrahub/.infrahub.yml new file mode 100644 index 0000000..2ad7b54 --- /dev/null +++ b/infrahub/.infrahub.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://schema.infrahub.app/python-sdk/repository-config/latest.json +--- +# Infrahub Repository Configuration +# This file defines generators and queries for datacenter automation + +generator_definitions: + - name: datacenter_generator + file_path: "generators/datacenter_generator.py" + class_name: DatacenterGenerator + query: datacenter_query + targets: datacenters + convert_query_response: true + parameters: + datacenter_id: "id" + +queries: + - name: datacenter_query + file_path: "generators/datacenter_query.gql" \ No newline at end of file diff --git a/infrahub/generators/datacenter_generator.py b/infrahub/generators/datacenter_generator.py new file mode 100644 index 0000000..a390286 --- /dev/null +++ b/infrahub/generators/datacenter_generator.py @@ -0,0 +1,905 @@ +""" +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']}" + ) diff --git a/infrahub/generators/datacenter_query.gql b/infrahub/generators/datacenter_query.gql new file mode 100644 index 0000000..d8f630e --- /dev/null +++ b/infrahub/generators/datacenter_query.gql @@ -0,0 +1,55 @@ +# GraphQL query to fetch InfraDatacenter with all required attributes +# This query retrieves all necessary data for the datacenter generator + +query DatacenterQuery($datacenter_id: String!) { + InfraDatacenter(ids: [$datacenter_id]) { + edges { + node { + id + __typename + + # Basic attributes + name { value } + dc_id { value } + description { value } + + # Topology configuration + number_of_bays { value } + spine_count { value } + border_leaf_count { value } + + # Network configuration + parent_subnet { value } + bgp_base_asn { value } + spine_asn { value } + mlag_domain_id { value } + mtu { value } + + # DCI configuration + dci_enabled { value } + dci_remote_dc_id { value } + has_border_leafs { value } + + # Status + status { value } + + # Relationships + site { + node { + id + __typename + name { value } + organization { + node { + id + __typename + name { value } + asn_base { value } + } + } + } + } + } + } + } +}