3434 get_connected_interface_ipv4_address ,
3535)
3636from .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
@@ -288,7 +280,7 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
288280 _add_portchannel_configuration (config , portchannel_info , evpn_system_mac )
289281
290282 # Add VRF configuration
291- _add_vrf_configuration (config , vrf_info , netbox_interfaces )
283+ _add_vrf_configuration (config , vrf_info , vlan_info , netbox_interfaces )
292284
293285 # Set DATABASE VERSION from config_version parameter or default
294286 if "VERSIONS" not in config :
@@ -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
18041820def _add_loopback_configuration (config , loopback_info ):
18051821 """Add Loopback configuration from NetBox."""
@@ -1978,12 +1994,13 @@ def _get_vrf_info(device):
19781994 return vrf_info
19791995
19801996
1981- def _add_vrf_configuration (config , vrf_info , netbox_interfaces ):
1997+ def _add_vrf_configuration (config , vrf_info , vlan_info , netbox_interfaces ):
19821998 """Add VRF configuration to config.
19831999
19842000 Args:
19852001 config: Configuration dictionary to update
19862002 vrf_info: VRF information dictionary from _get_vrf_info()
2003+ vlan_info: VLAN information dictionary from get_device_vlans()
19872004 netbox_interfaces: Dict mapping SONiC names to NetBox interface info
19882005 """
19892006 # Track VRFs with VNI for VXLAN configuration
@@ -2070,8 +2087,11 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20702087 config ["BGP_GLOBALS" ][vrf_name ] = copy .deepcopy (default_bgp )
20712088 logger .info (f"Added BGP_GLOBALS for VRF { vrf_name } " )
20722089
2073- # Add VXLAN configuration if there are VRFs with VNI
2074- if vrfs_with_vni :
2090+ # Collect L2 VNI VLANs (tagged evpn-l2vni in NetBox, VNI == VID)
2091+ l2vni_vlans = vlan_info .get ("l2vni_vlans" , {})
2092+
2093+ # Add VXLAN configuration if there are VRFs with VNI or L2 VNI VLANs
2094+ if vrfs_with_vni or l2vni_vlans :
20752095 # Get source IP from BGP_GLOBALS default router_id
20762096 src_ip = config .get ("BGP_GLOBALS" , {}).get ("default" , {}).get ("router_id" , "" )
20772097
@@ -2090,7 +2110,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20902110 }
20912111 logger .info (f"Added VXLAN_EVPN_NVO nvo1 with source_vtep { VXLAN_VTEP_NAME } " )
20922112
2093- # Add VXLAN_TUNNEL_MAP for each VRF with VNI
2113+ # Add VXLAN_TUNNEL_MAP for each VRF with VNI (L3 / IRB)
20942114 for vrf_entry in vrfs_with_vni :
20952115 vni = vrf_entry ["vni" ]
20962116 vlan_name = f"Vlan{ vni } "
@@ -2101,6 +2121,22 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
21012121 }
21022122 logger .info (f"Added VXLAN_TUNNEL_MAP { map_key } " )
21032123
2124+ # Add VXLAN_TUNNEL_MAP for each L2 VNI VLAN (pure L2, no VRF assignment)
2125+ vrf_vnis = {entry ["vni" ] for entry in vrfs_with_vni }
2126+ for vid , vni in l2vni_vlans .items ():
2127+ if vni in vrf_vnis :
2128+ logger .debug (
2129+ f"Skipping L2 VNI { vni } for Vlan{ vid } : already covered by VRF tunnel map"
2130+ )
2131+ continue
2132+ vlan_name = f"Vlan{ vid } "
2133+ map_key = f"{ VXLAN_VTEP_NAME } |map_{ vni } _{ vlan_name } "
2134+ config ["VXLAN_TUNNEL_MAP" ][map_key ] = {
2135+ "vlan" : vlan_name ,
2136+ "vni" : str (vni ),
2137+ }
2138+ logger .info (f"Added L2 VXLAN_TUNNEL_MAP { map_key } " )
2139+
21042140 # Add VRF assignments to interfaces
21052141 for sonic_interface , vrf_name in vrf_info ["interface_vrf_mapping" ].items ():
21062142 # Check if this is a regular interface
@@ -2119,7 +2155,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
21192155 )
21202156
21212157
2122- def _add_portchannel_configuration (config , portchannel_info , evpn_system_mac = None ):
2158+ def _add_portchannel_configuration (config , portchannel_info , evpn_system_mac ):
21232159 """Add port channel configuration from NetBox."""
21242160 if portchannel_info ["portchannels" ]:
21252161 for pc_name , pc_data in portchannel_info ["portchannels" ].items ():
@@ -2134,6 +2170,18 @@ def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac=Non
21342170 pc_config ["system_mac" ] = evpn_system_mac
21352171 config ["PORTCHANNEL" ][pc_name ] = pc_config
21362172
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+
21372185 # Add PORTCHANNEL_INTERFACE configuration to enable IPv6 link-local
21382186 config ["PORTCHANNEL_INTERFACE" ][pc_name ] = {
21392187 "ipv6_use_link_local_only" : "enable"
0 commit comments