Feat/leaf template (#7)

* fix(template) : Change `Ethernet3`configuration
* fix(Template): Missing Underlay configuration
* Fix(template) : error 'ipam.models.ip.IPAddress DoesNotExist
* fix(routing) : ! IP routing not enabled
* fix(leaves template): adding ip routing and multi-agent model
This commit is contained in:
D. Arnodo
2025-03-28 16:52:37 +01:00
committed by GitHub
parent add5805b91
commit c8daee6c11
15 changed files with 1380 additions and 652 deletions

7
.gitignore vendored
View File

@@ -1,12 +1,13 @@
.DS_Store .DS_Store
documentation/assets/images/diagrams/.$VXLAN.drawio.bkp documentation/assets/images/diagrams/.$VXLAN.drawio.bkp
clab-vxlan-evpn-l2/* fabric_vxlan/*
netbox/ netbox/
.env .env
.venv .venv
.python-version .python-version
containerlab/clab-vxlan-evpn-l2 containerlab/clab-vxlan_fabric
containerlab/.lab_vxlan.yml.bak
.vscode/settings.json .vscode/settings.json
vxlan_automation.egg-info vxlan_automation.egg-info
__pycache__ __pycache__
containerlab/.fabric_vxlan.yml.bak
containerlab/network_images/*.tar.xz

View File

@@ -1,47 +1,47 @@
name: vxlan-evpn-l2 name: vxlan_fabric
mgmt: mgmt:
network: management network: management
topology: topology:
nodes: nodes:
padc_sp1_00: padc_sp1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.10 mgmt-ipv4: 172.20.20.10
padc_sp2_00: padc_sp2_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.11 mgmt-ipv4: 172.20.20.11
pa01_lf1_00: pa01_lf1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.100 mgmt-ipv4: 172.20.20.100
pa02_lf2_00: pa02_lf2_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.101 mgmt-ipv4: 172.20.20.101
pa03_lf3_00: pa03_lf3_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.102 mgmt-ipv4: 172.20.20.102
pa04_lf4_00: pa04_lf4_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.103 mgmt-ipv4: 172.20.20.103
pa01_sw1_00: pa01_sw1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.110 mgmt-ipv4: 172.20.20.110
pa02_sw1_00: pa02_sw1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.111 mgmt-ipv4: 172.20.20.111
pa03_sw1_00: pa03_sw1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.112 mgmt-ipv4: 172.20.20.112
pa04_sw1_00: pa04_sw1_00:
kind: ceos kind: ceos
image: ceos:4.33.1F image: ceos:4.33.2F
mgmt-ipv4: 172.20.20.113 mgmt-ipv4: 172.20.20.113
host1: host1:
kind: linux kind: linux

View File

@@ -1,4 +1,4 @@
# Network images # Network images
Arista cEOS image can be downlaoded at : arista.com Arista cEOS image can be downlaoded at : arista.com
`cEOS64-lab-4.32.0.1F.tar.xz` `cEOS64-lab-4.33.2F.tar.xz`

View File

@@ -2,11 +2,15 @@
## Table of Contents ## Table of Contents
1. [Installing ContainerLab](#installing-containerlab) - [Installation Guide](#installation-guide)
2. [Installing Docker](#installing-docker) - [Table of Contents](#table-of-contents)
3. [Images Installation](#images-installation) - [Installing ContainerLab](#installing-containerlab)
4. [Install Netbox and plugins](#install-netbox-and-plugins) - [Installing Docker](#installing-docker)
5. [Sources](#sources) - [Images installation](#images-installation)
- [Arista cEOS](#arista-ceos)
- [Nokia SR Linux](#nokia-sr-linux)
- [Install Netbox and plugins](#install-netbox-and-plugins)
- [Sources](#sources)
## Installing ContainerLab ## Installing ContainerLab
@@ -64,7 +68,7 @@ Once you created an account, please logged in and down the cEOS docker images.
To add this new image to docker, please use the docker CLI command : To add this new image to docker, please use the docker CLI command :
```bash ```bash
docker import cEOS64-lab-4.32.0.1F.tar.xz ceos:4.32.0.1F docker import cEOS64-lab-4.33.2F.tar.xz ceos:4.33.2F
``` ```
### Nokia SR Linux ### Nokia SR Linux
@@ -85,8 +89,6 @@ ghcr.io/nokia/srlinux latest 801eb020ad70 11 days ago 2.59GB
## Install Netbox and plugins ## Install Netbox and plugins
For this project, we need to install specific plugin : For this project, we need to install specific plugin :
- [Netbox BGP](https://github.com/netbox-community/netbox-bgp)
- [Netbox Diode](https://github.com/netboxlabs/diode)
- [Netbox Topology Views](https://github.com/netbox-community/netbox-topology-views) - [Netbox Topology Views](https://github.com/netbox-community/netbox-topology-views)
```bash ```bash
@@ -95,8 +97,6 @@ ghcr.io/nokia/srlinux latest 801eb020ad70 11 days ago 2.59GB
touch plugin_requirements.txt Dockerfile-Plugins docker-compose.override.yml touch plugin_requirements.txt Dockerfile-Plugins docker-compose.override.yml
cat <<EOF > plugin_requirements.txt cat <<EOF > plugin_requirements.txt
netbox_topology_views netbox_topology_views
netboxlabs-diode-netbox-plugin
netbox-napalm-plugin
EOF EOF
``` ```
@@ -165,7 +165,7 @@ ghcr.io/nokia/srlinux latest 801eb020ad70 11 days ago 2.59GB
```python ```python
PLUGINS = [ PLUGINS = [
"netbox-topology-views" "netbox_topology_views"
] ]
``` ```

View File

@@ -1,44 +1,157 @@
{# Interface Configuration #} {# Leaf Configuration Template #}
{% for interface in device.interfaces.all() %} {%- for interface in device.interfaces.all() %}
interface {{ interface.name }} interface {{ interface.name }}
{%- if interface.description %} {%- if interface.description %}
description {{ interface.description }} description {{ interface.description }}
{%- endif %}
{%- if interface.name == 'Loopback0' %}
description VTEP
{%- endif %}
{%- if interface.name == 'Ethernet3' %}
switchport mode trunk
no shutdown
mtu 9214
{%- else %}
no shutdown
no switchport
{%- set ip_address = interface.ip_addresses.first() %}
{%- if ip_address %}
ip address {{ ip_address.address }}
{%- endif %}
mtu 9214
{%- endif %} {%- endif %}
no shutdown
no switchport
ip address {{ ipam.IPAddress.objects.get(assigned_object_id=interface.id).address }}
mtu 9214
! !
{% endfor %} {%- endfor %}
{# BGP Configuration #} service routing protocols model multi-agent
{% set loopback_interface = device.interfaces.get(name='Loopback0') %} ip routing
{% set router_id = ipam.IPAddress.objects.get(assigned_object_id=loopback_interface.id).address %}
{# BGP Route-Maps and Prefix Lists #}
{%- set loopback_ip = device.interfaces.get(name='Loopback0').ip_addresses.first().address %}
ip prefix-list VTEP_PREFIX seq 10 permit {{ loopback_ip }}
!
route-map RMAP_VTEP permit 10
match ip address prefix-list VTEP_PREFIX
!
{# Complete BGP Configuration #}
{%- set router_id = loopback_ip.ip %}
router bgp {{ device.custom_field_data.ASN }} router bgp {{ device.custom_field_data.ASN }}
router-id {{ router_id }} router-id {{ router_id }}
maximum-paths 4 ecmp 4 maximum-paths 4 ecmp 4
neighbor SPINE_GROUP peer group !
neighbor SPINE_GROUP allowas-in 1 neighbor SPINE_GROUP peer group
neighbor SPINE_GROUP ebgp-multihop 4 neighbor SPINE_GROUP allowas-in 1
neighbor SPINE_GROUP send-community extended neighbor SPINE_GROUP ebgp-multihop 4
neighbor SPINE_GROUP maximum-routes 12000 neighbor SPINE_GROUP send-community extended
neighbor SPINE_GROUP maximum-routes 12000
!
neighbor VTEP_GROUP peer group
neighbor VTEP_GROUP ebgp-multihop 5
neighbor VTEP_GROUP send-community extended
{%- for interface in device.interfaces.all() %} {%- for interface in device.interfaces.all() %}
{%- if interface.connected_endpoints and interface.name != 'Ethernet3' %} {%- if interface.connected_endpoints and interface.name != 'Ethernet3' and interface.name != 'Loopback0' %}
{%- for remote_interface in interface.connected_endpoints %} {%- set local_ip = interface.ip_addresses.first() %}
{%- set remote_ip = ipam.IPAddress.objects.get(assigned_object_id=remote_interface.id) %} {%- if local_ip %}
neighbor {{ remote_ip.address }} peer group SPINE_GROUP {%- for remote_interface in interface.connected_endpoints %}
neighbor {{ remote_ip.address }} remote-as {{ remote_interface.device.custom_field_data.ASN }} {%- set remote_ip = remote_interface.ip_addresses.first() %}
{%- endfor %} {%- if remote_ip %}
{%- endif %} neighbor {{ remote_ip.address.ip }} peer group SPINE_GROUP
neighbor {{ remote_ip.address.ip }} remote-as {{ remote_interface.device.custom_field_data.ASN }}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- set other_leafs = device.site.devices.filter(role__slug='leaf').exclude(id=device.id) %}
{%- for leaf in other_leafs %}
{%- set leaf_lo = leaf.interfaces.get(name='Loopback0').ip_addresses.first() %}
{%- if leaf_lo %}
neighbor {{ leaf_lo.address.ip }} peer group VTEP_GROUP
neighbor {{ leaf_lo.address.ip }} remote-as {{ leaf.custom_field_data.ASN }}
neighbor {{ leaf_lo.address.ip }} update-source Loopback0
{%- endif %}
{%- endfor %}
!
address-family ipv4
{%- for interface in device.interfaces.all() %}
{%- if interface.connected_endpoints and interface.name != 'Ethernet3' and interface.name != 'Loopback0' %}
{%- set local_ip = interface.ip_addresses.first() %}
{%- if local_ip %}
{%- for remote_interface in interface.connected_endpoints %}
{%- set remote_ip = remote_interface.ip_addresses.first() %}
{%- if remote_ip %}
neighbor {{ remote_ip.address.ip }} activate
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endif %}
{%- endfor %}
redistribute connected route-map RMAP_VTEP
!
address-family evpn
{%- for leaf in other_leafs %}
{%- set leaf_lo = leaf.interfaces.get(name='Loopback0').ip_addresses.first() %}
{%- if leaf_lo %}
neighbor {{ leaf_lo.address.ip }} activate
{%- endif %}
{%- endfor %} {%- endfor %}
! !
address-family ipv4
{%- for interface in device.interfaces.all() %} {# VXLAN Configuration #}
{%- if interface.connected_endpoints and interface.name != 'Ethernet3' %} {%- if device.location and device.location.tenant %}
{%- for remote_interface in interface.connected_endpoints %} {%- set tenant = device.location.tenant %}
{%- set remote_ip = ipam.IPAddress.objects.get(assigned_object_id=remote_interface.id) %} {%- set tenant_vlans = tenant.vlans.all() %}
neighbor {{ remote_ip.address }} activate {%- set tenant_prefix = tenant.prefixes.first() %}
{%- endfor %} {%- set tenant_l2vpns = tenant.l2vpns.filter(type='vxlan-evpn') %}
{%- endif %} {%- set loopback_ip = device.interfaces.get(name='Loopback0').ip_addresses.first().address.ip %}
ip virtual-router mac-address 00:0a:bc:10:11:02
!
{%- for vlan in tenant_vlans %}
vlan {{ vlan.vid }}
name IRB_{{ tenant.name|upper }}_SERVICE
!
interface Vlan{{ vlan.vid }}
description {{ tenant.name }} Service Interface
vrf {{ tenant.name|upper }}
{%- if tenant_prefix %}
{%- set network = tenant_prefix.prefix.network %}
{%- set first_ip = network + 1 %}
ip address virtual {{ first_ip }}/{{ tenant_prefix.prefix.prefixlen }}
{%- endif %}
!
{%- endfor %}
{%- set spine_loopbacks = [] %}
{%- for spine in device.site.devices.filter(role__slug='spine') %}
{%- set spine_lo = spine.interfaces.get(name='Loopback0').ip_addresses.first() %}
{%- if spine_lo %}
{%- set _ = spine_loopbacks.append(spine_lo.address.ip) %}
{%- endif %}
{%- endfor %}
{%- for l2vpn in tenant_l2vpns %}
{%- set vxlan_index = loop.index %}
interface Vxlan{{ vxlan_index }}
description VTI
vxlan source-interface Loopback0
{%- for vlan in tenant_vlans %}
vxlan vlan {{ vlan.vid }} vni {{ l2vpn.identifier }}
{%- for spine_ip in spine_loopbacks %}
vxlan vlan {{ vlan.vid }} flood vtep {{ spine_ip }}
{%- endfor %}
{%- endfor %} {%- endfor %}
! !
{%- endfor %}
router bgp {{ device.custom_field_data.ASN }}
{%- for vlan in tenant_vlans %}
{%- set l2vpn = tenant_l2vpns.first() %}
{%- if l2vpn %}
vlan {{ vlan.vid }}
rd {{ loopback_ip }}:{{ l2vpn.identifier }}
route-target both {{ l2vpn.identifier }}:{{ vlan.vid }}
redistribute learned
{%- endif %}
{%- endfor %}
{%- endif %}

View File

@@ -1,44 +1,50 @@
{# Interface Configuration #} {%- for interface in device.interfaces.all() %}
{% for interface in device.interfaces.all() %}
interface {{ interface.name }} interface {{ interface.name }}
{%- if interface.description %} {%- if interface.description %}
description {{ interface.description }} description {{ interface.description }}
{%- endif %} {%- endif %}
no shutdown no shutdown
no switchport no switchport
ip address {{ ipam.IPAddress.objects.get(assigned_object_id=interface.id).address }} {%- set ip_address = interface.ip_addresses.first() %}
mtu 9214 {%- if ip_address %}
ip address {{ ip_address.address }}
{%- endif %}
mtu 9214
! !
{% endfor %} {%- endfor %}
{# BGP Configuration #} {# BGP Configuration #}
{% set loopback_interface = device.interfaces.get(name='Loopback0') %} ip routing
{% set router_id = ipam.IPAddress.objects.get(assigned_object_id=loopback_interface.id).address %} {%- set loopback_interface = device.interfaces.get(name='Loopback0') %}
{%- set router_id = loopback_interface.ip_addresses.first().address.ip %}
router bgp {{ device.custom_field_data.ASN }} router bgp {{ device.custom_field_data.ASN }}
router-id {{ router_id }} router-id {{ router_id }}
maximum-paths 4 ecmp 4 maximum-paths 4 ecmp 4
neighbor LEAF_GROUP peer group neighbor LEAF_GROUP peer group
neighbor LEAF_GROUP allowas-in 1 neighbor LEAF_GROUP allowas-in 1
neighbor LEAF_GROUP ebgp-multihop 4 neighbor LEAF_GROUP ebgp-multihop 4
neighbor LEAF_GROUP send-community extended neighbor LEAF_GROUP send-community extended
neighbor LEAF_GROUP maximum-routes 12000 neighbor LEAF_GROUP maximum-routes 12000
{%- for interface in device.interfaces.all() %} {%- for interface in device.interfaces.all() %}
{%- if interface.connected_endpoints %} {%- if interface.connected_endpoints %}
{%- for remote_interface in interface.connected_endpoints %} {%- for remote_interface in interface.connected_endpoints %}
{%- set remote_ip = ipam.IPAddress.objects.get(assigned_object_id=remote_interface.id) %} {%- set remote_ip = remote_interface.ip_addresses.first() %}
neighbor {{ remote_ip.address }} peer group LEAF_GROUP {%- if remote_ip %}
neighbor {{ remote_ip.address }} remote-as {{ remote_interface.device.custom_field_data.ASN }} neighbor {{ remote_ip.address.ip }} peer group LEAF_GROUP
{%- endfor %} neighbor {{ remote_ip.address.ip }} remote-as {{ remote_interface.device.custom_field_data.ASN }}
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
! {%- endif %}
address-family ipv4 {%- endfor %}
{%- for interface in device.interfaces.all() %} address-family ipv4
{%- if interface.connected_endpoints %} {%- for interface in device.interfaces.all() %}
{%- for remote_interface in interface.connected_endpoints %} {%- if interface.connected_endpoints %}
{%- set remote_ip = ipam.IPAddress.objects.get(assigned_object_id=remote_interface.id) %} {%- for remote_interface in interface.connected_endpoints %}
neighbor {{ remote_ip.address }} activate {%- set remote_ip = remote_interface.ip_addresses.first() %}
{%- endfor %} {%- if remote_ip %}
{%- endif %} neighbor {{ remote_ip.address.ip }} activate
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endfor %} {%- endfor %}
! !

View File

@@ -1,68 +1,250 @@
from helpers.netbox_backend import NetBoxBackend #!/usr/bin/env python3
"""
add_customer.py
==============
Script pour ajouter un nouveau client dans NetBox avec :
- Création du tenant
- Attribution des locations
- Allocation d'un préfixe /24
- Configuration VXLAN/VLAN
- Attribution des interfaces clients
"""
import sys import sys
import logging
from typing import List, Optional
from dataclasses import dataclass
# Ask user for NetBox connection details from helpers.netbox_backend import NetBoxBackend
url = input("Enter NetBox URL: ")
token = input("Enter NetBox API Token: ")
nb_backend = NetBoxBackend(url, token)
# Ask for customer details # Configuration du logging
customer_name = input("Enter Customer Name: ") logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
vlan_id = int(input("Enter VLAN ID: ")) logger = logging.getLogger(__name__)
vni_id = int(input("Enter VNI ID: "))
# Get available locations @dataclass
locations = list(nb_backend.nb.dcim.locations.all()) class CustomerConfig:
for idx, loc in enumerate(locations): """Configuration du client"""
print(f"{idx}: {loc.name}") name: str
selected_indices = input("Select one or multiple locations by index (comma-separated): ") slug: str
selected_locations = [loc for i, loc in enumerate(locations) if str(i) in selected_indices.split(",")] vlan_id: int
vni_id: int
locations: List[dict]
# Create tenant class CustomerProvisioner:
tenant = nb_backend.create_tenant(customer_name, customer_name.lower().replace(" ", "-")) """Gère la provision d'un nouveau client dans NetBox"""
# Update locations to attach them to the tenant def __init__(self, netbox: NetBoxBackend):
for location in selected_locations: self.netbox = netbox
try: self.config: Optional[CustomerConfig] = None
location.tenant = tenant.id self.tenant: Optional[dict] = None
location.save() self.vlan: Optional[dict] = None
except Exception as e: self.l2vpn: Optional[dict] = None
print(f"[ERROR] Failed to update location {location.name} with tenant: {e}")
# Allocate /24 prefix for customer def get_user_input(self) -> CustomerConfig:
role_id = nb_backend.nb.ipam.roles.get(slug="customerscontainer").id """Récupère les informations du client depuis l'utilisateur"""
parent_prefixes = list(nb_backend.nb.ipam.prefixes.filter(role_id=role_id)) try:
if not parent_prefixes: # Informations de base
print("[ERROR] No available parent prefix found.") customer_name = input("Enter Customer Name: ").strip()
sys.exit(1) if not customer_name:
raise ValueError("Customer name is required")
customer_prefix = nb_backend.allocate_prefix(parent_prefixes[0], 24, None, None) customer_slug = customer_name.lower().replace(" ", "-")
if not customer_prefix:
print("[ERROR] Could not allocate /24 for customer.")
sys.exit(1)
# Create L2VPN # VLAN et VNI
l2vpn_slug = f"{customer_name.lower().replace(' ', '-')}-vpn" vlan_id = int(input("Enter VLAN ID (1-4094): ").strip())
l2vpn = nb_backend.create_l2vpn(vni_id, f"{customer_name}_vpn", l2vpn_slug, tenant.id) if not 1 <= vlan_id <= 4094:
raise ValueError("VLAN ID must be between 1 and 4094")
# Create VLAN vni_id = int(input("Enter VNI ID: ").strip())
vlan_slug = f"{customer_name.lower().replace(' ', '-')}-vlan" if vni_id < 1:
vlan = nb_backend.create_vlan(vlan_id, f"{customer_name}_vlan", vlan_slug, tenant.id) raise ValueError("VNI ID must be positive")
# Create VXLAN termination # Sélection des locations
vxlan_termination = nb_backend.create_vxlan_termination(l2vpn.id, "ipam.vlan", vlan.id) locations = list(self.netbox.nb.dcim.locations.all())
if not locations:
raise ValueError("No locations found in NetBox")
# Assign IP to leaf devices Ethernet3 print("\nAvailable Locations:")
for location in selected_locations: for idx, loc in enumerate(locations):
leaf_devices = nb_backend.nb.dcim.devices.filter(role="leaf", location_id=location.id) print(f"{idx}: {loc.name}")
if leaf_devices:
ip_list = nb_backend.get_available_ips_in_prefix(customer_prefix) indices = input("Select locations (comma-separated indices): ").strip()
if len(ip_list) < len(leaf_devices): selected_locations = [
print("[ERROR] Not enough IP addresses available in the allocated /24.") loc for i, loc in enumerate(locations)
if str(i) in indices.split(",")
]
if not selected_locations:
raise ValueError("At least one location must be selected")
return CustomerConfig(
name=customer_name,
slug=customer_slug,
vlan_id=vlan_id,
vni_id=vni_id,
locations=selected_locations
)
except ValueError as e:
logger.error(f"Invalid input: {str(e)}")
sys.exit(1) sys.exit(1)
for device, ip in zip(leaf_devices, ip_list): def create_tenant(self) -> None:
interface = nb_backend.get_or_create_interface(device.id, "Ethernet3") """Crée le tenant pour le client"""
nb_backend.assign_ip_to_interface(interface, ip.address) self.tenant = self.netbox.create_tenant(
else: self.config.name,
print(f"[ERROR] No leaf devices found in location {location.name}.") self.config.slug
)
if not self.tenant:
raise RuntimeError(f"Failed to create tenant for {self.config.name}")
logger.info(f"Created tenant: {self.tenant.name}")
def assign_locations(self) -> None:
"""Assigne les locations au tenant"""
for location in self.config.locations:
try:
location.tenant = self.tenant.id
location.save()
logger.info(f"Assigned location {location.name} to tenant")
except Exception as e:
logger.error(f"Failed to update location {location.name}: {str(e)}")
def allocate_prefix(self) -> None:
"""Alloue un préfixe /24 pour le client"""
try:
role = self.netbox.nb.ipam.roles.get(slug="customerscontainer")
if not role:
raise ValueError("Customer container role not found")
parent_prefixes = list(self.netbox.nb.ipam.prefixes.filter(role_id=role.id))
if not parent_prefixes:
raise ValueError("No available parent prefix found")
customer_prefix = self.netbox.allocate_prefix(
parent_prefixes[0], 24, None, None, self.tenant.id
)
if not customer_prefix:
raise RuntimeError("Failed to allocate /24 prefix")
logger.info(f"Allocated prefix: {customer_prefix.prefix}")
return customer_prefix
except Exception as e:
logger.error(f"Prefix allocation failed: {str(e)}")
raise
def setup_vxlan(self) -> None:
"""Configure VXLAN et VLAN pour le client"""
try:
# Création du L2VPN
self.l2vpn = self.netbox.create_l2vpn(
self.config.vni_id,
f"{self.config.name}_vpn",
f"{self.config.slug}-vpn",
self.tenant.id
)
if not self.l2vpn:
raise RuntimeError("Failed to create L2VPN")
# Création du VLAN
self.vlan = self.netbox.create_vlan(
self.config.vlan_id,
f"{self.config.name}_vlan",
f"{self.config.slug}-vlan",
self.tenant.id
)
if not self.vlan:
raise RuntimeError("Failed to create VLAN")
# Création de la terminaison VXLAN
vxlan_termination = self.netbox.create_vxlan_termination(
self.l2vpn.id,
"ipam.vlan",
self.vlan.id
)
if not vxlan_termination:
raise RuntimeError("Failed to create VXLAN termination")
logger.info(f"Created VXLAN configuration for {self.config.name}")
except Exception as e:
logger.error(f"VXLAN setup failed: {str(e)}")
raise
def configure_interfaces(self) -> None:
"""Configure les interfaces client sur les leafs"""
for location in self.config.locations:
leaf_devices = self.netbox.nb.dcim.devices.filter(
role="leaf",
location_id=location.id
)
if not leaf_devices:
logger.warning(f"No leaf devices found in location {location.name}")
continue
for device in leaf_devices:
interface = self.netbox.get_or_create_interface(device.id, "Ethernet3")
if not interface:
logger.error(f"Failed to get/create interface for {device.name}")
continue
try:
interface.custom_field_data = {'Customer': self.tenant.id}
interface.save()
logger.info(f"Configured interface on {device.name}")
except Exception as e:
logger.error(f"Failed to configure interface on {device.name}: {str(e)}")
def provision(self) -> None:
"""Processus principal de provision du client"""
try:
# 1. Récupération des informations
self.config = self.get_user_input()
# 2. Création du tenant
self.create_tenant()
# 3. Attribution des locations
self.assign_locations()
# 4. Allocation du préfixe
self.allocate_prefix()
# 5. Configuration VXLAN
self.setup_vxlan()
# 6. Configuration des interfaces
self.configure_interfaces()
logger.info(f"Successfully provisioned customer: {self.config.name}")
except Exception as e:
logger.error(f"Customer provisioning failed: {str(e)}")
sys.exit(1)
def main():
"""Point d'entrée principal"""
try:
# Connexion à NetBox
netbox_url = input("Enter NetBox URL: ").strip()
netbox_token = input("Enter NetBox API Token: ").strip()
if not all([netbox_url, netbox_token]):
raise ValueError("NetBox URL and token are required")
netbox = NetBoxBackend(netbox_url, netbox_token)
if not netbox.check_connection():
raise ConnectionError("Failed to connect to NetBox")
# Provision du client
provisioner = CustomerProvisioner(netbox)
provisioner.provision()
except Exception as e:
logger.error(f"Error: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,35 @@
"""Configuration et constantes pour la création de fabric VXLAN"""
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class InterfaceTypes(Enum):
"""Types d'interfaces supportés"""
QSFPP = "40gbase-x-qsfpp"
SFP = "1000base-x-sfp"
SFP_PLUS = "10gbase-x-sfpp"
VIRTUAL = "virtual"
class DeviceRoles(Enum):
"""Rôles des équipements"""
SPINE = "spine"
LEAF = "leaf"
ACCESS = "access"
class IPRoles(Enum):
"""Rôles des préfixes IP"""
UNDERLAY = "underlaycontainer"
LOOPBACK = "loopbackcontainer"
@dataclass
class FabricConfig:
"""Configuration de la fabric VXLAN"""
site_code: str
num_buildings: int
spine_type: str
leaf_type: str
access_type: str
tenant_id: Optional[int] = None
base_spine_asn: int = 65001
base_leaf_asn: int = 65101

View File

@@ -1,343 +0,0 @@
#!/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,17 @@
"""Exceptions personnalisées pour la création de fabric VXLAN"""
class FabricError(Exception):
"""Erreur de base pour la fabric"""
pass
class DeviceCreationError(FabricError):
"""Erreur lors de la création d'un équipement"""
pass
class IPAllocationError(FabricError):
"""Erreur lors de l'allocation d'adresses IP"""
pass
class CablingError(FabricError):
"""Erreur lors du câblage"""
pass

View File

@@ -0,0 +1,414 @@
"""
Classe principale pour la création de fabric VXLAN dans NetBox
"""
import logging
from typing import Dict, List, Tuple, Optional
from config import FabricConfig, DeviceRoles, IPRoles, InterfaceTypes
from exceptions import DeviceCreationError, IPAllocationError, CablingError, FabricError
from helpers.netbox_backend import NetBoxBackend
logger = logging.getLogger(__name__)
class VXLANFabricCreator:
"""Gère la création d'une fabric VXLAN complète"""
def __init__(self, netbox: NetBoxBackend):
"""
Initialise le créateur de fabric
Args:
netbox: Instance de NetBoxBackend
"""
self.nb = netbox
self.config: Optional[FabricConfig] = None
self._roles: Dict = {}
self._ip_roles: Dict = {}
def validate_prerequisites(self) -> bool:
"""
Valide tous les prérequis nécessaires
Returns:
bool: True si tous les prérequis sont validés
Raises:
DeviceCreationError: Si un rôle d'équipement est manquant
IPAllocationError: Si un rôle IP est manquant
"""
try:
# Validation des rôles d'équipements
for role in DeviceRoles:
role_obj = self.nb.get_device_role(role.value)
if not role_obj:
raise DeviceCreationError(f"Role {role.value} not found")
self._roles[role.value] = role_obj
# Validation des rôles IP
for role in IPRoles:
ip_role = self.nb.nb.ipam.roles.get(slug=role.value)
if not ip_role:
raise IPAllocationError(f"IP role {role.value} not found")
self._ip_roles[role.value] = ip_role
# Validation des types d'équipements
device_types = [
self.config.spine_type,
self.config.leaf_type,
self.config.access_type
]
for dev_type in device_types:
if not self.nb.get_device_type_by_slug(dev_type):
raise DeviceCreationError(f"Device type {dev_type} not found")
return True
except Exception as e:
logger.error(f"Validation failed: {str(e)}")
return False
def get_or_create_site(self) -> dict:
"""
Sélectionne un site existant ou en crée un nouveau
Returns:
dict: Objet site
Raises:
DeviceCreationError: Si la création/sélection échoue
"""
existing_sites = self.nb.get_sites()
if not existing_sites:
raise DeviceCreationError("No sites found in NetBox")
print("\nExisting Sites:")
for idx, site in enumerate(existing_sites, start=1):
print(f" {idx}. {site.name} (slug={site.slug})")
choice = input("Choose site number or 'new': ").strip().lower()
if choice == "new":
name = input("Site name: ").strip()
code = input("Site code: ").strip().upper()
if not all([name, code]):
raise DeviceCreationError("Site name and code required")
return self.nb.create_site(name, code.lower())
try:
return existing_sites[int(choice) - 1]
except (ValueError, IndexError):
raise DeviceCreationError("Invalid site selection")
def create_spines(self, site) -> List[dict]:
"""
Crée les switches spine
Args:
site: Objet site
Returns:
List[dict]: Liste des spines créés
Raises:
DeviceCreationError: Si la création échoue
"""
spines = []
for i in range(1, 3): # Toujours 2 spines
name = f"{self.config.site_code.lower()}dc_sp{i}_00"
spine = self.nb.create_device(
name=name,
device_type_slug=self.config.spine_type,
role_id=self._roles[DeviceRoles.SPINE.value].id,
site_id=site.id
)
if not spine:
raise DeviceCreationError(f"Failed to create spine {name}")
spines.append(spine)
logger.info(f"Created spine: {spine.name}")
return spines
def create_building_pair(self, site, building_num) -> Tuple[dict, dict]:
"""
Crée une paire leaf/access pour un bâtiment
Args:
site: Objet site
building_num: Numéro du bâtiment
Returns:
Tuple[dict, dict]: Paire (leaf, access)
Raises:
DeviceCreationError: Si la création échoue
"""
try:
# Création du location
location_name = f"{self.config.site_code}{building_num}"
location = self.nb.nb.dcim.locations.create(
name=location_name,
slug=location_name.lower(),
site=site.id
)
# Création Leaf
leaf_name = f"{self.config.site_code.lower()}{str(building_num).zfill(2)}_lf1_00"
leaf = self.nb.create_device(
name=leaf_name,
device_type_slug=self.config.leaf_type,
role_id=self._roles[DeviceRoles.LEAF.value].id,
site_id=site.id,
location_id=location.id
)
if not leaf:
raise DeviceCreationError(f"Failed to create leaf {leaf_name}")
# Création Access
access_name = f"{self.config.site_code.lower()}{str(building_num).zfill(2)}_sw1_00"
access = self.nb.create_device(
name=access_name,
device_type_slug=self.config.access_type,
role_id=self._roles[DeviceRoles.ACCESS.value].id,
site_id=site.id,
location_id=location.id
)
if not access:
raise DeviceCreationError(f"Failed to create access switch {access_name}")
logger.info(f"Created leaf/access pair: {leaf_name} / {access_name}")
return leaf, access
except Exception as e:
raise DeviceCreationError(f"Failed to create building pair: {str(e)}")
def setup_cabling(self, leaf, spines, access) -> Tuple[dict, dict, dict, dict]:
"""
Configure le câblage pour un ensemble leaf/spines/access
Topologie:
- Spine1.EthX -> LeafY.Eth1 (où X = numéro du leaf)
- Spine2.EthX -> LeafY.Eth2 (où X = numéro du leaf)
- LeafY.Eth3 -> AccessY.Eth1
Args:
leaf: Objet leaf
spines: Liste des spines
access: Objet access switch
Returns:
Tuple[dict, dict, dict, dict]: Interfaces connectées (leaf_if1, leaf_if2, spine1_if, spine2_if)
Raises:
CablingError: Si le câblage échoue
"""
try:
# Extraire le numéro du leaf (ex: pa01_lf1_00 -> 1)
leaf_number = int(leaf.name.split('_')[0][-2:])
# Leaf -> Spine1 (Leaf.Eth1 -> Spine1.EthX)
leaf_if1 = self.nb.get_or_create_interface(
leaf.id, "Ethernet1", InterfaceTypes.QSFPP.value
)
spine1_if = self.nb.get_or_create_interface(
spines[0].id, f"Ethernet{leaf_number}", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if1, spine1_if):
raise CablingError(f"Failed to cable {leaf_if1.name} to {spine1_if.name}")
logger.info(f"Connected {leaf.name}.Eth1 to {spines[0].name}.Eth{leaf_number}")
# Leaf -> Spine2 (Leaf.Eth2 -> Spine2.EthX)
leaf_if2 = self.nb.get_or_create_interface(
leaf.id, "Ethernet2", InterfaceTypes.QSFPP.value
)
spine2_if = self.nb.get_or_create_interface(
spines[1].id, f"Ethernet{leaf_number}", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if2, spine2_if):
raise CablingError(f"Failed to cable {leaf_if2.name} to {spine2_if.name}")
logger.info(f"Connected {leaf.name}.Eth2 to {spines[1].name}.Eth{leaf_number}")
# Leaf -> Access (Leaf.Eth3 -> Access.Eth1)
leaf_if3 = self.nb.get_or_create_interface(
leaf.id, "Ethernet3", InterfaceTypes.QSFPP.value
)
access_if = self.nb.get_or_create_interface(
access.id, "Ethernet1", InterfaceTypes.QSFPP.value
)
if not self.nb.create_cable_if_not_exists(leaf_if3, access_if):
raise CablingError(f"Failed to cable {leaf_if3.name} to {access_if.name}")
logger.info(f"Connected {leaf.name}.Eth3 to {access.name}.Eth1")
return leaf_if1, leaf_if2, spine1_if, spine2_if
except ValueError as e:
raise CablingError(f"Invalid device naming format: {str(e)}")
except Exception as e:
raise CablingError(f"Cabling failed: {str(e)}")
def setup_ip_addressing(self, site, interfaces, devices) -> None:
"""
Configure l'adressage IP pour les interfaces et les loopbacks
Args:
site: Objet site
interfaces: Liste de tuples d'interfaces à connecter
devices: Liste des équipements nécessitant un loopback
Raises:
IPAllocationError: Si l'allocation IP échoue
"""
try:
# Récupération des prefixes parents
underlay_pfxs = self.nb.nb.ipam.prefixes.filter(
role_id=self._ip_roles[IPRoles.UNDERLAY.value].id,
scope_id=site.id
)
loopback_pfxs = self.nb.nb.ipam.prefixes.filter(
role_id=self._ip_roles[IPRoles.LOOPBACK.value].id,
scope_id=site.id
)
if not underlay_pfxs or not loopback_pfxs:
raise IPAllocationError("Missing required prefix pools")
parent_prefix = list(underlay_pfxs)[0]
loopback_prefix = list(loopback_pfxs)[0]
# Attribution des /31 pour les liens
for if_a, if_b in interfaces:
child_prefix = self.nb.allocate_prefix(
parent_prefix, 31, site.id,
self._ip_roles[IPRoles.UNDERLAY.value].id,
self.config.tenant_id
)
if not child_prefix:
raise IPAllocationError(f"Failed to allocate /31 for {if_a.name}-{if_b.name}")
ips = self.nb.get_available_ips_in_prefix(child_prefix)
if len(ips) < 2:
raise IPAllocationError(f"Not enough IPs in /31 for {if_a.name}-{if_b.name}")
self.nb.assign_ip_to_interface(if_a, ips[0].address)
self.nb.assign_ip_to_interface(if_b, ips[1].address)
logger.info(f"Assigned IPs to {if_a.name}-{if_b.name}")
# Attribution des loopbacks
for device in devices:
# Création de l'interface loopback
loopback = self.nb.get_or_create_interface(
device.id,
"Loopback0",
InterfaceTypes.VIRTUAL.value
)
if not loopback:
raise IPAllocationError(f"Failed to create Loopback0 for {device.name}")
# Allocation du /32
loopback_32 = self.nb.allocate_prefix(
loopback_prefix, 32, site.id,
self._ip_roles[IPRoles.LOOPBACK.value].id,
self.config.tenant_id
)
if not loopback_32:
raise IPAllocationError(f"Failed to allocate /32 for {device.name}")
ips = self.nb.get_available_ips_in_prefix(loopback_32)
if not ips:
raise IPAllocationError(f"No IPs available in /32 for {device.name}")
ip = self.nb.assign_ip_to_interface(loopback, ips[0].address)
if ip:
logger.info(f"Assigned {ip.address} to {device.name} Loopback0")
else:
raise IPAllocationError(f"Failed to assign IP to {device.name} Loopback0")
except Exception as e:
raise IPAllocationError(f"IP addressing failed: {str(e)}")
def assign_asns(self, spines: List[dict], leaves: List[dict]) -> None:
"""
Attribue les ASN aux équipements
Args:
spines: Liste des spines
leaves: Liste des leaves
Raises:
DeviceCreationError: Si l'attribution des ASN échoue
"""
try:
# ASNs pour les spines
for i, spine in enumerate(spines):
asn = self.config.base_spine_asn + i
if not self.nb.save_custom_fields(spine, {"ASN": asn}):
raise DeviceCreationError(f"Failed to assign ASN {asn} to {spine.name}")
logger.info(f"Assigned ASN {asn} to {spine.name}")
# ASNs pour les leaves
for i, leaf in enumerate(leaves):
asn = self.config.base_leaf_asn + i
if not self.nb.save_custom_fields(leaf, {"ASN": asn}):
raise DeviceCreationError(f"Failed to assign ASN {asn} to {leaf.name}")
logger.info(f"Assigned ASN {asn} to {leaf.name}")
except Exception as e:
raise DeviceCreationError(f"ASN assignment failed: {str(e)}")
def create_fabric(self) -> Dict:
"""
Processus principal de création de la fabric
Returns:
Dict: Résultat de la création
Raises:
FabricError: Si la création échoue
"""
try:
if not self.validate_prerequisites():
raise FabricError("Prerequisites validation failed")
# 1. Création/sélection du site
site = self.get_or_create_site()
self.config.site_code = site.name[:2].upper()
# 2. Création des équipements
spines = self.create_spines(site)
leaves = []
access_switches = []
interface_pairs = []
# 3. Création des paires leaf/access par bâtiment
for building in range(1, self.config.num_buildings + 1):
leaf, access = self.create_building_pair(site, building)
leaves.append(leaf)
access_switches.append(access)
# 4. Câblage
interfaces = self.setup_cabling(leaf, spines, access)
interface_pairs.extend([
(interfaces[0], interfaces[2]), # Leaf-Spine1
(interfaces[1], interfaces[3]) # Leaf-Spine2
])
# 5. Configuration IP
self.setup_ip_addressing(
site=site,
interfaces=interface_pairs,
devices=spines + leaves # Loopbacks uniquement pour spines et leaves
)
# 6. Attribution des ASN
self.assign_asns(spines, leaves)
logger.info("Fabric creation completed successfully")
return {
'site': site,
'spines': spines,
'leaves': leaves,
'access_switches': access_switches
}
except Exception as e:
logger.error(f"Fabric creation failed: {str(e)}")
raise FabricError(f"Fabric creation failed: {str(e)}")

View File

@@ -4,223 +4,353 @@ NetBox_backend.py
A Python class to interact with NetBox using pynetbox. A Python class to interact with NetBox using pynetbox.
""" """
import logging
import pynetbox import pynetbox
from typing import Optional, List, Dict from datetime import datetime, timedelta
from enum import Enum
from typing import Optional, List, Dict, Any
from functools import wraps
# Configuration du logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class InterfaceTypes(Enum):
QSFPP = "40gbase-x-qsfpp"
SFP = "1000base-x-sfp"
SFP_PLUS = "10gbase-x-sfpp"
QSFP28 = "100gbase-x-qsfp28"
class DeviceStatus(Enum):
ACTIVE = "active"
PLANNED = "planned"
OFFLINE = "offline"
FAILED = "failed"
def error_handler(func):
"""Décorateur pour la gestion uniforme des erreurs"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"{func.__name__} failed: {str(e)}")
return None
return wrapper
class NetBoxBackend: class NetBoxBackend:
def __init__(self, url: str, token: str, verify_ssl: bool = True): def __init__(self, url: str, token: str, verify_ssl: bool = True):
""" """
Initializes the NetBox API connection. Initializes the NetBox API connection.
Args:
url (str): NetBox instance URL
token (str): API token for authentication
verify_ssl (bool): Whether to verify SSL certificates
""" """
self.url = url self.url = url
self.token = token self.token = token
self.nb = pynetbox.api(self.url, token=self.token) self.nb = pynetbox.api(self.url, token=self.token)
self.nb.http_session.verify = verify_ssl self.nb.http_session.verify = verify_ssl
def validate_input(self, **kwargs) -> bool:
"""
Validates input parameters.
Args:
**kwargs: Key-value pairs to validate
Returns:
bool: True if all inputs are valid
Raises:
ValueError: If any input is invalid
"""
for key, value in kwargs.items():
if value is None or (isinstance(value, str) and not value.strip()):
raise ValueError(f"Invalid {key}: {value}")
return True
def check_connection(self) -> bool:
"""Verifies connection to NetBox instance"""
try:
self.nb.status()
return True
except Exception as e:
logger.error(f"Connection check failed: {e}")
return False
## ---------------------------------- ## ----------------------------------
## TENANTS MANAGEMENT ## TENANTS MANAGEMENT
## ---------------------------------- ## ----------------------------------
@error_handler
def get_tenants(self) -> List: def get_tenants(self) -> List:
""" Returns all tenants in NetBox. """ """Returns all tenants in NetBox."""
try: return list(self.nb.tenancy.tenants.all())
return list(self.nb.tenancy.tenants.all())
except Exception as e:
print(f"[ERROR] Failed to fetch tenants: {e}")
return []
@error_handler
def create_tenant(self, name: str, slug: str): def create_tenant(self, name: str, slug: str):
""" Creates a new tenant in NetBox. """ """
try: Creates a new tenant in NetBox.
return self.nb.tenancy.tenants.create({"name": name, "slug": slug})
except Exception as e: Args:
print(f"[ERROR] Failed to create tenant '{name}': {e}") name (str): Tenant name
return None slug (str): URL-friendly slug
"""
self.validate_input(name=name, slug=slug)
return self.nb.tenancy.tenants.create({"name": name, "slug": slug})
## ---------------------------------- ## ----------------------------------
## SITES MANAGEMENT ## SITES MANAGEMENT
## ---------------------------------- ## ----------------------------------
@error_handler
def get_sites(self) -> List: def get_sites(self) -> List:
""" Returns all sites in NetBox. """ """Returns all sites in NetBox."""
try: return list(self.nb.dcim.sites.all())
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): @error_handler
""" Creates a new site in NetBox. """ def create_site(self, name: str, slug: str, status: str = "active"):
try: """
return self.nb.dcim.sites.create({"name": name, "slug": slug}) Creates a new site in NetBox.
except Exception as e:
print(f"[ERROR] Failed to create site '{name}': {e}") Args:
return None name (str): Site name
slug (str): URL-friendly slug
status (str): Site status
"""
self.validate_input(name=name, slug=slug)
return self.nb.dcim.sites.create({
"name": name,
"slug": slug,
"status": status
})
## ---------------------------------- ## ----------------------------------
## DEVICE MANAGEMENT ## DEVICE MANAGEMENT
## ---------------------------------- ## ----------------------------------
@error_handler
def get_device_type_by_slug(self, slug: str) -> Optional[Dict]: def get_device_type_by_slug(self, slug: str) -> Optional[Dict]:
""" Returns a device type by slug. """ """Returns a device type by slug."""
try: self.validate_input(slug=slug)
return self.nb.dcim.device_types.get(slug=slug) 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
@error_handler
def get_device_role(self, slug: str) -> Optional[Dict]: def get_device_role(self, slug: str) -> Optional[Dict]:
""" Returns a device role by slug. """ """Returns a device role by slug."""
try: self.validate_input(slug=slug)
return self.nb.dcim.device_roles.get(slug=slug) return self.nb.dcim.device_roles.get(slug=slug)
except Exception as e:
print(f"[ERROR] Failed to fetch device role '{slug}': {e}") def device_exists(self, name: str) -> bool:
"""Checks if a device exists by name."""
return bool(self.nb.dcim.devices.get(name=name))
@error_handler
def get_device_by_name(self, name: str) -> Optional[Dict]:
"""Returns a device by name."""
self.validate_input(name=name)
return self.nb.dcim.devices.get(name=name)
@error_handler
def create_device(self, name: str, device_type_slug: str, role_id: int,
site_id: int, location_id: Optional[int] = None,
status: str = DeviceStatus.ACTIVE.value):
"""
Creates a device in NetBox if it doesn't already exist.
Args:
name (str): Device name
device_type_slug (str): Device type slug
role_id (int): Role ID
site_id (int): Site ID
location_id (Optional[int]): Location ID
status (str): Device status
Returns:
Device object if successful, None otherwise
"""
self.validate_input(name=name, device_type_slug=device_type_slug)
if self.device_exists(name):
return self.get_device_by_name(name)
device_type = self.get_device_type_by_slug(device_type_slug)
if not device_type:
logger.error(f"Device type '{device_type_slug}' not found")
return None 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: try:
existing_device = self.nb.dcim.devices.get(name=name) device_data = {
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, "name": name,
"device_type": {"id": device_type.id}, "device_type": device_type.id,
"role": role_id, "role": role_id,
"site": site_id, "site": site_id,
"location": location_id "location": location_id,
}) "status": status
}
# Debug
logger.debug("Creating device with data:")
for key, value in device_data.items():
logger.debug(f"{key}: {value} (type: {type(value)})")
return self.nb.dcim.devices.create(device_data)
except (ValueError, AttributeError) as e:
logger.error(f"Error preparing device data: {str(e)}")
return None
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to create device '{name}': {e}") logger.error(f"Error creating device: {str(e)}")
return None return None
## ---------------------------------- ## ----------------------------------
## INTERFACES & CABLING ## INTERFACES & CABLING
## ---------------------------------- ## ----------------------------------
def get_or_create_interface(self, device_id: int, if_name: str, if_type: str = "40gbase-x-qsfpp"): @error_handler
""" Retrieves or creates an interface on a given device. """ def get_or_create_interface(self, device_id: int, if_name: str,
try: if_type: str = InterfaceTypes.QSFPP.value):
intf = self.nb.dcim.interfaces.get(device_id=device_id, name=if_name) """
if intf: Retrieves or creates an interface on a given device.
return intf
return self.nb.dcim.interfaces.create({ Args:
"device": device_id, device_id (int): Device ID
"name": if_name, if_name (str): Interface name
"type": if_type, if_type (str): Interface type
}) """
except Exception as e: self.validate_input(if_name=if_name)
print(f"[ERROR] Failed to create/get interface '{if_name}': {e}")
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,
})
@error_handler
def create_cable_if_not_exists(self, intf_a, intf_b):
"""
Creates a cable between two interfaces if it doesn't exist.
Args:
intf_a: First interface object
intf_b: Second interface object
"""
if not all([intf_a, intf_b]):
logger.warning("Missing interfaces to create cable")
return None return None
def create_cable_if_not_exists(self, intf_a, intf_b): return self.nb.dcim.cables.create({
""" Creates a cable between two interfaces if it doesn't exist. """ "a_terminations": [{"object_type": "dcim.interface", "object_id": intf_a.id}],
if not intf_a or not intf_b: "b_terminations": [{"object_type": "dcim.interface", "object_id": intf_b.id}],
print("[WARN] Missing interfaces to create cable.") "status": "connected",
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 ## NETWORK MANAGEMENT
## ---------------------------------- ## ----------------------------------
def create_vlan(self, vlan_id: int, vlan_name: str, slug:str, tenant_id: str): @error_handler
""" Creates a VLAN in NetBox. """ def create_vlan(self, vlan_id: int, vlan_name: str, slug: str, tenant_id: str):
try: """Creates a VLAN in NetBox."""
return self.nb.ipam.vlans.create({ self.validate_input(vlan_name=vlan_name, slug=slug)
"vid": vlan_id, return self.nb.ipam.vlans.create({
"name": vlan_name, "vid": vlan_id,
"slug": slug, "name": vlan_name,
"tenant": tenant_id, "slug": slug,
}) "tenant": tenant_id,
except Exception as e: })
print(f"[ERROR] Failed to create VLAN '{vlan_name}': {e}")
return None
@error_handler
def create_l2vpn(self, vni_id: int, vpn_name: str, slug: str, tenant_id: str): def create_l2vpn(self, vni_id: int, vpn_name: str, slug: str, tenant_id: str):
""" Creates an L2VPN in NetBox. """ """Creates an L2VPN in NetBox."""
try: self.validate_input(vpn_name=vpn_name, slug=slug)
return self.nb.vpn.l2vpns.create({ return self.nb.vpn.l2vpns.create({
"name": vpn_name, "name": vpn_name,
"slug": slug, "slug": slug,
"type": "vxlan-evpn", "type": "vxlan-evpn",
"tenant": tenant_id, "tenant": tenant_id,
"identifier": vni_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): @error_handler
""" Creates a VXLAN termination for L2VPN. """ def create_vxlan_termination(self, l2vpn_id: int, assigned_object_type: str,
try: assigned_object_id: int):
return self.nb.vpn.l2vpn_terminations.create({ """Creates a VXLAN termination for L2VPN."""
"l2vpn": l2vpn_id, return self.nb.vpn.l2vpn_terminations.create({
"assigned_object_type": assigned_object_type, "l2vpn": l2vpn_id,
"assigned_object_id": assigned_object_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): @error_handler
def allocate_prefix(self, parent_prefix, prefix_length: int,
site_id: int, role_id: int, tenant_id: int):
""" """
Alloue un sous-réseau enfant (ex: /31 ou /32) à partir d'un préfixe parent Allocates a child subnet from a parent prefix.
via available_prefixes.create().
Args:
parent_prefix: Parent prefix object
prefix_length (int): Length of the child prefix
site_id (int): Site ID
role_id (int): Role ID
tenant_id (int): Tenant ID
""" """
try: return parent_prefix.available_prefixes.create({
child_prefix = parent_prefix.available_prefixes.create({ "prefix_length": prefix_length,
"prefix_length": prefix_length, "site": site_id,
"site": site_id, "role": role_id,
"role": role_id, "tenant": tenant_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"): @error_handler
""" Assigns an IP address to an interface. """ def assign_ip_to_interface(self, interface, ip_address: str,
try: status: str = DeviceStatus.ACTIVE.value):
return self.nb.ipam.ip_addresses.create({ """Assigns an IP address to an interface."""
"address": ip_address, self.validate_input(ip_address=ip_address)
"assigned_object_id": interface.id, return self.nb.ipam.ip_addresses.create({
"assigned_object_type": "dcim.interface", "address": ip_address,
"status": status, "assigned_object_id": interface.id,
}) "assigned_object_type": "dcim.interface",
except Exception as e: "status": status,
print(f"[ERROR] Failed to assign IP {ip_address}: {e}") })
return None
@error_handler
def get_available_ips_in_prefix(self, prefix) -> List: def get_available_ips_in_prefix(self, prefix) -> List:
""" Fetches available IPs within a prefix. """ """Fetches available IPs within a prefix."""
if not hasattr(prefix, "available_ips"): if not hasattr(prefix, "available_ips"):
print(f"[ERROR] Invalid prefix object: {prefix}") logger.error(f"Invalid prefix object: {prefix}")
return [] return []
return list(prefix.available_ips.list()) return list(prefix.available_ips.list())
def save_custom_fields(self, device, fields: Dict[str, any]): @error_handler
""" Saves custom fields for a device. """ def save_custom_fields(self, device, fields: Dict[str, Any]) -> bool:
try: """
for key, value in fields.items(): Saves custom fields for a device.
device.custom_fields[key] = value
device.save() Args:
return True device: Device object
except Exception as e: fields (Dict[str, Any]): Custom fields to save
print(f"[ERROR] Failed to save custom fields: {e}") """
return False for key, value in fields.items():
device.custom_fields[key] = value
device.save()
return True
@error_handler
def cleanup_unused_devices(self, older_than_days: int = 30):
"""
Removes devices that haven't been updated in specified days.
Args:
older_than_days (int): Number of days of inactivity
"""
cutoff = datetime.now() - timedelta(days=older_than_days)
devices = self.nb.dcim.devices.filter(last_updated__lt=cutoff)
for device in devices:
device.delete()

View File

@@ -0,0 +1,117 @@
"""
Script principal pour la création de fabric VXLAN dans NetBox
"""
import sys
import getpass
import logging
from typing import Dict
from config import FabricConfig
from exceptions import FabricError
from fabric_creator import VXLANFabricCreator
from helpers.netbox_backend import NetBoxBackend
# Configuration du logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def get_user_input() -> Dict:
"""
Récupère les entrées utilisateur pour la configuration de la fabric
Returns:
Dict: Dictionnaire contenant les paramètres de configuration
Raises:
FabricError: Si les entrées sont invalides
"""
try:
return {
'netbox_url': input("NetBox URL: ").strip(),
'netbox_token': getpass.getpass("NetBox API Token: "),
'num_buildings': int(input("Number of buildings (1-5): ").strip()),
'spine_type_slug': input("Spine device type slug: ").strip(),
'leaf_type_slug': input("Leaf device type slug: ").strip(),
'access_type_slug': input("Access switch device type slug: ").strip(),
}
except ValueError as error:
raise FabricError(f"Invalid input: {str(error)}")
def validate_input(fabric_config: Dict) -> None:
"""
Valide les entrées utilisateur pour la configuration de la fabric
Args:
fabric_config: Dictionnaire contenant les paramètres de configuration
Raises:
FabricError: Si la validation échoue
"""
# Validation des paramètres de connexion NetBox
if not all([fabric_config['netbox_url'], fabric_config['netbox_token']]):
raise FabricError("NetBox URL and token are required")
# Validation du nombre de bâtiments
if not 1 <= fabric_config['num_buildings'] <= 5:
raise FabricError("Number of buildings must be between 1 and 5")
# Validation des types d'équipements
required_device_types = [
fabric_config['spine_type_slug'],
fabric_config['leaf_type_slug'],
fabric_config['access_type_slug']
]
if not all(required_device_types):
raise FabricError("All device type slugs are required")
def main():
"""Point d'entrée principal du script"""
try:
# 1. Récupération des entrées utilisateur
fabric_config = get_user_input()
validate_input(fabric_config)
# 2. Initialisation de la connexion NetBox
netbox_backend = NetBoxBackend(
fabric_config['netbox_url'],
fabric_config['netbox_token']
)
if not netbox_backend.check_connection():
raise FabricError("Failed to connect to NetBox")
# 3. Configuration de la fabric
fabric_settings = FabricConfig(
site_code="", # Sera défini plus tard
num_buildings=fabric_config['num_buildings'],
spine_type=fabric_config['spine_type_slug'],
leaf_type=fabric_config['leaf_type_slug'],
access_type=fabric_config['access_type_slug']
)
# 4. Création de la fabric
fabric_creator = VXLANFabricCreator(netbox_backend)
fabric_creator.config = fabric_settings
fabric_result = fabric_creator.create_fabric()
# 5. Affichage du résultat
logger.info("=== Fabric Creation Completed ===")
logger.info(f"Site: {fabric_result['site'].name}")
logger.info("Spines: %s", [spine.name for spine in fabric_result['spines']])
logger.info("Leaves: %s", [leaf.name for leaf in fabric_result['leaves']])
logger.info("Access Switches: %s",
[access.name for access in fabric_result['access_switches']])
except FabricError as fabric_error:
logger.error("Fabric creation error: %s", str(fabric_error))
sys.exit(1)
except Exception as unexpected_error:
logger.error("Unexpected error: %s", str(unexpected_error))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -3,7 +3,7 @@ Tenant:
Subnets: 10.100.0.0/24 Subnets: 10.100.0.0/24
VLAN: 100 VLAN: 100
VNI: 1100 VNI: 1100
- Name: Purpel - Name: Purple
Subnets: 10.50.0.0/24 Subnets: 10.50.0.0/24
VLAN: 50 VLAN: 50
VNI: 1050 VNI: 1050

View File

@@ -53,6 +53,32 @@ def get_or_create_device_role(netbox_url, headers, role):
print(f"[INFO] Device Role '{name}' created (ID={created['id']}).") print(f"[INFO] Device Role '{name}' created (ID={created['id']}).")
return created return created
def get_or_create_interface_template(netbox_url, headers, device_type_id, interface_data):
"""
Create interface template for a device type if it doesn't exist
"""
name = interface_data["name"]
url = f"{netbox_url}/api/dcim/interface-templates/?name={name}&devicetype_id={device_type_id}"
resp = requests.get(url, headers=headers)
resp.raise_for_status()
results = resp.json()["results"]
if results:
print(f"[INFO] Interface template '{name}' already exists for device type ID={device_type_id}")
return results[0]
create_url = f"{netbox_url}/api/dcim/interface-templates/"
payload = {
"device_type": device_type_id,
"name": name,
"type": interface_data["type"],
"mgmt_only": interface_data.get("mgmt_only", False)
}
resp = requests.post(create_url, headers=headers, json=payload)
resp.raise_for_status()
created = resp.json()
print(f"[INFO] Interface template '{name}' created for device type ID={device_type_id}")
return created
def get_or_create_device_type(netbox_url, headers, device_type, manufacturers_cache): def get_or_create_device_type(netbox_url, headers, device_type, manufacturers_cache):
manufacturer_slug = device_type["manufacturer"] manufacturer_slug = device_type["manufacturer"]
@@ -91,7 +117,12 @@ def get_or_create_device_type(netbox_url, headers, device_type, manufacturers_ca
print( print(
f"[INFO] Device Type '{created_dt['model']}' created (ID={created_dt['id']})." f"[INFO] Device Type '{created_dt['model']}' created (ID={created_dt['id']})."
) )
# Create interface templates if defined
if "interfaces" in device_type:
for interface in device_type["interfaces"]:
get_or_create_interface_template(netbox_url, headers, created_dt["id"], interface)
return created_dt
######################################## ########################################
# Subnets import # Subnets import
@@ -211,8 +242,19 @@ def create_container_prefix(netbox_url, headers, cidr, description, role_id, sit
# Divers Creation # Divers Creation
######################################## ########################################
def get_or_create_custom_field(netbox_url, headers): def get_or_create_custom_field(netbox_url, headers, field_name, object_types, field_type, label=None, description=None, related_object_type=None):
field_name = "ASN" """
Create a custom field in NetBox.
Args:
netbox_url (str): The NetBox URL
headers (dict): Request headers
field_name (str): Name of the custom field
object_types (list): List of object types (e.g. ['dcim.device'])
field_type (str): Type of field (e.g. 'integer', 'text', 'boolean', 'object')
label (str, optional): Label for the field. Defaults to field_name
description (str, optional): Description of the field. Defaults to field_name
related_object_type (str, optional): Required for object type fields (e.g. 'ipam.vlan')
"""
url = f"{netbox_url}/api/extras/custom-fields/" url = f"{netbox_url}/api/extras/custom-fields/"
# Check if the custom field already exists # Check if the custom field already exists
@@ -223,21 +265,32 @@ def get_or_create_custom_field(netbox_url, headers):
print(f"[INFO] Custom field '{field_name}' already exists.") print(f"[INFO] Custom field '{field_name}' already exists.")
return return
# Set defaults if not provided
label = label or field_name
description = description or field_name
# Define the custom field payload # Define the custom field payload
custom_field_data = { custom_field_data = {
"name": field_name, "name": field_name,
"label": "ASN", "label": label,
"type": "integer", "type": field_type,
"description": "ASN", "description": description,
"required": False, "required": False,
"default": "", "default": "",
"weight": 100, "weight": 100,
"filter_logic": "loose", "filter_logic": "loose",
"ui_visible": "always", "ui_visible": "always",
"is_cloneable": True, "is_cloneable": True,
"object_types": ["dcim.device"], "object_types": object_types,
"related_object_type": related_object_type
} }
# Add related_object_type if field_type is 'object'
if field_type == "object" and related_object_type:
custom_field_data["object_type"] = related_object_type
elif field_type == "object" and not related_object_type:
raise ValueError("related_object_type is required for object type fields")
# Create the custom field # Create the custom field
create_response = requests.post(url, headers=headers, json=custom_field_data) create_response = requests.post(url, headers=headers, json=custom_field_data)
if create_response.status_code == 201: if create_response.status_code == 201:
@@ -277,7 +330,8 @@ def main():
subnets_data = yaml.safe_load(f) subnets_data = yaml.safe_load(f)
# Divers Creation # Divers Creation
get_or_create_custom_field(netbox_url, headers) get_or_create_custom_field(netbox_url, headers, "ASN", ["dcim.device"], "integer", "ASN", "Autonomous System Number")
#get_or_create_custom_field(netbox_url, headers, "Customer", ["dcim.interface"], "object", "Customer", "Customer Name", "tenancy.tenant")
###################################################### ######################################################
# device_model.yml : manufacturers, roles, types # device_model.yml : manufacturers, roles, types
@@ -297,7 +351,9 @@ def main():
if "device_types" in device_model_data: if "device_types" in device_model_data:
for dt in device_model_data["device_types"]: for dt in device_model_data["device_types"]:
get_or_create_device_type(netbox_url, headers, dt, manufacturers_cache) device_type = get_or_create_device_type(netbox_url, headers, dt, manufacturers_cache)
if not device_type:
print(f"[ERROR] Failed to create device type for {dt.get('model', 'unknown')}")
###################################################### ######################################################
# subnets.yml : Region, Site, Containers, etc. # subnets.yml : Region, Site, Containers, etc.