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 (
38+ BGP_AF_L2VPN_EVPN_TAG ,
39+ DEFAULT_SONIC_ROLES ,
40+ DEFAULT_EVPN_SYSTEM_MAC ,
41+ DEFAULT_SAG_MAC ,
42+ )
3843
3944# Global cache for NTP servers to avoid multiple queries
4045_ntp_servers_cache = None
@@ -73,16 +78,7 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
7378 # Get port channel configuration from NetBox first (needed by get_connected_interfaces)
7479 portchannel_info = detect_port_channels (device )
7580
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- )
81+ evpn_system_mac = DEFAULT_EVPN_SYSTEM_MAC
8682
8783 # Get connected interfaces to determine admin_status
8884 connected_interfaces , connected_portchannels = get_connected_interfaces (
@@ -270,7 +266,8 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
270266 config ["MGMT_INTERFACE" ]["eth0" ] = {"admin_status" : "up" }
271267 config ["MGMT_INTERFACE" ][f"eth0|{ oob_ip } /{ prefix_len } " ] = {}
272268 metalbox_ip = _get_metalbox_ip_for_device (device )
273- config ["STATIC_ROUTE" ] = {}
269+ if "STATIC_ROUTE" not in config :
270+ config ["STATIC_ROUTE" ] = {}
274271 config ["STATIC_ROUTE" ]["mgmt|0.0.0.0/0" ] = {"nexthop" : metalbox_ip }
275272 else :
276273 oob_ip = None
@@ -288,7 +285,7 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
288285 _add_portchannel_configuration (config , portchannel_info , evpn_system_mac )
289286
290287 # Add VRF configuration
291- _add_vrf_configuration (config , vrf_info , netbox_interfaces )
288+ _add_vrf_configuration (config , vrf_info , vlan_info , netbox_interfaces )
292289
293290 # Set DATABASE VERSION from config_version parameter or default
294291 if "VERSIONS" not in config :
@@ -1756,7 +1753,11 @@ def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device):
17561753
17571754 if addresses or anycast_addresses :
17581755 # Add the VLAN interface base entry
1759- config ["VLAN_INTERFACE" ][vlan_name ] = {"admin_status" : "up" }
1756+ vlan_iface_entry = {"admin_status" : "up" }
1757+ vrf_name = interface_data .get ("vrf_name" )
1758+ if vrf_name :
1759+ vlan_iface_entry ["vrf_name" ] = vrf_name
1760+ config ["VLAN_INTERFACE" ][vlan_name ] = vlan_iface_entry
17601761
17611762 # Add regular IP configuration for each address (IPv4 and IPv6)
17621763 for address in addresses :
@@ -1786,20 +1787,40 @@ def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device):
17861787 config ["SAG" ][f"{ vlan_name } |IPv6" ] = {"gwip" : ipv6_anycast }
17871788
17881789 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- )
17951790 if "SAG_GLOBAL" not in config :
17961791 config ["SAG_GLOBAL" ] = {}
17971792 config ["SAG_GLOBAL" ]["IP" ] = {
17981793 "IPv4" : "enable" ,
17991794 "IPv6" : "enable" ,
1800- "gwmac" : gwmac ,
1795+ "gwmac" : DEFAULT_SAG_MAC ,
18011796 }
18021797
1798+ # Add static default routes per VRF from sonic_parameters on VLAN interfaces
1799+ for vid , interface_data in vlan_info ["vlan_interfaces" ].items ():
1800+ vrf_name = interface_data .get ("vrf_name" )
1801+ if not vrf_name :
1802+ continue
1803+ logger .debug (f"Adding static default routes for VRF { vrf_name } (Vlan{ vid } )" )
1804+
1805+ default_route_ipv4 = interface_data .get ("default_route_ipv4" )
1806+ default_route_ipv6 = interface_data .get ("default_route_ipv6" )
1807+ if not default_route_ipv4 and not default_route_ipv6 :
1808+ continue
1809+ if "STATIC_ROUTE" not in config :
1810+ config ["STATIC_ROUTE" ] = {}
1811+ if default_route_ipv4 :
1812+ config ["STATIC_ROUTE" ][f"{ vrf_name } |0.0.0.0/0" ] = {
1813+ "nexthop" : default_route_ipv4
1814+ }
1815+ logger .debug (
1816+ f"Added static IPv4 default route for VRF { vrf_name } via { default_route_ipv4 } (Vlan{ vid } )"
1817+ )
1818+ if default_route_ipv6 :
1819+ config ["STATIC_ROUTE" ][f"{ vrf_name } |::/0" ] = {"nexthop" : default_route_ipv6 }
1820+ logger .debug (
1821+ f"Added static IPv6 default route for VRF { vrf_name } via { default_route_ipv6 } (Vlan{ vid } )"
1822+ )
1823+
18031824
18041825def _add_loopback_configuration (config , loopback_info ):
18051826 """Add Loopback configuration from NetBox."""
@@ -1978,12 +1999,13 @@ def _get_vrf_info(device):
19781999 return vrf_info
19792000
19802001
1981- def _add_vrf_configuration (config , vrf_info , netbox_interfaces ):
2002+ def _add_vrf_configuration (config , vrf_info , vlan_info , netbox_interfaces ):
19822003 """Add VRF configuration to config.
19832004
19842005 Args:
19852006 config: Configuration dictionary to update
19862007 vrf_info: VRF information dictionary from _get_vrf_info()
2008+ vlan_info: VLAN information dictionary from get_device_vlans()
19872009 netbox_interfaces: Dict mapping SONiC names to NetBox interface info
19882010 """
19892011 # Track VRFs with VNI for VXLAN configuration
@@ -2070,8 +2092,11 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20702092 config ["BGP_GLOBALS" ][vrf_name ] = copy .deepcopy (default_bgp )
20712093 logger .info (f"Added BGP_GLOBALS for VRF { vrf_name } " )
20722094
2073- # Add VXLAN configuration if there are VRFs with VNI
2074- if vrfs_with_vni :
2095+ # Collect L2 VNI VLANs (tagged evpn-l2vni in NetBox, VNI == VID)
2096+ l2vni_vlans = vlan_info .get ("l2vni_vlans" , {})
2097+
2098+ # Add VXLAN configuration if there are VRFs with VNI or L2 VNI VLANs
2099+ if vrfs_with_vni or l2vni_vlans :
20752100 # Get source IP from BGP_GLOBALS default router_id
20762101 src_ip = config .get ("BGP_GLOBALS" , {}).get ("default" , {}).get ("router_id" , "" )
20772102
@@ -2090,7 +2115,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
20902115 }
20912116 logger .info (f"Added VXLAN_EVPN_NVO nvo1 with source_vtep { VXLAN_VTEP_NAME } " )
20922117
2093- # Add VXLAN_TUNNEL_MAP for each VRF with VNI
2118+ # Add VXLAN_TUNNEL_MAP for each VRF with VNI (L3 / IRB)
20942119 for vrf_entry in vrfs_with_vni :
20952120 vni = vrf_entry ["vni" ]
20962121 vlan_name = f"Vlan{ vni } "
@@ -2101,6 +2126,22 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
21012126 }
21022127 logger .info (f"Added VXLAN_TUNNEL_MAP { map_key } " )
21032128
2129+ # Add VXLAN_TUNNEL_MAP for each L2 VNI VLAN (pure L2, no VRF assignment)
2130+ vrf_vnis = {entry ["vni" ] for entry in vrfs_with_vni }
2131+ for vid , vni in l2vni_vlans .items ():
2132+ if vni in vrf_vnis :
2133+ logger .debug (
2134+ f"Skipping L2 VNI { vni } for Vlan{ vid } : already covered by VRF tunnel map"
2135+ )
2136+ continue
2137+ vlan_name = f"Vlan{ vid } "
2138+ map_key = f"{ VXLAN_VTEP_NAME } |map_{ vni } _{ vlan_name } "
2139+ config ["VXLAN_TUNNEL_MAP" ][map_key ] = {
2140+ "vlan" : vlan_name ,
2141+ "vni" : str (vni ),
2142+ }
2143+ logger .info (f"Added L2 VXLAN_TUNNEL_MAP { map_key } " )
2144+
21042145 # Add VRF assignments to interfaces
21052146 for sonic_interface , vrf_name in vrf_info ["interface_vrf_mapping" ].items ():
21062147 # Check if this is a regular interface
@@ -2119,7 +2160,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces):
21192160 )
21202161
21212162
2122- def _add_portchannel_configuration (config , portchannel_info , evpn_system_mac = None ):
2163+ def _add_portchannel_configuration (config , portchannel_info , evpn_system_mac ):
21232164 """Add port channel configuration from NetBox."""
21242165 if portchannel_info ["portchannels" ]:
21252166 for pc_name , pc_data in portchannel_info ["portchannels" ].items ():
@@ -2134,6 +2175,18 @@ def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac=Non
21342175 pc_config ["system_mac" ] = evpn_system_mac
21352176 config ["PORTCHANNEL" ][pc_name ] = pc_config
21362177
2178+ # Add EVPN_ETHERNET_SEGMENT configuration for EVPN multihoming LAGs
2179+ if pc_data .get ("evpn_lag" ):
2180+ if "EVPN_ETHERNET_SEGMENT" not in config :
2181+ config ["EVPN_ETHERNET_SEGMENT" ] = {}
2182+ config ["EVPN_ETHERNET_SEGMENT" ][pc_name ] = {
2183+ "esi" : "AUTO" ,
2184+ "esi_type" : "TYPE_3_MAC_BASED" ,
2185+ "ifname" : pc_name ,
2186+ }
2187+ if "EVPN_MH_GLOBAL" not in config :
2188+ config ["EVPN_MH_GLOBAL" ] = {"default" : {"startup_delay" : "300" }}
2189+
21372190 # Add PORTCHANNEL_INTERFACE configuration to enable IPv6 link-local
21382191 config ["PORTCHANNEL_INTERFACE" ][pc_name ] = {
21392192 "ipv6_use_link_local_only" : "enable"
0 commit comments