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