diff --git a/docs/dev/config/vlan.md b/docs/dev/config/vlan.md index 8af8a18fc..1a87012bd 100644 --- a/docs/dev/config/vlan.md +++ b/docs/dev/config/vlan.md @@ -73,7 +73,7 @@ devices: features: vlan: model: router - svi_interface_name: "{ifname}.{vlan}" + svi_interface_name: "vlan{vlan}" subif_name: "{ifname}.{subif_index}" vyos: features: diff --git a/docs/module/evpn.md b/docs/module/evpn.md index 8cd0b013f..a2f699279 100644 --- a/docs/module/evpn.md +++ b/docs/module/evpn.md @@ -23,7 +23,7 @@ The following table describes per-platform support of individual VXLAN features: | Operating system | VXLAN
transport | VLAN-based
service | VLAN Bundle
service | Asymmetric
IRB | Symmetric
IRB | | ------------------ | :-: | :-: | :-: | :-: | :-: | | Arista EOS | ✅ | ✅ | ✅ | ❌ | ✅ | -| Nokia SR Linux | ✅ | ✅ | ❌ | ❌ | ❌ | +| Nokia SR Linux | ✅ | ✅ | ❌ | ❌ | ✅ | | Nokia SR OS | ❌ | ❌ | ❌ | ❌ | ❌ | | FRR | ❌ | ❌ | ❌ | ❌ | ❌ | | VyOS | ✅ | ✅ | ❌ | ❌ | ✅ | @@ -56,7 +56,7 @@ EVPN module supports these default/global/node parameters: * **evpn.session** (global or node parameter): A list of BGP session types on which the EVPN address family is enabled (default: `ibgp`) * **evpn.vlan_bundle_service** (global or node parameter): Use VLAN bundle service for VLANs within a VRF (default: `False`) -* **evpn.start_transit_vni** (system default parameter) -- the first symmetric IRB transit VNI +* **evpn.start_transit_vni** (system default parameter) -- the first symmetric IRB transit VNI, range 4096..16777215 ### VLAN-Based Service Parameters @@ -85,6 +85,6 @@ The default value of VRF EVPN Instance identifier is the VLAN ID of the first VL IRB is configured whenever EVPN-enabled VLANs in a VRF contain IPv4 or IPv6 addresses: * Asymmetric IRB requires no extra parameters[^NS] -* Symmetric IRB needs a transit VNI that has to be set with the **evpn.transit_vni** parameter. That parameter could be set to an integer value or to *True* in which case the EVPN configuration module assigns a VNI to the VRF. +* Symmetric IRB needs a transit VNI that has to be set with the **evpn.transit_vni** parameter. This parameter could be set to an integer value or to *True* in which case the EVPN configuration module auto-assigns a VNI to the VRF. Note that the EVI value used in this case is currently based on the VRF ID (vrfidx) [^NS]: Asymmetric IRB is not supported at the moment diff --git a/netsim/ansible/templates/initial/srlinux.j2 b/netsim/ansible/templates/initial/srlinux.j2 index 1189659b3..9d637d63d 100644 --- a/netsim/ansible/templates/initial/srlinux.j2 +++ b/netsim/ansible/templates/initial/srlinux.j2 @@ -1,26 +1,29 @@ -{% macro ip_addresses(intf,ipv6_ra,is_system) %} +{% macro ip_addresses(name,index,intf,ipv6_ra=True,is_system=False) %} +- path: interface[name={{name}}]/subinterface[index={{index}}] + val: + description: {{ intf.name | default( "No description" )|replace('->','~')|regex_replace('[\\[\\]]','') }} {% if 'ipv4' in intf and intf.ipv4 is string %} - ipv4: - address: - - ip-prefix: "{{ intf.ipv4 }}" + ipv4: + address: + - ip-prefix: "{{ intf.ipv4 }}" {% if not is_system %} - primary: [null] + primary: [null] {% endif %} {% endif %} {% if 'ipv6' in intf %} - ipv6: + ipv6: {% if intf.ipv6 is string %} - address: - - ip-prefix: "{{ intf.ipv6 }}" + address: + - ip-prefix: "{{ intf.ipv6 }}" {% endif %} {% if ipv6_ra %} - neighbor-discovery: - learn-unsolicited: link-local - router-advertisement: - router-role: - admin-state: enable # no ipv6 nd suppress-ra - # min-advertisement-interval: 5 # Leave at platform default 200..600 - # max-advertisement-interval: 5 + neighbor-discovery: + learn-unsolicited: link-local + router-advertisement: + router-role: + admin-state: enable # no ipv6 nd suppress-ra + # min-advertisement-interval: 5 # Leave at platform default 200..600 + # max-advertisement-interval: 5 {% endif %} {% endif %} {% endmacro %} @@ -39,14 +42,13 @@ updates: _annotate_default-ip-mtu: "Custom system wide setting, overrides default 1500" {% endif %} {% endif %} -- path: interface[name=system0]/subinterface[index=0] - val: -{{ ip_addresses(loopback,False,True) }} -{% for l in interfaces|default([]) %} +{{ ip_addresses('system0',0,loopback,False,True) }} + +{% for l in interfaces|default([]) if l.vlan is not defined or l.vlan.mode|default('irb')=='route' %} {% set if_name_index = l.ifname.split('.') %} {% set if_name = if_name_index[0] %} -{% set if_index = if_name_index[1] if if_name_index|length > 1 else l.vlan.access_id|default(0) if l.vlan is defined else '0' %} +{% set if_index = if_name_index[1] if if_name_index|length > 1 else '0' %} {% set vlan = l.vlan.access|default(l.vlan.access_id|default('routed')|string()) if l.vlan is defined else l.ifname %} {% set if_desc = l.name|default( "vlan " + vlan )|replace('->','~')|regex_replace('[\\[\\]]','') %} - path: interface[name={{ if_name }}] @@ -54,29 +56,14 @@ updates: {% if l.mtu is defined %} # min 1500; max 9412 for 7220, 9500 for 7250 platforms mtu {{ [l.mtu + 14,1500]|max }} # TODO not supported on loopback interfaces {% endif %} -{% if l.subif_index is not defined %}{# Skip trunk parent interfaces #} -{% if l.type in ["vlan_member"] %} - vlan-tagging: True -{% endif %} +{% if l.subif_index is defined %}{# Trunk parent interfaces #} + description: "Trunk {{ if_desc }}" +{% else %} subinterface: index: {{ if_index }} description: "{{ if_desc }}" -{% if l.vlan is defined and l.type in ["vlan_member","lan"] %} - type: {{ 'bridged' if l.vlan.access is defined and vlans[ l.vlan.access ].mode|default('irb') in ['bridge','irb'] else 'routed' }} -{% if l.type in ["vlan_member"] %} - vlan: - encap: -{% if l.vlan.access_id is defined %} - single-tagged: - vlan-id: "{{ l.vlan.access_id }}" -{% else %} - untagged: { } -{% endif %} -{% endif %} -{% endif %} -{{ ip_addresses(l,True,False) }} -{% else %} - description: "Trunk {{ if_desc }}" + +{{ ip_addresses(if_name,if_index,l) }} {% endif %} {% endfor %} diff --git a/netsim/ansible/templates/ospf/srlinux.macro.j2 b/netsim/ansible/templates/ospf/srlinux.macro.j2 index 40d704f2b..460856df6 100644 --- a/netsim/ansible/templates/ospf/srlinux.macro.j2 +++ b/netsim/ansible/templates/ospf/srlinux.macro.j2 @@ -22,15 +22,10 @@ - interface-name: system0.0 passive: True {% endif %} -{% for l in vrf_interfaces if (l.vlan is not defined or l.vlan.mode|default('bridge')=='route') and l.subif_index is not defined %} -{% set ifname = l.ifname if '.' in l.ifname else (l.ifname+'.0') %} +{% for l in vrf_interfaces if (l.vlan is not defined or l.vlan.mode|default('irb')!='bridge') and l.subif_index is not defined %} +{% set ifname = l.ifname if '.' in l.ifname else l.ifname|replace('vlan','irb0.') if l.type=='svi' else (l.ifname+'.0') %} {% if 'ospf' not in l %} # OSPF not configured on external interface {{ ifname }} - - area-id: {{ ospf.area }} - interface: - - interface-name: {{ ifname }} - admin-state: disable - _annotate_admin-state: "Disabled: external interface" {% else %} - area-id: {{ l.ospf.area }} interface: diff --git a/netsim/ansible/templates/vlan/srlinux.j2 b/netsim/ansible/templates/vlan/srlinux.j2 index 19350f798..6bd8562fc 100644 --- a/netsim/ansible/templates/vlan/srlinux.j2 +++ b/netsim/ansible/templates/vlan/srlinux.j2 @@ -1,20 +1,61 @@ +{% from "templates/initial/srlinux.j2" import ip_addresses with context %} + updates: {# Create mac-vrfs for L2 VLANs, add IRB interface if any #} {% if vlans is defined %} {% for vname,vdata in vlans.items() if vdata.mode|default('irb') != 'route' %} -{% set irb_ifname = "irb0." + vdata.id | string() %} -- path: network-instance[name=vlan_{{ vname }}] +{# Use only vlan.id in name, such that svi interfaces can be associated #} +- path: network-instance[name=vlan{{ vdata.id }}] val: type: mac-vrf + description: "VLAN {{ vname }}" +{% endfor %} +{% endif %} + +{% macro add_interface(macvrf,ifname,vlan,i) %} +- path: interface[name={{ifname}}] + val: +{% if i.type in ["vlan_member"] %} + vlan-tagging: True +{% endif %} + subinterface: + - index: {{ vlan }} +{% if ifname!="irb0" %} + type: bridged +{% if i.type in ["vlan_member"] %} + vlan: + encap: +{% if i.vlan.access_id is defined %} + single-tagged: + vlan-id: "{{ i.vlan.access_id }}" +{% else %} + untagged: { } +{% endif %} +{% endif %} +{% endif %} + +{{ ip_addresses(ifname,vlan,i) }} + +- path: network-instance[name={{ macvrf }}] + val: interface: -{% for l in interfaces|default([]) %} -{% if l.type in ['lan'] and l.vlan is defined and l.vlan.access == vname %} - - name: {{ l.ifname }}.{{ l.vlan.access_id }} -{% elif l.type in ['vlan_member'] and l.vlan is defined and l.vlan.access|default('?') == vname %} - - name: {{ l.ifname }} -{% elif l.type=='svi' and l.ifname == irb_ifname and (l.vlan is not defined or l.vlan.mode|default('irb') == 'irb') %} - - name: {{ l.ifname }} + - name: {{ ifname }}.{{ vlan }} + +{% if ifname=="irb0" %} +- path: network-instance[name={{ i.vrf|default('default') }}] + val: + interface: + - name: {{ ifname }}.{{ vlan }} +{% endif %} + +{% endmacro %} + +{% for l in interfaces|default([]) if l.vlan is defined %} +{% if l.type in ['svi'] and l.vlan.mode|default('irb') == 'irb' %} +{% set vlan = l.ifname[4:]|int %} +{{ add_interface( l.ifname, "irb0", vlan, l ) }} +{% elif l.type in ['lan','vlan_member'] %} +{% set vlan = l.vlan.access_id %} +{{ add_interface( "vlan" + vlan|string, l.parent_ifname|default(l.ifname), vlan, l ) }} {% endif %} {% endfor %} -{% endfor %} -{% endif %} diff --git a/netsim/ansible/templates/vrf/srlinux.j2 b/netsim/ansible/templates/vrf/srlinux.j2 index 39003fb29..d1ce219d8 100644 --- a/netsim/ansible/templates/vrf/srlinux.j2 +++ b/netsim/ansible/templates/vrf/srlinux.j2 @@ -2,6 +2,11 @@ {% from "templates/bgp/srlinux.macro.j2" import bgp_config with context %} updates: {% for vname,vdata in vrfs.items() %} + +- path: network-instance[name={{vname}}] + val: + type: ip-vrf + {% if 'ospf' in vdata %} {{ ospf_config(0,'ipv4' if vdata.af.ipv4|default(0) else 'ipv6',vname,vdata.ospf,vdata.ospf.interfaces)}} {% endif %} @@ -9,3 +14,12 @@ updates: {{ bgp_config(vname,vrf.as,bgp.router_id,vdata.bgp,vdata) }} {% endif %} {% endfor %} + +# Associate irb interfaces with the corresponding vrf +{% for i in interfaces if i.type=='svi' and 'vrf' in i and i.vlan.mode|default('irb')=='irb' %} +{% set vlan = i.ifname[4:]|int %} +- path: network-instance[name={{ i.vrf }}] + val: + interface: + - name: irb0.{{ vlan }} +{% endfor %} diff --git a/netsim/ansible/templates/vxlan/srlinux.j2 b/netsim/ansible/templates/vxlan/srlinux.j2 index feb475927..c08e55c71 100644 --- a/netsim/ansible/templates/vxlan/srlinux.j2 +++ b/netsim/ansible/templates/vxlan/srlinux.j2 @@ -1,29 +1,44 @@ -updates: -{% if vxlan.vlans is defined %} -{% for vname in vxlan.vlans if vlans[vname].vni is defined %} -{% set vlan = vlans[vname] %} -- path: tunnel-interface[name=vxlan0]/vxlan-interface[index={{vlan.id}}] +{% macro vxlan_interface(vrf,index,type,vni,evi) %} +- path: tunnel-interface[name=vxlan0]/vxlan-interface[index={{index}}] val: - type: bridged + type: {{ type }} ingress: - vni: {{ vlan.vni }} + vni: {{ vni }} egress: source-ip: use-system-ipv4-address -- path: network-instance[name=vlan_{{vname}}] +- path: network-instance[name={{vrf}}] val: - type: mac-vrf + type: {{ 'mac-vrf' if type=='bridged' else 'ip-vrf' }} vxlan-interface: - - name: vxlan0.{{ vlan.id }} + - name: vxlan0.{{ index }} protocols: bgp-vpn: bgp-instance: - id: 1 + route-target: + _annotate: "For compatibility with frr, override auto-derived RT based on EVI {{evi}} with VNI {{vni}}" + import-rt: "target:{{ bgp.as }}:{{ vni }}" + export-rt: "target:{{ bgp.as }}:{{ vni }}" bgp-evpn: bgp-instance: - id: 1 - evi: {{ vlan.evpn.evi }} + evi: {{ evi }} ecmp: 8 - vxlan-interface: vxlan0.{{ vlan.id }} + vxlan-interface: vxlan0.{{ index }} +{% endmacro %} + +updates: +{% if vlans is defined and vxlan.vlans is defined %} +{% for vname in vxlan.vlans if vlans[vname].vni is defined %} +{% set vlan = vlans[vname] %} +{{ vxlan_interface('vlan'+vlan.id|string,vlan.id,'bridged',vlan.vni,vlan.evpn.evi) }} {% endfor %} {% endif %} + +{# Symmetric IRB interfaces, note using VRF ID as transit EVI value #} +{% if vrfs is defined %} +{% for vname,vdata in vrfs.items() if 'evpn' in vdata and 'transit_vni' in vdata.evpn %} +{{ vxlan_interface(vname,vdata.vrfidx,'routed',vdata.evpn.transit_vni,vdata.vrfidx) }} +{% endfor %} +{% endif %} diff --git a/netsim/modules/evpn.py b/netsim/modules/evpn.py index 65f38a442..8c571bbe5 100644 --- a/netsim/modules/evpn.py +++ b/netsim/modules/evpn.py @@ -91,6 +91,7 @@ def vrf_transit_vni(topology: Box) -> None: common.IncorrectValue, 'evpn') continue + vni_list.append( vni ) # Insert it to detect duplicates elsewhere vni_start = topology.defaults.evpn.start_transit_vni for vrf_name,vrf_data in topology.vrfs.items(): # Second pass: set transit VNI values for VRFs with "transit_vni: True" @@ -101,6 +102,8 @@ def vrf_transit_vni(topology: Box) -> None: key='evpn.transit_vni', path=f'vrfs.{vrf_name}', module='evpn', + min_value=4096, # As recommended by Cisco, outside of VLAN range + max_value=16777215, true_value=vni_start) # Make sure evpn.transit_vni is an integer if transit_vni == vni_start: # If we had to assign the default value, increment the default transit VNI vni_start = get_next_vni(vni_start,vni_list) diff --git a/netsim/topology-defaults.yml b/netsim/topology-defaults.yml index 8ce551d27..65e5018f9 100644 --- a/netsim/topology-defaults.yml +++ b/netsim/topology-defaults.yml @@ -162,6 +162,7 @@ vlan: # VLAN support vxlan: # VXLAN support supported_on: [ eos, nxos, vyos, dellos10, srlinux ] requires: [ vlan ] + config_after: [ vrf ] # For platforms that suppport L3 VXLAN, vrfs must be created first domain: global flooding: static attributes: @@ -516,7 +517,7 @@ devices: ipv4: unnumbered: True ipv6: - lla: True + lla: True bgp: activate_af: True ipv6_lla: True @@ -713,7 +714,7 @@ devices: lla: True vlan: model: router - svi_interface_name: "irb0.{vlan}" + svi_interface_name: "vlan{vlan}" # Used as mac-vrf name subif_name: "{ifname}.{vlan.access_id}" mixed_trunk: True bgp: @@ -724,7 +725,9 @@ devices: ipv6_lla: True rfc8950: True vxlan: - requires: [ evpn, vrf ] + requires: [ evpn ] # vrf for l3 vxlan + evpn: + irb: True ospf: unnumbered: False isis: @@ -732,6 +735,9 @@ devices: ipv4: False ipv6: True network: False + vrf: + loopback_interface_name: "lo0.{vrfidx}" + keep_module: True external: image: none graphite.icon: router