Start dev (#4)

* Add Netbox configuration and plugins
* Add Containerlab topology 
* Add template
* Update Documentation
This commit is contained in:
D. Arnodo
2025-02-25 19:22:12 +01:00
committed by GitHub
parent 21ff9058e4
commit add5805b91
40 changed files with 2185 additions and 1431 deletions

View File

@@ -0,0 +1,68 @@
from helpers.netbox_backend import NetBoxBackend
import sys
# Ask user for NetBox connection details
url = input("Enter NetBox URL: ")
token = input("Enter NetBox API Token: ")
nb_backend = NetBoxBackend(url, token)
# Ask for customer details
customer_name = input("Enter Customer Name: ")
vlan_id = int(input("Enter VLAN ID: "))
vni_id = int(input("Enter VNI ID: "))
# Get available locations
locations = list(nb_backend.nb.dcim.locations.all())
for idx, loc in enumerate(locations):
print(f"{idx}: {loc.name}")
selected_indices = input("Select one or multiple locations by index (comma-separated): ")
selected_locations = [loc for i, loc in enumerate(locations) if str(i) in selected_indices.split(",")]
# Create tenant
tenant = nb_backend.create_tenant(customer_name, customer_name.lower().replace(" ", "-"))
# Update locations to attach them to the tenant
for location in selected_locations:
try:
location.tenant = tenant.id
location.save()
except Exception as e:
print(f"[ERROR] Failed to update location {location.name} with tenant: {e}")
# Allocate /24 prefix for customer
role_id = nb_backend.nb.ipam.roles.get(slug="customerscontainer").id
parent_prefixes = list(nb_backend.nb.ipam.prefixes.filter(role_id=role_id))
if not parent_prefixes:
print("[ERROR] No available parent prefix found.")
sys.exit(1)
customer_prefix = nb_backend.allocate_prefix(parent_prefixes[0], 24, None, None)
if not customer_prefix:
print("[ERROR] Could not allocate /24 for customer.")
sys.exit(1)
# Create L2VPN
l2vpn_slug = f"{customer_name.lower().replace(' ', '-')}-vpn"
l2vpn = nb_backend.create_l2vpn(vni_id, f"{customer_name}_vpn", l2vpn_slug, tenant.id)
# Create VLAN
vlan_slug = f"{customer_name.lower().replace(' ', '-')}-vlan"
vlan = nb_backend.create_vlan(vlan_id, f"{customer_name}_vlan", vlan_slug, tenant.id)
# Create VXLAN termination
vxlan_termination = nb_backend.create_vxlan_termination(l2vpn.id, "ipam.vlan", vlan.id)
# Assign IP to leaf devices Ethernet3
for location in selected_locations:
leaf_devices = nb_backend.nb.dcim.devices.filter(role="leaf", location_id=location.id)
if leaf_devices:
ip_list = nb_backend.get_available_ips_in_prefix(customer_prefix)
if len(ip_list) < len(leaf_devices):
print("[ERROR] Not enough IP addresses available in the allocated /24.")
sys.exit(1)
for device, ip in zip(leaf_devices, ip_list):
interface = nb_backend.get_or_create_interface(device.id, "Ethernet3")
nb_backend.assign_ip_to_interface(interface, ip.address)
else:
print(f"[ERROR] No leaf devices found in location {location.name}.")

View File

@@ -0,0 +1,343 @@
#!/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? (15): ").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()

View File

@@ -0,0 +1,226 @@
"""
NetBox_backend.py
=================
A Python class to interact with NetBox using pynetbox.
"""
import pynetbox
from typing import Optional, List, Dict
class NetBoxBackend:
def __init__(self, url: str, token: str, verify_ssl: bool = True):
"""
Initializes the NetBox API connection.
"""
self.url = url
self.token = token
self.nb = pynetbox.api(self.url, token=self.token)
self.nb.http_session.verify = verify_ssl
## ----------------------------------
## TENANTS MANAGEMENT
## ----------------------------------
def get_tenants(self) -> List:
""" Returns all tenants in NetBox. """
try:
return list(self.nb.tenancy.tenants.all())
except Exception as e:
print(f"[ERROR] Failed to fetch tenants: {e}")
return []
def create_tenant(self, name: str, slug: str):
""" Creates a new tenant in NetBox. """
try:
return self.nb.tenancy.tenants.create({"name": name, "slug": slug})
except Exception as e:
print(f"[ERROR] Failed to create tenant '{name}': {e}")
return None
## ----------------------------------
## SITES MANAGEMENT
## ----------------------------------
def get_sites(self) -> List:
""" Returns all sites in NetBox. """
try:
return list(self.nb.dcim.sites.all())
except Exception as e:
print(f"[ERROR] Failed to fetch sites: {e}")
return []
def create_site(self, name: str, slug: str):
""" Creates a new site in NetBox. """
try:
return self.nb.dcim.sites.create({"name": name, "slug": slug})
except Exception as e:
print(f"[ERROR] Failed to create site '{name}': {e}")
return None
## ----------------------------------
## DEVICE MANAGEMENT
## ----------------------------------
def get_device_type_by_slug(self, slug: str) -> Optional[Dict]:
""" Returns a device type by slug. """
try:
return self.nb.dcim.device_types.get(slug=slug)
except Exception as e:
print(f"[ERROR] Failed to fetch device type '{slug}': {e}")
return None
def get_device_role(self, slug: str) -> Optional[Dict]:
""" Returns a device role by slug. """
try:
return self.nb.dcim.device_roles.get(slug=slug)
except Exception as e:
print(f"[ERROR] Failed to fetch device role '{slug}': {e}")
return None
def create_device(self, name: str, device_type_slug: str, role_id: int, site_id: int, location_id: Optional[int] = None):
""" Creates a device in NetBox if it doesn't already exist. """
try:
existing_device = self.nb.dcim.devices.get(name=name)
if existing_device:
return existing_device
device_type = self.get_device_type_by_slug(device_type_slug)
if not device_type:
print(f"[ERROR] Device type '{device_type_slug}' not found.")
return None
return self.nb.dcim.devices.create({
"name": name,
"device_type": {"id": device_type.id},
"role": role_id,
"site": site_id,
"location": location_id
})
except Exception as e:
print(f"[ERROR] Failed to create device '{name}': {e}")
return None
## ----------------------------------
## INTERFACES & CABLING
## ----------------------------------
def get_or_create_interface(self, device_id: int, if_name: str, if_type: str = "40gbase-x-qsfpp"):
""" Retrieves or creates an interface on a given device. """
try:
intf = self.nb.dcim.interfaces.get(device_id=device_id, name=if_name)
if intf:
return intf
return self.nb.dcim.interfaces.create({
"device": device_id,
"name": if_name,
"type": if_type,
})
except Exception as e:
print(f"[ERROR] Failed to create/get interface '{if_name}': {e}")
return None
def create_cable_if_not_exists(self, intf_a, intf_b):
""" Creates a cable between two interfaces if it doesn't exist. """
if not intf_a or not intf_b:
print("[WARN] Missing interfaces to create cable.")
return None
try:
return self.nb.dcim.cables.create({
"a_terminations": [{"object_type": "dcim.interface", "object_id": intf_a.id}],
"b_terminations": [{"object_type": "dcim.interface", "object_id": intf_b.id}],
"status": "connected",
})
except Exception as e:
print(f"[ERROR] Failed to create cable: {e}")
return None
## ----------------------------------
## NETWORK MANAGEMENT
## ----------------------------------
def create_vlan(self, vlan_id: int, vlan_name: str, slug:str, tenant_id: str):
""" Creates a VLAN in NetBox. """
try:
return self.nb.ipam.vlans.create({
"vid": vlan_id,
"name": vlan_name,
"slug": slug,
"tenant": tenant_id,
})
except Exception as e:
print(f"[ERROR] Failed to create VLAN '{vlan_name}': {e}")
return None
def create_l2vpn(self, vni_id: int, vpn_name: str, slug: str, tenant_id: str):
""" Creates an L2VPN in NetBox. """
try:
return self.nb.vpn.l2vpns.create({
"name": vpn_name,
"slug": slug,
"type": "vxlan-evpn",
"tenant": tenant_id,
"identifier": vni_id
})
except Exception as e:
print(f"[ERROR] Failed to create L2VPN '{vpn_name}': {e}")
return None
def create_vxlan_termination(self, l2vpn_id: int, assigned_object_type: str, assigned_object_id: int):
""" Creates a VXLAN termination for L2VPN. """
try:
return self.nb.vpn.l2vpn_terminations.create({
"l2vpn": l2vpn_id,
"assigned_object_type": assigned_object_type,
"assigned_object_id": assigned_object_id
})
except Exception as e:
print(f"[ERROR] Failed to create VXLAN termination: {e}")
return None
def allocate_prefix(self, parent_prefix, prefix_length: int, site_id: int, role_id: int):
"""
Alloue un sous-réseau enfant (ex: /31 ou /32) à partir d'un préfixe parent
via available_prefixes.create().
"""
try:
child_prefix = parent_prefix.available_prefixes.create({
"prefix_length": prefix_length,
"site": site_id,
"role": role_id,
})
return child_prefix
except Exception as exc:
print(f"[ERROR] Echec de l'allocation d'un /{prefix_length} pour {parent_prefix.prefix}: {exc}")
return None
def assign_ip_to_interface(self, interface, ip_address: str, status: str = "active"):
""" Assigns an IP address to an interface. """
try:
return self.nb.ipam.ip_addresses.create({
"address": ip_address,
"assigned_object_id": interface.id,
"assigned_object_type": "dcim.interface",
"status": status,
})
except Exception as e:
print(f"[ERROR] Failed to assign IP {ip_address}: {e}")
return None
def get_available_ips_in_prefix(self, prefix) -> List:
""" Fetches available IPs within a prefix. """
if not hasattr(prefix, "available_ips"):
print(f"[ERROR] Invalid prefix object: {prefix}")
return []
return list(prefix.available_ips.list())
def save_custom_fields(self, device, fields: Dict[str, any]):
""" Saves custom fields for a device. """
try:
for key, value in fields.items():
device.custom_fields[key] = value
device.save()
return True
except Exception as e:
print(f"[ERROR] Failed to save custom fields: {e}")
return False