* Add Netbox configuration and plugins * Add Containerlab topology * Add template * Update Documentation
344 lines
13 KiB
Python
344 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
create_vxlan_fabric.py (version avec NetBoxBackend)
|
||
|
||
Ce script illustre comment créer une fabric VXLAN sur NetBox,
|
||
notamment :
|
||
- Création ou sélection d'un site.
|
||
- Création de spines, leaves, et access.
|
||
- Création du câblage.
|
||
- Allocation automatique des /31 pour les liaisons.
|
||
- Attribution d'un /32 loopback par device.
|
||
- Attribution automatique d'ASN (Custom Field "ASN").
|
||
|
||
Il utilise une classe d'abstraction "NetBoxBackend" (dans NetBox_backend.py)
|
||
pour simplifier et clarifier les interactions avec l'API.
|
||
"""
|
||
|
||
import getpass
|
||
import sys
|
||
|
||
from helpers.netbox_backend import NetBoxBackend
|
||
|
||
|
||
def main():
|
||
print("=== VXLAN Fabric Creation Script (via NetBoxBackend) ===")
|
||
|
||
# 1) NetBox details
|
||
netbox_url = input("NetBox URL (e.g. https://netbox.local): ").strip()
|
||
if not netbox_url:
|
||
print("ERROR: NetBox URL is required.")
|
||
sys.exit(1)
|
||
|
||
netbox_token = getpass.getpass("NetBox API Token: ")
|
||
if not netbox_token:
|
||
print("ERROR: NetBox API token is required.")
|
||
sys.exit(1)
|
||
|
||
# 2) Init the NetBox backend wrapper
|
||
try:
|
||
nb = NetBoxBackend(netbox_url, netbox_token, verify_ssl=True)
|
||
except Exception as exc:
|
||
print(f"ERROR: Failed to connect to NetBox: {exc}")
|
||
sys.exit(1)
|
||
|
||
# 3) Choose or create Site
|
||
existing_sites = nb.get_sites()
|
||
if not existing_sites:
|
||
print("No sites found in NetBox.")
|
||
sys.exit(1)
|
||
|
||
print("\nExisting Sites:")
|
||
for idx, s in enumerate(existing_sites, start=1):
|
||
print(f" {idx}. {s.name} (slug={s.slug})")
|
||
|
||
choice = input("Choose a site by number, or type 'new' to create one: ").strip().lower()
|
||
if choice == "new":
|
||
site_name = input("New site name (e.g. 'Paris'): ").strip()
|
||
site_code_input = input("New site code (e.g. 'PA'): ").strip()
|
||
if not site_name or not site_code_input:
|
||
print("ERROR: Site name and code required.")
|
||
sys.exit(1)
|
||
try:
|
||
site = nb.create_site(site_name, site_code_input.lower())
|
||
print(f"Created new site: {site.name} ({site.slug})")
|
||
except Exception as exc:
|
||
print(f"ERROR: Failed to create site: {exc}")
|
||
sys.exit(1)
|
||
|
||
site_clean = site.name.strip()
|
||
site_code = site_clean[:2].upper() if len(site_clean) >= 2 else site_clean.upper()
|
||
else:
|
||
try:
|
||
site_index = int(choice)
|
||
site = existing_sites[site_index - 1]
|
||
site_clean = site.name.strip()
|
||
site_code = site_clean[:2].upper() if len(site_clean) >= 2 else site_clean.upper()
|
||
except (ValueError, IndexError):
|
||
print("ERROR: Invalid site selection.")
|
||
sys.exit(1)
|
||
|
||
# 4) Number of buildings
|
||
while True:
|
||
try:
|
||
num_buildings = int(input("How many buildings? (1–5): ").strip())
|
||
if 1 <= num_buildings <= 5:
|
||
break
|
||
else:
|
||
print("ERROR: Please choose between 1 and 5.")
|
||
except ValueError:
|
||
print("ERROR: Invalid input. Try again.")
|
||
|
||
# 5) Device type slugs
|
||
print("\nEnter device type slugs (must exist in NetBox).")
|
||
spine_devtype_slug = input("Spine Device Type Slug: ").strip()
|
||
leaf_devtype_slug = input("Leaf Device Type Slug: ").strip()
|
||
access_devtype_slug = input("Access Switch Device Type Slug: ").strip()
|
||
|
||
# 6) Roles
|
||
spine_role = nb.get_device_role("spine")
|
||
if not spine_role:
|
||
print("ERROR: No device role with slug='spine'.")
|
||
sys.exit(1)
|
||
|
||
leaf_role = nb.get_device_role("leaf")
|
||
if not leaf_role:
|
||
print("ERROR: No device role with slug='leaf'.")
|
||
sys.exit(1)
|
||
|
||
access_role = nb.get_device_role("access")
|
||
if not access_role:
|
||
print("ERROR: No device role with slug='access'.")
|
||
sys.exit(1)
|
||
|
||
print(f"Using roles -> Spine={spine_role.id}, Leaf={leaf_role.id}, Access={access_role.id}")
|
||
|
||
# 7) Create / Retrieve 2 Spines
|
||
spine_names = [f"{site_code.lower()}dc_sp1_00", f"{site_code.lower()}dc_sp2_00"]
|
||
spines = []
|
||
|
||
for name in spine_names:
|
||
try:
|
||
new_spine = nb.create_device(
|
||
name=name,
|
||
device_type_slug=spine_devtype_slug,
|
||
role_id=spine_role.id,
|
||
site_id=site.id
|
||
)
|
||
print(f"Spine: {new_spine.name}")
|
||
spines.append(new_spine)
|
||
except Exception as exc:
|
||
print(f"ERROR creating spine '{name}': {exc}")
|
||
sys.exit(1)
|
||
|
||
# 8) Create Leaves + Access per building
|
||
leaves = []
|
||
access_switches = []
|
||
|
||
# Helper to create/find location
|
||
def get_or_create_location(site_obj, location_name: str):
|
||
existing_loc = nb.nb.dcim.locations.get(site_id=site_obj.id, name=location_name)
|
||
if existing_loc:
|
||
print(f"Location '{existing_loc.name}' already exists; reusing.")
|
||
return existing_loc
|
||
try:
|
||
loc = nb.nb.dcim.locations.create(
|
||
name=location_name,
|
||
slug=location_name.lower(),
|
||
site=site_obj.id
|
||
)
|
||
print(f"Created Location '{loc.name}'")
|
||
return loc
|
||
except Exception as loc_exc:
|
||
print(f"ERROR creating location '{location_name}': {loc_exc}")
|
||
sys.exit(1)
|
||
|
||
for b_num in range(1, num_buildings + 1):
|
||
building_code = f"{site_code}{b_num}"
|
||
location = get_or_create_location(site, building_code)
|
||
|
||
# Leaf device
|
||
leaf_name = f"{site_code.lower()}{str(b_num).zfill(2)}_lf1_00"
|
||
try:
|
||
leaf_dev = nb.create_device(
|
||
name=leaf_name,
|
||
device_type_slug=leaf_devtype_slug,
|
||
role_id=leaf_role.id,
|
||
site_id=site.id,
|
||
location_id=location.id
|
||
)
|
||
print(f"Leaf: {leaf_dev.name}")
|
||
except Exception as exc:
|
||
print(f"ERROR creating leaf '{leaf_name}': {exc}")
|
||
sys.exit(1)
|
||
leaves.append(leaf_dev)
|
||
|
||
# Access Switch
|
||
sw_name = f"{site_code.lower()}{str(b_num).zfill(2)}_sw1_00"
|
||
try:
|
||
acc_dev = nb.create_device(
|
||
name=sw_name,
|
||
device_type_slug=access_devtype_slug,
|
||
role_id=access_role.id,
|
||
site_id=site.id,
|
||
location_id=location.id
|
||
)
|
||
print(f"Access Switch: {acc_dev.name}")
|
||
except Exception as exc:
|
||
print(f"ERROR creating access switch '{sw_name}': {exc}")
|
||
sys.exit(1)
|
||
access_switches.append(acc_dev)
|
||
|
||
# 9) Cabling
|
||
def create_leaf_spine_cables(leaf_dev, spine_dev, leaf_if_name, spine_if_name):
|
||
leaf_if = nb.get_or_create_interface(leaf_dev.id, leaf_if_name)
|
||
spine_if = nb.get_or_create_interface(spine_dev.id, spine_if_name)
|
||
nb.create_cable_if_not_exists(leaf_if, spine_if)
|
||
|
||
for i, leaf_dev in enumerate(leaves, start=1):
|
||
# Leaf <-> Spine1 sur Ethernet1
|
||
create_leaf_spine_cables(leaf_dev, spines[0], "Ethernet1", f"Ethernet{i}")
|
||
# Leaf <-> Spine2 sur Ethernet2
|
||
create_leaf_spine_cables(leaf_dev, spines[1], "Ethernet2", f"Ethernet{i}")
|
||
|
||
# Leaf <-> Access Switch sur Ethernet3 (leaf) / Ethernet1 (access)
|
||
leaf_eth3 = nb.get_or_create_interface(leaf_dev.id, "Ethernet3")
|
||
acc_dev = access_switches[i - 1]
|
||
acc_if = nb.get_or_create_interface(acc_dev.id, "Ethernet1")
|
||
nb.create_cable_if_not_exists(leaf_eth3, acc_if)
|
||
|
||
# 10) IP Assignments (/31) + ASN custom field
|
||
# 10a) Récupérer le prefix underlay
|
||
underlay_role = nb.nb.ipam.roles.get(slug="underlaycontainer")
|
||
if not underlay_role:
|
||
print("ERROR: No IPAM role 'underlaycontainer' found.")
|
||
sys.exit(1)
|
||
|
||
underlay_pfxs = nb.nb.ipam.prefixes.filter(role_id=underlay_role.id, scope_id=site.id)
|
||
underlay_list = list(underlay_pfxs)
|
||
if not underlay_list:
|
||
print("ERROR: No underlay prefix found for this site.")
|
||
sys.exit(1)
|
||
|
||
parent_prefix = underlay_list[0]
|
||
print(f"Using parent prefix '{parent_prefix.prefix}' for /31 allocations.")
|
||
|
||
# 10b) Assign ASNs (spines 65001, leaves 65101)
|
||
next_spine_asn = 65001
|
||
next_leaf_asn = 65101
|
||
|
||
# Spines
|
||
for spine_dev in spines:
|
||
dev_obj = nb.nb.dcim.devices.get(spine_dev.id)
|
||
if not dev_obj:
|
||
print(f"ERROR: Could not re-fetch spine '{spine_dev.name}'")
|
||
sys.exit(1)
|
||
if "ASN" not in dev_obj.custom_fields:
|
||
print(f"[WARNING] Spine '{dev_obj.name}' has no custom field 'ASN'.")
|
||
else:
|
||
dev_obj.custom_fields["ASN"] = next_spine_asn
|
||
try:
|
||
dev_obj.save()
|
||
print(f"Assigned ASN={next_spine_asn} to spine '{dev_obj.name}'.")
|
||
except Exception as exc:
|
||
print(f"ERROR saving 'ASN' on {dev_obj.name}: {exc}")
|
||
next_spine_asn += 1
|
||
|
||
# Leaves
|
||
for leaf_dev in leaves:
|
||
dev_obj = nb.nb.dcim.devices.get(leaf_dev.id)
|
||
if not dev_obj:
|
||
print(f"ERROR: Could not re-fetch leaf '{leaf_dev.name}'")
|
||
sys.exit(1)
|
||
if "ASN" not in dev_obj.custom_fields:
|
||
print(f"[WARNING] Leaf '{dev_obj.name}' has no custom field 'ASN'.")
|
||
else:
|
||
dev_obj.custom_fields["ASN"] = next_leaf_asn
|
||
try:
|
||
dev_obj.save()
|
||
print(f"Assigned ASN={next_leaf_asn} to leaf '{dev_obj.name}'.")
|
||
except Exception as exc:
|
||
print(f"ERROR saving 'ASN' on {dev_obj.name}: {exc}")
|
||
next_leaf_asn += 1
|
||
|
||
# 10c) Allouer /31 pour chaque liaison Spine<->Leaf
|
||
for i, leaf_dev in enumerate(leaves, start=1):
|
||
# Leaf.Eth1 <-> Spine1.Eth{i}
|
||
leaf_eth1 = nb.nb.dcim.interfaces.get(device_id=leaf_dev.id, name="Ethernet1")
|
||
sp1_if = nb.nb.dcim.interfaces.get(device_id=spines[0].id, name=f"Ethernet{i}")
|
||
|
||
child_31 = nb.allocate_prefix(parent_prefix, 31, site.id, underlay_role.id)
|
||
if not child_31:
|
||
print("ERROR: Could not allocate /31 for Spine1<->Leaf.")
|
||
sys.exit(1)
|
||
ip_list = nb.get_available_ips_in_prefix(child_31)
|
||
if len(ip_list) < 2:
|
||
print("ERROR: Not enough IP addresses in newly allocated /31.")
|
||
sys.exit(1)
|
||
|
||
nb.assign_ip_to_interface(sp1_if, ip_list[0].address)
|
||
nb.assign_ip_to_interface(leaf_eth1, ip_list[1].address)
|
||
|
||
# Leaf.Eth2 <-> Spine2.Eth{i}
|
||
leaf_eth2 = nb.nb.dcim.interfaces.get(device_id=leaf_dev.id, name="Ethernet2")
|
||
sp2_if = nb.nb.dcim.interfaces.get(device_id=spines[1].id, name=f"Ethernet{i}")
|
||
|
||
child_31b = nb.allocate_prefix(parent_prefix, 31, site.id, underlay_role.id)
|
||
if not child_31b:
|
||
print("ERROR: No /31 returned for Spine2<->Leaf.")
|
||
sys.exit(1)
|
||
ip_list_b = nb.get_available_ips_in_prefix(child_31b)
|
||
if len(ip_list_b) < 2:
|
||
print("ERROR: Not enough IP addresses in newly allocated /31.")
|
||
sys.exit(1)
|
||
|
||
nb.assign_ip_to_interface(sp2_if, ip_list_b[0].address)
|
||
nb.assign_ip_to_interface(leaf_eth2, ip_list_b[1].address)
|
||
|
||
# 11) Loopback /32 assignment
|
||
loopback_role = nb.nb.ipam.roles.get(slug="loopbackcontainer")
|
||
if not loopback_role:
|
||
print("ERROR: No IPAM role 'loopbackcontainer' found.")
|
||
sys.exit(1)
|
||
|
||
loopback_pfxs = nb.nb.ipam.prefixes.filter(role_id=loopback_role.id, scope_id=site.id)
|
||
loopback_list = list(loopback_pfxs)
|
||
if not loopback_list:
|
||
print("ERROR: No loopback prefix found for this site.")
|
||
sys.exit(1)
|
||
|
||
loopback_parent = loopback_list[0]
|
||
print(f"Using parent prefix '{loopback_parent.prefix}' for /32 loopback allocations.")
|
||
|
||
for dev in spines + leaves:
|
||
# Get or create Loopback0
|
||
loop0_if = nb.get_or_create_interface(dev.id, "Loopback0", "virtual")
|
||
if not loop0_if:
|
||
print(f"ERROR: Could not create/retrieve Loopback0 for {dev.name}")
|
||
continue
|
||
|
||
child_32 = nb.allocate_prefix(loopback_parent, 32, site.id, loopback_role.id)
|
||
if not child_32:
|
||
print(f"ERROR: Could not allocate /32 for {dev.name}.")
|
||
continue
|
||
|
||
ip_list_c = nb.get_available_ips_in_prefix(child_32)
|
||
if not ip_list_c:
|
||
print(f"ERROR: Not enough IP addresses in newly allocated /32 for {dev.name}.")
|
||
continue
|
||
|
||
new_lo_ip = nb.assign_ip_to_interface(loop0_if, ip_list_c[0].address)
|
||
if new_lo_ip:
|
||
print(f"Assigned {new_lo_ip.address} to {dev.name} Loopback0.")
|
||
|
||
print("\n=== Fabric Creation Completed ===")
|
||
print(f"Site: {site.name} (slug={site.slug})")
|
||
print("Spines:", [dev.name for dev in spines])
|
||
print("Leaves:", [dev.name for dev in leaves])
|
||
print("Access Switches:", [dev.name for dev in access_switches])
|
||
print("Each leaf/spine link got a new /31, Loopback0 got a new /32, and ASNs were assigned.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|