Skip to content

Commit 5b2a5d4

Browse files
authored
EVPN-based multihoming with ESI-LAG on EOS and Junos (#2425)
1 parent e27d05d commit 5b2a5d4

File tree

19 files changed

+734
-10
lines changed

19 files changed

+734
-10
lines changed

docs/plugins.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
plugins/ospf.areas.md
2222
plugins/vrrp.version.md
2323
plugins/firewall.zonebased.md
24+
plugins/evpn.multihoming.md
2425
```
2526

2627
Plugins needed by a topology file are listed in the **plugin** top-level element, for example:

docs/plugins/evpn.multihoming.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
(plugin-evpn-multihoming)=
2+
# EVPN Multihoming (EVPN Ethernet Segment)
3+
4+
This plugin allows for simple EVPN Ethernet Segment configuration, also know as EVPN Multihoming.
5+
6+
For now, the plugin supports:
7+
* ES definition on LAG or "physical" interfaces (depending on platform support)
8+
* Only `all-active` multihoming
9+
* Manual or Auto-Generated (based on LACP) Ethernet Segment Identifier
10+
11+
## Supported Platforms
12+
13+
The plugin includes Jinja2 templates for the following platforms:
14+
15+
| Operating system | ES on LAG (ESI-LAG) | ES on other interfaces | Auto ESI |
16+
| ------------------- | :--: | :--: | :--: |
17+
| Arista EOS | ✅ | ✅ | ✅ |
18+
| Cumulus NVUE | ✅ | ❌ | ❌ |
19+
| vJunos Switch | ✅ | ✅ | ✅ |
20+
21+
22+
## Using the Plugin (auto mode)
23+
24+
* Add `plugin: [ evpn.multihoming ]` to the lab topology.
25+
* Include the **evpn.es** attribute in the device interface
26+
27+
netlab will generate, for each *Ethernet Segment*, ESI value (*Ethernet Segment ID*) and LACP System ID (for *ESI-LAG*).
28+
29+
**NOTE**: Ethernet Segments ID will be generated starting from an integer value, which will be used as the first 5 most significant bytes (excluding the initial `0x00`). This is to:
30+
31+
* be able to generate a 6-bytes LACP System ID starting with `0x02`.
32+
* be able to generate unique *ES-Import* target for each auto generated ESI value.
33+
34+
## Using the Plugin (manual mode)
35+
36+
It is also possible to manually define ESI values for your *Ethernet Segments*. In that case:
37+
38+
* Add `plugin: [ evpn.multihoming ]` to the lab topology.
39+
* Define a set of *ethernet segments* on the topology top-level
40+
* Include the **evpn.es** attribute in the device interface
41+
42+
**NOTE**: EVPN Multihoming, for ESI-LAG, requires that all LAG interfaces belonging to the same *ethernet segment* share the same (unique) LACP System ID. This can be achieved using the `lag.lacp_system_id` attribute - which can accept a "real" mac address value or an integer (*1-65535*) value: in that case it will generate a mac value in the format `02:xx:yy:xx:yy:00` (i.e., `1` will become `02:00:01:00:01:00`).
43+
44+
### Supported attributes
45+
46+
The plugin adds the following attributes defined at topology level:
47+
* **evpn.ethernet_segments** (dict) -- Key is the **ethernet segment** name. Each item is a *dict* with the following attributes:
48+
* **id** (esi_id, mandatory) -- ESI in format `00:XX:XX:XX:XX:XX:XX:XX:XX:XX` (only Type-0 ESI is supported for now)
49+
* **auto** (bool) -- Use ESI auto generation based on LACP System ID. If both `id` and `auto` are specified, explicit `id` takes over.
50+
51+
Interface level attributes:
52+
* **evpn.es** (str) -- ethernet segment name (can be defined on `evpn.ethernet_segments`).
53+
54+
## Example (auto mode)
55+
56+
```
57+
plugin: [ 'evpn.multihoming' ]
58+
59+
bgp.as: 65000
60+
61+
groups:
62+
_auto_create: true
63+
switches:
64+
members: [ s1, s2 ]
65+
module: [ vlan, vxlan, ospf, bgp, evpn, lag ]
66+
probes:
67+
members: [ x1 ]
68+
module: [ lag, vlan ]
69+
device: eos
70+
hosts:
71+
members: [ h1, h2, h3 ]
72+
device: linux
73+
provider: clab
74+
75+
vlans:
76+
red:
77+
mode: bridge
78+
links: [ h1-x1, h2-s1, h3-s2 ]
79+
80+
links:
81+
# EVPN/VXLAN Switch to Switch Link
82+
- s1:
83+
s2:
84+
mtu: 1600
85+
# ESI-LAG
86+
- lag:
87+
members:
88+
- s1:
89+
evpn.es: seg_1
90+
x1:
91+
- s2:
92+
evpn.es: seg_1
93+
x1:
94+
vlan.access: red
95+
```
96+
97+
## Example (manual mode)
98+
99+
```
100+
plugin: [ 'evpn.multihoming' ]
101+
102+
bgp.as: 65000
103+
104+
evpn.ethernet_segments:
105+
seg_1.id: 00:11:22:33:44:55:66:77:88:99
106+
107+
groups:
108+
_auto_create: true
109+
switches:
110+
members: [ s1, s2 ]
111+
module: [ vlan, vxlan, ospf, bgp, evpn, lag ]
112+
probes:
113+
members: [ x1 ]
114+
module: [ lag, vlan ]
115+
device: eos
116+
hosts:
117+
members: [ h1, h2, h3 ]
118+
device: linux
119+
provider: clab
120+
121+
vlans:
122+
red:
123+
mode: bridge
124+
links: [ h1-x1, h2-s1, h3-s2 ]
125+
126+
links:
127+
# EVPN/VXLAN Switch to Switch Link
128+
- s1:
129+
s2:
130+
mtu: 1600
131+
# ESI-LAG
132+
- lag:
133+
members:
134+
- s1:
135+
lag.lacp_system_id: 1
136+
evpn.es: seg_1
137+
x1:
138+
- s2:
139+
lag.lacp_system_id: 1
140+
evpn.es: seg_1
141+
x1:
142+
vlan.access: red
143+
```

netsim/ansible/templates/lag/eos.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ interface {{ intf.ifname }}
4848
description {{ intf.name }}
4949
{% endif %}
5050
!
51+
{% if intf.lag.lacp_system_id is defined %}
52+
lacp system-id {{intf.lag.lacp_system_id}}
53+
{% endif %}
54+
!
5155
{% for ch in interfaces if ch.lag._parentindex|default(None) == intf.lag.ifindex %}
5256
!
5357
{% set _lag_mode =

netsim/ansible/templates/lag/junos.j2

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ interfaces {
2525
passive;
2626
{% endif %}
2727
periodic {{intf.lag.lacp}};
28+
{% if intf.lag.lacp_system_id is defined %}
29+
system-id {{intf.lag.lacp_system_id}};
30+
{% endif %}
2831
}
2932
{% endif %}
30-
3133
}
3234
}
3335

netsim/ansible/templates/lag/linux.j2

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ ip link set dev {{ l.ifname }} up
1919
{% endif %}
2020
{% endfor %}
2121
{% for l in interfaces if 'lag' in l and l.type in ['lag','bond'] %}
22+
{% if l.lag.lacp_system_id is defined %}
23+
echo "{{ l.lag.lacp_system_id }}" > /sys/class/net/{{ l.ifname }}/bonding/ad_actor_system
24+
{% endif %}
2225
ip link set dev {{ l.ifname }} up
2326
{% endfor %}
24-
exit 0

netsim/data/types.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,22 @@ def must_be_mac(value: typing.Any) -> dict:
831831

832832
return { '_valid': True }
833833

834+
@type_test()
835+
def must_be_esi_value(value: typing.Any) -> dict:
836+
# Allow only Type 0 ESI value to be supplied
837+
_value_msg = '10-byte ESI Value in format 00:XX:XX:XX:XX:XX:XX:XX:XX:XX'
838+
839+
if not isinstance(value,str):
840+
return { '_type': '10-byte ESI Value' }
841+
842+
if not value.startswith("00:"):
843+
return { '_value': _value_msg + " - must start with '00:'" }
844+
845+
if not re.match(r"^00(:[0-9a-fA-F]{2}){9}$", value):
846+
return { '_value': _value_msg }
847+
848+
return { '_valid': True }
849+
834850
@type_test()
835851
def must_be_net(value: typing.Any) -> dict:
836852
if not isinstance(value,str):

netsim/devices/eos.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ def passive_stub_interfaces(node: Box, topology: Box) -> None:
101101

102102
intf.ospf.network_type = 'point-to-point'
103103

104+
def esi_identifier_format(node: Box, topology: Box) -> None:
105+
# Arista EOS, in EVPN ESI, wants ESI in format: 0000:0000:0000:0000:0000
106+
for intf in node.interfaces:
107+
if intf.get('evpn._esi.id'):
108+
esi = intf.evpn._esi.id.replace(":", "")
109+
intf.evpn._esi._eos_id = ':'.join(esi[i:i+4] for i in range(0, len(esi), 4))
110+
104111
class EOS(_Quirks):
105112

106113
@classmethod
@@ -118,3 +125,5 @@ def device_quirks(self, node: Box, topology: Box) -> None:
118125
passive_stub_interfaces(node,topology)
119126
if 'eos' in node:
120127
configure_ceos_attributes(node,topology)
128+
if 'evpn.multihoming' in topology.get('plugin',[]):
129+
esi_identifier_format(node,topology)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{% for i in interfaces if i.type == 'lag' and i.evpn._esi is defined %}
2+
{% if loop.first %}
3+
- set:
4+
evpn:
5+
enable: on
6+
multihoming:
7+
enable: on
8+
startup-delay: 1
9+
mac-holdtime: 30
10+
neighbor-holdtime: 30
11+
interface:
12+
{% endif %}
13+
{{ i.ifname }}:
14+
evpn:
15+
multihoming:
16+
segment:
17+
enable: on
18+
mac-address: {{i.lag.lacp_system_id}}
19+
identifier: {{i.evpn._esi.id}}
20+
{% endfor %}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# evpn.multihoming: Support EVPN Multihoming (Ethernet Segments)
2+
#
3+
---
4+
devices:
5+
vjunos-switch:
6+
features.evpn.multihoming:
7+
# support ES definition on LAG interfaces (ESI-LAG)
8+
lag: true
9+
# support ES definition on "standard" ethernet interfaces
10+
interface: true
11+
# ESI Auto value based on LACP System ID
12+
esi_auto: true
13+
modes: [ 'all-active' ] # not used yet
14+
eos:
15+
features.evpn.multihoming:
16+
lag: true
17+
interface: true
18+
esi_auto: true
19+
modes: [ 'all-active' ] # not used yet
20+
cumulus_nvue:
21+
features.evpn.multihoming:
22+
lag: true
23+
interface: false
24+
esi_auto: false
25+
modes: [ 'all-active' ] # not used yet
26+
# for validation
27+
none:
28+
features.evpn.multihoming:
29+
# support ES definition on LAG interfaces (ESI-LAG)
30+
lag: true
31+
# support ES definition on "standard" ethernet interfaces
32+
interface: true
33+
# ESI Auto value based on LACP System ID
34+
esi_auto: true
35+
modes: [ 'all-active' ] # not used yet
36+
37+
38+
# data model:
39+
# -- global: evpn.ethernet_segments
40+
# -- on interface: es: xxx (must be a valid ethernet_segment reference)
41+
evpn:
42+
attributes:
43+
global:
44+
ethernet_segments:
45+
type: dict
46+
_keytype: id
47+
_subtype: _es_item
48+
node:
49+
ethernet_segments: { copy: global }
50+
interface:
51+
es: id
52+
# specific attributes
53+
_es_item:
54+
auto: bool
55+
id: esi_value
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% for intf in interfaces if intf.evpn._esi is defined %}
2+
interface {{ intf.ifname }}
3+
evpn ethernet-segment
4+
{% if intf.evpn._esi.auto|default(false) %}
5+
identifier auto lacp
6+
{% else %}
7+
{# Arista wants it in format: 0000:0000:0000:0000:0000 (generated by device quirk) #}
8+
identifier {{intf.evpn._esi._eos_id}}
9+
{% endif %}
10+
!
11+
{% endfor %}
12+
!
13+
router bgp {{ bgp.as }}
14+
address-family evpn
15+
route type ethernet-segment route-target auto

0 commit comments

Comments
 (0)