push generator
This commit is contained in:
18
infrahub/.infrahub.yml
Normal file
18
infrahub/.infrahub.yml
Normal file
@@ -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"
|
||||
905
infrahub/generators/datacenter_generator.py
Normal file
905
infrahub/generators/datacenter_generator.py
Normal file
@@ -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']}"
|
||||
)
|
||||
55
infrahub/generators/datacenter_query.gql
Normal file
55
infrahub/generators/datacenter_query.gql
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user