Skip to content

Commit a3cea67

Browse files
committed
Add capability for L2 VNIs
AI-assisted: Claude Code Signed-off-by: Freerk-Ole Zakfeld <fzakfeld@scaleuptech.com>
1 parent 84b1c84 commit a3cea67

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

osism/tasks/conductor/netbox.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,18 @@ def get_device_vlans(device):
160160
{
161161
'vlans': {vid: {'name': name, 'description': desc}},
162162
'vlan_members': {vid: {'port_name': 'tagging_mode'}},
163-
'vlan_interfaces': {vid: {'addresses': [ip_with_prefix, ...]}}
163+
'vlan_interfaces': {vid: {'addresses': [ip_with_prefix, ...]}},
164+
'l2vni_vlans': {vid: vni} -- VLANs tagged evpn-l2vni (VNI == VID)
164165
}
165166
"""
166167
from .sonic.cache import get_cached_device_interfaces
168+
from .sonic.constants import EVPN_L2VNI_TAG
167169

168170
vlans = {}
169171
vlan_members = {}
170172
vlan_interfaces = {}
173+
# Map of NetBox VLAN object id -> vid, collected while iterating interfaces
174+
vlan_obj_ids = {}
171175

172176
try:
173177
# Use cached interfaces instead of separate query
@@ -204,6 +208,7 @@ def get_device_vlans(device):
204208
"name": vlan.name or f"Vlan{vid}",
205209
"description": vlan.description or "",
206210
}
211+
vlan_obj_ids[vlan.id] = vid
207212

208213
# Add interface to VLAN members as untagged
209214
if vid not in vlan_members:
@@ -223,6 +228,7 @@ def get_device_vlans(device):
223228
"name": vlan.name or f"Vlan{vid}",
224229
"description": vlan.description or "",
225230
}
231+
vlan_obj_ids[vlan.id] = vid
226232

227233
# Add interface to VLAN members as tagged
228234
if vid not in vlan_members:
@@ -259,13 +265,33 @@ def get_device_vlans(device):
259265
# Skip if interface name doesn't follow Vlan<number> pattern
260266
pass
261267

268+
# Determine which VLANs are tagged evpn-l2vni by fetching full VLAN objects
269+
l2vni_vlans = {}
270+
if vlan_obj_ids:
271+
try:
272+
full_vlans = list(
273+
utils.nb.ipam.vlans.filter(id=list(vlan_obj_ids.keys()))
274+
)
275+
for v in full_vlans:
276+
if any(
277+
getattr(t, "slug", None) == EVPN_L2VNI_TAG
278+
for t in getattr(v, "tags", [])
279+
):
280+
l2vni_vlans[v.vid] = v.vid # VNI equals VID
281+
logger.debug(
282+
f"VLAN {v.vid} tagged {EVPN_L2VNI_TAG}, will add L2 VXLAN_TUNNEL_MAP entry"
283+
)
284+
except Exception as e:
285+
logger.warning(f"Could not fetch VLAN tags for L2 VNI check: {e}")
286+
262287
except Exception as e:
263288
logger.warning(f"Could not get VLANs for device {device.name}: {e}")
264289

265290
return {
266291
"vlans": vlans,
267292
"vlan_members": vlan_members,
268293
"vlan_interfaces": vlan_interfaces,
294+
"l2vni_vlans": l2vni_vlans,
269295
}
270296

271297

osism/tasks/conductor/sonic/config_generator.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
278278
_add_portchannel_configuration(config, portchannel_info, evpn_system_mac)
279279

280280
# Add VRF configuration
281-
_add_vrf_configuration(config, vrf_info, netbox_interfaces)
281+
_add_vrf_configuration(config, vrf_info, vlan_info, netbox_interfaces)
282282

283283
# Set DATABASE VERSION from config_version parameter or default
284284
if "VERSIONS" not in config:
@@ -1927,12 +1927,13 @@ def _get_vrf_info(device):
19271927
return vrf_info
19281928

19291929

1930-
def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
1930+
def _add_vrf_configuration(config, vrf_info, vlan_info, netbox_interfaces):
19311931
"""Add VRF configuration to config.
19321932
19331933
Args:
19341934
config: Configuration dictionary to update
19351935
vrf_info: VRF information dictionary from _get_vrf_info()
1936+
vlan_info: VLAN information dictionary from get_device_vlans()
19361937
netbox_interfaces: Dict mapping SONiC names to NetBox interface info
19371938
"""
19381939
# Track VRFs with VNI for VXLAN configuration
@@ -2019,8 +2020,11 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20192020
config["BGP_GLOBALS"][vrf_name] = copy.deepcopy(default_bgp)
20202021
logger.info(f"Added BGP_GLOBALS for VRF {vrf_name}")
20212022

2022-
# Add VXLAN configuration if there are VRFs with VNI
2023-
if vrfs_with_vni:
2023+
# Collect L2 VNI VLANs (tagged evpn-l2vni in NetBox, VNI == VID)
2024+
l2vni_vlans = vlan_info.get("l2vni_vlans", {})
2025+
2026+
# Add VXLAN configuration if there are VRFs with VNI or L2 VNI VLANs
2027+
if vrfs_with_vni or l2vni_vlans:
20242028
# Get source IP from BGP_GLOBALS default router_id
20252029
src_ip = config.get("BGP_GLOBALS", {}).get("default", {}).get("router_id", "")
20262030

@@ -2039,7 +2043,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20392043
}
20402044
logger.info(f"Added VXLAN_EVPN_NVO nvo1 with source_vtep {VXLAN_VTEP_NAME}")
20412045

2042-
# Add VXLAN_TUNNEL_MAP for each VRF with VNI
2046+
# Add VXLAN_TUNNEL_MAP for each VRF with VNI (L3 / IRB)
20432047
for vrf_entry in vrfs_with_vni:
20442048
vni = vrf_entry["vni"]
20452049
vlan_name = f"Vlan{vni}"
@@ -2050,6 +2054,22 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20502054
}
20512055
logger.info(f"Added VXLAN_TUNNEL_MAP {map_key}")
20522056

2057+
# Add VXLAN_TUNNEL_MAP for each L2 VNI VLAN (pure L2, no VRF assignment)
2058+
vrf_vnis = {entry["vni"] for entry in vrfs_with_vni}
2059+
for vid, vni in l2vni_vlans.items():
2060+
if vni in vrf_vnis:
2061+
logger.debug(
2062+
f"Skipping L2 VNI {vni} for Vlan{vid}: already covered by VRF tunnel map"
2063+
)
2064+
continue
2065+
vlan_name = f"Vlan{vid}"
2066+
map_key = f"{VXLAN_VTEP_NAME}|map_{vni}_{vlan_name}"
2067+
config["VXLAN_TUNNEL_MAP"][map_key] = {
2068+
"vlan": vlan_name,
2069+
"vni": str(vni),
2070+
}
2071+
logger.info(f"Added L2 VXLAN_TUNNEL_MAP {map_key}")
2072+
20532073
# Add VRF assignments to interfaces
20542074
for sonic_interface, vrf_name in vrf_info["interface_vrf_mapping"].items():
20552075
# Check if this is a regular interface

osism/tasks/conductor/sonic/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
# Tag to enable EVPN Multihoming (evpn-lag mode) on a port channel
99
EVPN_LAG_TAG = "evpn-lag"
1010

11+
# Tag to enable L2 VxLAN (EVPN L2 VNI) for a VLAN — VNI equals VLAN ID
12+
EVPN_L2VNI_TAG = "evpn-l2vni"
13+
1114
# Default AS prefix for local ASN calculation
1215
DEFAULT_LOCAL_AS_PREFIX = 4200
1316

0 commit comments

Comments
 (0)