-
Notifications
You must be signed in to change notification settings - Fork 3
EVPN Layer-2 features #2173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
EVPN Layer-2 features #2173
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,12 @@ | |
| get_connected_interface_ipv4_address, | ||
| ) | ||
| from .cache import get_cached_device_interfaces | ||
| from .constants import BGP_AF_L2VPN_EVPN_TAG, DEFAULT_SONIC_ROLES | ||
| from .constants import ( | ||
| BGP_AF_L2VPN_EVPN_TAG, | ||
| DEFAULT_SONIC_ROLES, | ||
| DEFAULT_EVPN_SYSTEM_MAC, | ||
| DEFAULT_SAG_MAC, | ||
| ) | ||
|
|
||
| # Global cache for NTP servers to avoid multiple queries | ||
| _ntp_servers_cache = None | ||
|
|
@@ -73,6 +78,8 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version= | |
| # Get port channel configuration from NetBox first (needed by get_connected_interfaces) | ||
| portchannel_info = detect_port_channels(device) | ||
|
|
||
| evpn_system_mac = DEFAULT_EVPN_SYSTEM_MAC | ||
|
|
||
| # Get connected interfaces to determine admin_status | ||
| connected_interfaces, connected_portchannels = get_connected_interfaces( | ||
| device, portchannel_info | ||
|
|
@@ -259,7 +266,8 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version= | |
| config["MGMT_INTERFACE"]["eth0"] = {"admin_status": "up"} | ||
| config["MGMT_INTERFACE"][f"eth0|{oob_ip}/{prefix_len}"] = {} | ||
| metalbox_ip = _get_metalbox_ip_for_device(device) | ||
| config["STATIC_ROUTE"] = {} | ||
| if "STATIC_ROUTE" not in config: | ||
| config["STATIC_ROUTE"] = {} | ||
| config["STATIC_ROUTE"]["mgmt|0.0.0.0/0"] = {"nexthop": metalbox_ip} | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
| else: | ||
| oob_ip = None | ||
|
|
@@ -274,10 +282,10 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version= | |
| config["BREAKOUT_PORTS"].update(breakout_info["breakout_ports"]) | ||
|
|
||
| # Add port channel configuration | ||
| _add_portchannel_configuration(config, portchannel_info) | ||
| _add_portchannel_configuration(config, portchannel_info, evpn_system_mac) | ||
|
|
||
| # Add VRF configuration | ||
| _add_vrf_configuration(config, vrf_info, netbox_interfaces) | ||
| _add_vrf_configuration(config, vrf_info, vlan_info, netbox_interfaces) | ||
|
|
||
| # Set DATABASE VERSION from config_version parameter or default | ||
| if "VERSIONS" not in config: | ||
|
|
@@ -1736,17 +1744,82 @@ def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device): | |
| member_key = f"{vlan_name}|{sonic_interface_name}" | ||
| config["VLAN_MEMBER"][member_key] = {"tagging_mode": tagging_mode} | ||
|
|
||
| # Add VLAN interfaces (SVIs) | ||
| # Add VLAN interfaces (SVIs) and SAG entries | ||
| sag_enabled = False | ||
| for vid, interface_data in vlan_info["vlan_interfaces"].items(): | ||
| vlan_name = f"Vlan{vid}" | ||
| if "addresses" in interface_data and interface_data["addresses"]: | ||
| # Add the VLAN interface | ||
| config["VLAN_INTERFACE"][vlan_name] = {"admin_status": "up"} | ||
| addresses = interface_data.get("addresses", []) | ||
| anycast_addresses = interface_data.get("anycast_addresses", []) | ||
|
|
||
| if addresses or anycast_addresses: | ||
| # Add the VLAN interface base entry | ||
| vlan_iface_entry = {"admin_status": "up"} | ||
| vrf_name = interface_data.get("vrf_name") | ||
| if vrf_name: | ||
| vlan_iface_entry["vrf_name"] = vrf_name | ||
| config["VLAN_INTERFACE"][vlan_name] = vlan_iface_entry | ||
|
|
||
| # Add regular IP configuration for each address (IPv4 and IPv6) | ||
| for address in addresses: | ||
| ip_key = f"{vlan_name}|{address}" | ||
| config["VLAN_INTERFACE"][ip_key] = {} | ||
|
|
||
| # Add SAG entries for anycast addresses | ||
| if anycast_addresses: | ||
| if "SAG" not in config: | ||
| config["SAG"] = {} | ||
| ipv4_anycast = [] | ||
| ipv6_anycast = [] | ||
| for addr in anycast_addresses: | ||
| try: | ||
| ip_obj = ipaddress.ip_interface(addr) | ||
| if ip_obj.version == 4: | ||
| ipv4_anycast.append(addr) | ||
| elif ip_obj.version == 6: | ||
| ipv6_anycast.append(addr) | ||
| except ValueError: | ||
| logger.warning(f"Invalid anycast IP address format: {addr}") | ||
| if ipv4_anycast: | ||
| sag_enabled = True | ||
| config["SAG"][f"{vlan_name}|IPv4"] = {"gwip": ipv4_anycast} | ||
| if ipv6_anycast: | ||
| sag_enabled = True | ||
| config["SAG"][f"{vlan_name}|IPv6"] = {"gwip": ipv6_anycast} | ||
|
|
||
| if sag_enabled: | ||
| if "SAG_GLOBAL" not in config: | ||
| config["SAG_GLOBAL"] = {} | ||
| config["SAG_GLOBAL"]["IP"] = { | ||
| "IPv4": "enable", | ||
| "IPv6": "enable", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I cannot find this in the Sonic User Guide that I have access to, so I wonder whether this might be a new feature that was only recently introduced? This might imply we should better safeguard this by a version check so we don't mistakenly generate config that an older sonic version cannot process? I also wonder whether we should track ipv4_anycast and ipv6_anycast seperately and only enable the needed AF here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4.5.0 defenitely supports it, probably earlier versions as well, but I can add a safeguard. Makes sense to track IPv4 and IPv6 seperately, I'll change that as well |
||
| "gwmac": DEFAULT_SAG_MAC, | ||
| } | ||
|
|
||
| # Add IP configuration for each address (IPv4 and IPv6) | ||
| for address in interface_data["addresses"]: | ||
| ip_key = f"{vlan_name}|{address}" | ||
| config["VLAN_INTERFACE"][ip_key] = {} | ||
| # Add static default routes per VRF from sonic_parameters on VLAN interfaces | ||
| for vid, interface_data in vlan_info["vlan_interfaces"].items(): | ||
| vrf_name = interface_data.get("vrf_name") | ||
| if not vrf_name: | ||
| continue | ||
| logger.debug(f"Adding static default routes for VRF {vrf_name} (Vlan{vid})") | ||
|
|
||
| default_route_ipv4 = interface_data.get("default_route_ipv4") | ||
| default_route_ipv6 = interface_data.get("default_route_ipv6") | ||
| if not default_route_ipv4 and not default_route_ipv6: | ||
| continue | ||
| if "STATIC_ROUTE" not in config: | ||
| config["STATIC_ROUTE"] = {} | ||
| if default_route_ipv4: | ||
| config["STATIC_ROUTE"][f"{vrf_name}|0.0.0.0/0"] = { | ||
| "nexthop": default_route_ipv4 | ||
| } | ||
| logger.debug( | ||
| f"Added static IPv4 default route for VRF {vrf_name} via {default_route_ipv4} (Vlan{vid})" | ||
| ) | ||
| if default_route_ipv6: | ||
| config["STATIC_ROUTE"][f"{vrf_name}|::/0"] = {"nexthop": default_route_ipv6} | ||
| logger.debug( | ||
| f"Added static IPv6 default route for VRF {vrf_name} via {default_route_ipv6} (Vlan{vid})" | ||
| ) | ||
|
|
||
|
|
||
| def _add_loopback_configuration(config, loopback_info): | ||
|
|
@@ -1926,12 +1999,13 @@ def _get_vrf_info(device): | |
| return vrf_info | ||
|
|
||
|
|
||
| def _add_vrf_configuration(config, vrf_info, netbox_interfaces): | ||
| def _add_vrf_configuration(config, vrf_info, vlan_info, netbox_interfaces): | ||
| """Add VRF configuration to config. | ||
|
|
||
| Args: | ||
| config: Configuration dictionary to update | ||
| vrf_info: VRF information dictionary from _get_vrf_info() | ||
| vlan_info: VLAN information dictionary from get_device_vlans() | ||
| netbox_interfaces: Dict mapping SONiC names to NetBox interface info | ||
| """ | ||
| # Track VRFs with VNI for VXLAN configuration | ||
|
|
@@ -2018,8 +2092,11 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces): | |
| config["BGP_GLOBALS"][vrf_name] = copy.deepcopy(default_bgp) | ||
| logger.info(f"Added BGP_GLOBALS for VRF {vrf_name}") | ||
|
|
||
| # Add VXLAN configuration if there are VRFs with VNI | ||
| if vrfs_with_vni: | ||
| # Collect L2 VNI VLANs (tagged evpn-l2vni in NetBox, VNI == VID) | ||
| l2vni_vlans = vlan_info.get("l2vni_vlans", {}) | ||
|
|
||
| # Add VXLAN configuration if there are VRFs with VNI or L2 VNI VLANs | ||
| if vrfs_with_vni or l2vni_vlans: | ||
| # Get source IP from BGP_GLOBALS default router_id | ||
| src_ip = config.get("BGP_GLOBALS", {}).get("default", {}).get("router_id", "") | ||
|
|
||
|
|
@@ -2038,7 +2115,7 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces): | |
| } | ||
| logger.info(f"Added VXLAN_EVPN_NVO nvo1 with source_vtep {VXLAN_VTEP_NAME}") | ||
|
|
||
| # Add VXLAN_TUNNEL_MAP for each VRF with VNI | ||
| # Add VXLAN_TUNNEL_MAP for each VRF with VNI (L3 / IRB) | ||
| for vrf_entry in vrfs_with_vni: | ||
| vni = vrf_entry["vni"] | ||
| vlan_name = f"Vlan{vni}" | ||
|
|
@@ -2049,6 +2126,22 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces): | |
| } | ||
| logger.info(f"Added VXLAN_TUNNEL_MAP {map_key}") | ||
|
|
||
| # Add VXLAN_TUNNEL_MAP for each L2 VNI VLAN (pure L2, no VRF assignment) | ||
| vrf_vnis = {entry["vni"] for entry in vrfs_with_vni} | ||
| for vid, vni in l2vni_vlans.items(): | ||
| if vni in vrf_vnis: | ||
| logger.debug( | ||
| f"Skipping L2 VNI {vni} for Vlan{vid}: already covered by VRF tunnel map" | ||
| ) | ||
| continue | ||
| vlan_name = f"Vlan{vid}" | ||
| map_key = f"{VXLAN_VTEP_NAME}|map_{vni}_{vlan_name}" | ||
| config["VXLAN_TUNNEL_MAP"][map_key] = { | ||
| "vlan": vlan_name, | ||
| "vni": str(vni), | ||
| } | ||
| logger.info(f"Added L2 VXLAN_TUNNEL_MAP {map_key}") | ||
|
|
||
| # Add VRF assignments to interfaces | ||
| for sonic_interface, vrf_name in vrf_info["interface_vrf_mapping"].items(): | ||
| # Check if this is a regular interface | ||
|
|
@@ -2067,17 +2160,32 @@ def _add_vrf_configuration(config, vrf_info, netbox_interfaces): | |
| ) | ||
|
|
||
|
|
||
| def _add_portchannel_configuration(config, portchannel_info): | ||
| def _add_portchannel_configuration(config, portchannel_info, evpn_system_mac): | ||
| """Add port channel configuration from NetBox.""" | ||
| if portchannel_info["portchannels"]: | ||
| for pc_name, pc_data in portchannel_info["portchannels"].items(): | ||
| # Add PORTCHANNEL configuration | ||
| config["PORTCHANNEL"][pc_name] = { | ||
| pc_config = { | ||
| "admin_status": pc_data["admin_status"], | ||
| "fast_rate": pc_data["fast_rate"], | ||
| "min_links": pc_data["min_links"], | ||
| "mtu": pc_data["mtu"], | ||
| } | ||
| if pc_data.get("evpn_lag") and evpn_system_mac: | ||
| pc_config["system_mac"] = evpn_system_mac | ||
| config["PORTCHANNEL"][pc_name] = pc_config | ||
|
|
||
| # Add EVPN_ETHERNET_SEGMENT configuration for EVPN multihoming LAGs | ||
| if pc_data.get("evpn_lag"): | ||
| if "EVPN_ETHERNET_SEGMENT" not in config: | ||
| config["EVPN_ETHERNET_SEGMENT"] = {} | ||
| config["EVPN_ETHERNET_SEGMENT"][pc_name] = { | ||
| "esi": "AUTO", | ||
| "esi_type": "TYPE_3_MAC_BASED", | ||
| "ifname": pc_name, | ||
| } | ||
| if "EVPN_MH_GLOBAL" not in config: | ||
| config["EVPN_MH_GLOBAL"] = {"default": {"startup_delay": "300"}} | ||
|
|
||
| # Add PORTCHANNEL_INTERFACE configuration to enable IPv6 link-local | ||
| config["PORTCHANNEL_INTERFACE"][pc_name] = { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.