Skip to content

Commit 77ca12b

Browse files
committed
Introduce Layer-2 EVPN Features
AI-assisted: Claude Code Signed-off-by: Freerk-Ole Zakfeld <fzakfeld@scaleuptech.com>
1 parent 68079fa commit 77ca12b

File tree

3 files changed

+89
-25
lines changed

3 files changed

+89
-25
lines changed

osism/tasks/conductor/netbox.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def get_device_vlans(device):
170170
vlans = {}
171171
vlan_members = {}
172172
vlan_interfaces = {}
173+
l2vni_vlans = {}
173174
# Map of NetBox VLAN object id -> vid, collected while iterating interfaces
174175
vlan_obj_ids = {}
175176

@@ -256,9 +257,7 @@ def get_device_vlans(device):
256257
for ip_addr in ip_addresses:
257258
if ip_addr.address:
258259
role = getattr(ip_addr, "role", None)
259-
role_value = (
260-
getattr(role, "value", None) if role else None
261-
)
260+
role_value = getattr(role, "value", None) if role else None
262261
if role_value == "anycast":
263262
anycast_addresses.append(ip_addr.address)
264263
else:
@@ -270,7 +269,36 @@ def get_device_vlans(device):
270269
if addresses:
271270
vlan_interfaces[vid]["addresses"] = addresses
272271
if anycast_addresses:
273-
vlan_interfaces[vid]["anycast_addresses"] = anycast_addresses
272+
vlan_interfaces[vid][
273+
"anycast_addresses"
274+
] = anycast_addresses
275+
if hasattr(interface, "vrf") and interface.vrf:
276+
vlan_interfaces[vid]["vrf_name"] = interface.vrf.name
277+
# Extract default route nexthops from sonic_parameters
278+
279+
sonic_params = (
280+
interface.custom_fields.get("sonic_parameters")
281+
if hasattr(interface, "custom_fields")
282+
and interface.custom_fields
283+
else None
284+
)
285+
286+
if sonic_params:
287+
if (
288+
"default_route_ipv4" in sonic_params
289+
and sonic_params["default_route_ipv4"]
290+
):
291+
vlan_interfaces[vid]["default_route_ipv4"] = (
292+
sonic_params["default_route_ipv4"]
293+
)
294+
if (
295+
"default_route_ipv6" in sonic_params
296+
and sonic_params["default_route_ipv6"]
297+
):
298+
vlan_interfaces[vid]["default_route_ipv6"] = (
299+
sonic_params["default_route_ipv6"]
300+
)
301+
274302
except (ValueError, IndexError):
275303
# Skip if interface name doesn't follow Vlan<number> pattern
276304
pass

osism/tasks/conductor/sonic/config_generator.py

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
get_connected_interface_ipv4_address,
3535
)
3636
from .cache import get_cached_device_interfaces
37-
from .constants import BGP_AF_L2VPN_EVPN_TAG, DEFAULT_SONIC_ROLES
37+
from .constants import BGP_AF_L2VPN_EVPN_TAG, DEFAULT_SONIC_ROLES, DEFAULT_EVPN_SYSTEM_MAC, DEFAULT_SAG_MAC
3838

3939
# Global cache for NTP servers to avoid multiple queries
4040
_ntp_servers_cache = None
@@ -73,16 +73,7 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
7373
# Get port channel configuration from NetBox first (needed by get_connected_interfaces)
7474
portchannel_info = detect_port_channels(device)
7575

76-
# Resolve evpn_system_mac early so it is validated once and passed explicitly later
77-
_raw_evpn_mac = device.config_context.get("_evpn_system_mac")
78-
evpn_system_mac = (
79-
_raw_evpn_mac if isinstance(_raw_evpn_mac, str) and _raw_evpn_mac else None
80-
)
81-
if _raw_evpn_mac and not evpn_system_mac:
82-
logger.warning(
83-
f"Device {device.name}: '_evpn_system_mac' in config_context is not a valid string"
84-
f" (got {type(_raw_evpn_mac).__name__!r}), ignoring"
85-
)
76+
evpn_system_mac = DEFAULT_EVPN_SYSTEM_MAC
8677

8778
# Get connected interfaces to determine admin_status
8879
connected_interfaces, connected_portchannels = get_connected_interfaces(
@@ -270,7 +261,8 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
270261
config["MGMT_INTERFACE"]["eth0"] = {"admin_status": "up"}
271262
config["MGMT_INTERFACE"][f"eth0|{oob_ip}/{prefix_len}"] = {}
272263
metalbox_ip = _get_metalbox_ip_for_device(device)
273-
config["STATIC_ROUTE"] = {}
264+
if "STATIC_ROUTE" not in config:
265+
config["STATIC_ROUTE"] = {}
274266
config["STATIC_ROUTE"]["mgmt|0.0.0.0/0"] = {"nexthop": metalbox_ip}
275267
else:
276268
oob_ip = None
@@ -1756,7 +1748,11 @@ def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device):
17561748

17571749
if addresses or anycast_addresses:
17581750
# Add the VLAN interface base entry
1759-
config["VLAN_INTERFACE"][vlan_name] = {"admin_status": "up"}
1751+
vlan_iface_entry = {"admin_status": "up"}
1752+
vrf_name = interface_data.get("vrf_name")
1753+
if vrf_name:
1754+
vlan_iface_entry["vrf_name"] = vrf_name
1755+
config["VLAN_INTERFACE"][vlan_name] = vlan_iface_entry
17601756

17611757
# Add regular IP configuration for each address (IPv4 and IPv6)
17621758
for address in addresses:
@@ -1786,20 +1782,40 @@ def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device):
17861782
config["SAG"][f"{vlan_name}|IPv6"] = {"gwip": ipv6_anycast}
17871783

17881784
if sag_enabled:
1789-
gwmac = device.config_context.get("_sag_gwmac")
1790-
if not gwmac:
1791-
raise ValueError(
1792-
f"Device {device.name} has SAG anycast addresses but no '_sag_gwmac' "
1793-
"defined in its config context"
1794-
)
17951785
if "SAG_GLOBAL" not in config:
17961786
config["SAG_GLOBAL"] = {}
17971787
config["SAG_GLOBAL"]["IP"] = {
17981788
"IPv4": "enable",
17991789
"IPv6": "enable",
1800-
"gwmac": gwmac,
1790+
"gwmac": DEFAULT_SAG_MAC,
18011791
}
18021792

1793+
# Add static default routes per VRF from sonic_parameters on VLAN interfaces
1794+
for vid, interface_data in vlan_info["vlan_interfaces"].items():
1795+
vrf_name = interface_data.get("vrf_name")
1796+
if not vrf_name:
1797+
continue
1798+
logger.debug(f"Adding static default routes for VRF {vrf_name} (Vlan{vid})")
1799+
1800+
default_route_ipv4 = interface_data.get("default_route_ipv4")
1801+
default_route_ipv6 = interface_data.get("default_route_ipv6")
1802+
if not default_route_ipv4 and not default_route_ipv6:
1803+
continue
1804+
if "STATIC_ROUTE" not in config:
1805+
config["STATIC_ROUTE"] = {}
1806+
if default_route_ipv4:
1807+
config["STATIC_ROUTE"][f"{vrf_name}|0.0.0.0/0"] = {
1808+
"nexthop": default_route_ipv4
1809+
}
1810+
logger.debug(
1811+
f"Added static IPv4 default route for VRF {vrf_name} via {default_route_ipv4} (Vlan{vid})"
1812+
)
1813+
if default_route_ipv6:
1814+
config["STATIC_ROUTE"][f"{vrf_name}|::/0"] = {"nexthop": default_route_ipv6}
1815+
logger.debug(
1816+
f"Added static IPv6 default route for VRF {vrf_name} via {default_route_ipv6} (Vlan{vid})"
1817+
)
1818+
18031819

18041820
def _add_loopback_configuration(config, loopback_info):
18051821
"""Add Loopback configuration from NetBox."""
@@ -2139,7 +2155,7 @@ def _add_vrf_configuration(config, vrf_info, vlan_info, netbox_interfaces):
21392155
)
21402156

21412157

2142-
def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac=None):
2158+
def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac):
21432159
"""Add port channel configuration from NetBox."""
21442160
if portchannel_info["portchannels"]:
21452161
for pc_name, pc_data in portchannel_info["portchannels"].items():
@@ -2154,6 +2170,18 @@ def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac=Non
21542170
pc_config["system_mac"] = evpn_system_mac
21552171
config["PORTCHANNEL"][pc_name] = pc_config
21562172

2173+
# Add EVPN_ETHERNET_SEGMENT configuration for EVPN multihoming LAGs
2174+
if pc_data.get("evpn_lag"):
2175+
if "EVPN_ETHERNET_SEGMENT" not in config:
2176+
config["EVPN_ETHERNET_SEGMENT"] = {}
2177+
config["EVPN_ETHERNET_SEGMENT"][pc_name] = {
2178+
"esi": "AUTO",
2179+
"esi_type": "TYPE_3_MAC_BASED",
2180+
"ifname": pc_name,
2181+
}
2182+
if "EVPN_MH_GLOBAL" not in config:
2183+
config["EVPN_MH_GLOBAL"] = {"default": {"startup_delay": "300"}}
2184+
21572185
# Add PORTCHANNEL_INTERFACE configuration to enable IPv6 link-local
21582186
config["PORTCHANNEL_INTERFACE"][pc_name] = {
21592187
"ipv6_use_link_local_only": "enable"

osism/tasks/conductor/sonic/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
# Default AS prefix for local ASN calculation
1515
DEFAULT_LOCAL_AS_PREFIX = 4200
1616

17+
# Default Base MAC for EVPN PortChannels (Calculated with each PortChannel Index)
18+
# using a locally administered MAC adress range
19+
DEFAULT_EVPN_SYSTEM_MAC = "02:00:00:00:00:00"
20+
21+
# Default MAC for Static Anycast Gateway (L2 Anycast Gateway)
22+
# using a locally administered MAC adress range
23+
DEFAULT_SAG_MAC = "02:00:10:00:00:00"
24+
1725
# Default SONiC device roles
1826
DEFAULT_SONIC_ROLES = [
1927
"accessleaf",

0 commit comments

Comments
 (0)