Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
plugins/ospf.areas.md
plugins/vrrp.version.md
plugins/firewall.zonebased.md
plugins/evpn.multihoming.md
```

Plugins needed by a topology file are listed in the **plugin** top-level element, for example:
Expand Down
143 changes: 143 additions & 0 deletions docs/plugins/evpn.multihoming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
(plugin-evpn-multihoming)=
# EVPN Multihoming (EVPN Ethernet Segment)

This plugin allows for simple EVPN Ethernet Segment configuration, also know as EVPN Multihoming.

For now, the plugin supports:
* ES definition on LAG or "physical" interfaces (depending on platform support)
* Only `all-active` multihoming
* Manual or Auto-Generated (based on LACP) Ethernet Segment Identifier

## Supported Platforms

The plugin includes Jinja2 templates for the following platforms:

| Operating system | ES on LAG (ESI-LAG) | ES on other interfaces | Auto ESI |
| ------------------- | :--: | :--: | :--: |
| Arista EOS | ✅ | ✅ | ✅ |
| Cumulus NVUE | ✅ | ❌ | ❌ |
| vJunos Switch | ✅ | ✅ | ✅ |


## Using the Plugin (auto mode)

* Add `plugin: [ evpn.multihoming ]` to the lab topology.
* Include the **evpn.es** attribute in the device interface

netlab will generate, for each *Ethernet Segment*, ESI value (*Ethernet Segment ID*) and LACP System ID (for *ESI-LAG*).

**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:

* be able to generate a 6-bytes LACP System ID starting with `0x02`.
* be able to generate unique *ES-Import* target for each auto generated ESI value.

## Using the Plugin (manual mode)

It is also possible to manually define ESI values for your *Ethernet Segments*. In that case:

* Add `plugin: [ evpn.multihoming ]` to the lab topology.
* Define a set of *ethernet segments* on the topology top-level
* Include the **evpn.es** attribute in the device interface

**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`).

### Supported attributes

The plugin adds the following attributes defined at topology level:
* **evpn.ethernet_segments** (dict) -- Key is the **ethernet segment** name. Each item is a *dict* with the following attributes:
* **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)
* **auto** (bool) -- Use ESI auto generation based on LACP System ID. If both `id` and `auto` are specified, explicit `id` takes over.

Interface level attributes:
* **evpn.es** (str) -- ethernet segment name (can be defined on `evpn.ethernet_segments`).

## Example (auto mode)

```
plugin: [ 'evpn.multihoming' ]

bgp.as: 65000

groups:
_auto_create: true
switches:
members: [ s1, s2 ]
module: [ vlan, vxlan, ospf, bgp, evpn, lag ]
probes:
members: [ x1 ]
module: [ lag, vlan ]
device: eos
hosts:
members: [ h1, h2, h3 ]
device: linux
provider: clab

vlans:
red:
mode: bridge
links: [ h1-x1, h2-s1, h3-s2 ]

links:
# EVPN/VXLAN Switch to Switch Link
- s1:
s2:
mtu: 1600
# ESI-LAG
- lag:
members:
- s1:
evpn.es: seg_1
x1:
- s2:
evpn.es: seg_1
x1:
vlan.access: red
```

## Example (manual mode)

```
plugin: [ 'evpn.multihoming' ]

bgp.as: 65000

evpn.ethernet_segments:
seg_1.id: 00:11:22:33:44:55:66:77:88:99

groups:
_auto_create: true
switches:
members: [ s1, s2 ]
module: [ vlan, vxlan, ospf, bgp, evpn, lag ]
probes:
members: [ x1 ]
module: [ lag, vlan ]
device: eos
hosts:
members: [ h1, h2, h3 ]
device: linux
provider: clab

vlans:
red:
mode: bridge
links: [ h1-x1, h2-s1, h3-s2 ]

links:
# EVPN/VXLAN Switch to Switch Link
- s1:
s2:
mtu: 1600
# ESI-LAG
- lag:
members:
- s1:
lag.lacp_system_id: 1
evpn.es: seg_1
x1:
- s2:
lag.lacp_system_id: 1
evpn.es: seg_1
x1:
vlan.access: red
```
4 changes: 4 additions & 0 deletions netsim/ansible/templates/lag/eos.j2
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ interface {{ intf.ifname }}
description {{ intf.name }}
{% endif %}
!
{% if intf.lag.lacp_system_id is defined %}
lacp system-id {{intf.lag.lacp_system_id}}
{% endif %}
!
{% for ch in interfaces if ch.lag._parentindex|default(None) == intf.lag.ifindex %}
!
{% set _lag_mode =
Expand Down
4 changes: 3 additions & 1 deletion netsim/ansible/templates/lag/junos.j2
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ interfaces {
passive;
{% endif %}
periodic {{intf.lag.lacp}};
{% if intf.lag.lacp_system_id is defined %}
system-id {{intf.lag.lacp_system_id}};
{% endif %}
}
{% endif %}

}
}

Expand Down
4 changes: 3 additions & 1 deletion netsim/ansible/templates/lag/linux.j2
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ ip link set dev {{ l.ifname }} up
{% endif %}
{% endfor %}
{% for l in interfaces if 'lag' in l and l.type in ['lag','bond'] %}
{% if l.lag.lacp_system_id is defined %}
echo "{{ l.lag.lacp_system_id }}" > /sys/class/net/{{ l.ifname }}/bonding/ad_actor_system
{% endif %}
ip link set dev {{ l.ifname }} up
{% endfor %}
exit 0
16 changes: 16 additions & 0 deletions netsim/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,22 @@ def must_be_mac(value: typing.Any) -> dict:

return { '_valid': True }

@type_test()
def must_be_esi_value(value: typing.Any) -> dict:
# Allow only Type 0 ESI value to be supplied
_value_msg = '10-byte ESI Value in format 00:XX:XX:XX:XX:XX:XX:XX:XX:XX'

if not isinstance(value,str):
return { '_type': '10-byte ESI Value' }

if not value.startswith("00:"):
return { '_value': _value_msg + " - must start with '00:'" }

if not re.match(r"^00(:[0-9a-fA-F]{2}){9}$", value):
return { '_value': _value_msg }

return { '_valid': True }

@type_test()
def must_be_net(value: typing.Any) -> dict:
if not isinstance(value,str):
Expand Down
9 changes: 9 additions & 0 deletions netsim/devices/eos.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ def passive_stub_interfaces(node: Box, topology: Box) -> None:

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

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

class EOS(_Quirks):

@classmethod
Expand All @@ -118,3 +125,5 @@ def device_quirks(self, node: Box, topology: Box) -> None:
passive_stub_interfaces(node,topology)
if 'eos' in node:
configure_ceos_attributes(node,topology)
if 'evpn.multihoming' in topology.get('plugin',[]):
esi_identifier_format(node,topology)
20 changes: 20 additions & 0 deletions netsim/extra/evpn.multihoming/cumulus_nvue.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% for i in interfaces if i.type == 'lag' and i.evpn._esi is defined %}
{% if loop.first %}
- set:
evpn:
enable: on
multihoming:
enable: on
startup-delay: 1
mac-holdtime: 30
neighbor-holdtime: 30
interface:
{% endif %}
{{ i.ifname }}:
evpn:
multihoming:
segment:
enable: on
mac-address: {{i.lag.lacp_system_id}}
identifier: {{i.evpn._esi.id}}
{% endfor %}
55 changes: 55 additions & 0 deletions netsim/extra/evpn.multihoming/defaults.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# evpn.multihoming: Support EVPN Multihoming (Ethernet Segments)
#
---
devices:
vjunos-switch:
features.evpn.multihoming:
# support ES definition on LAG interfaces (ESI-LAG)
lag: true
# support ES definition on "standard" ethernet interfaces
interface: true
# ESI Auto value based on LACP System ID
esi_auto: true
modes: [ 'all-active' ] # not used yet
eos:
features.evpn.multihoming:
lag: true
interface: true
esi_auto: true
modes: [ 'all-active' ] # not used yet
cumulus_nvue:
features.evpn.multihoming:
lag: true
interface: false
esi_auto: false
modes: [ 'all-active' ] # not used yet
# for validation
none:
features.evpn.multihoming:
# support ES definition on LAG interfaces (ESI-LAG)
lag: true
# support ES definition on "standard" ethernet interfaces
interface: true
# ESI Auto value based on LACP System ID
esi_auto: true
modes: [ 'all-active' ] # not used yet


# data model:
# -- global: evpn.ethernet_segments
# -- on interface: es: xxx (must be a valid ethernet_segment reference)
evpn:
attributes:
global:
ethernet_segments:
type: dict
_keytype: id
_subtype: _es_item
node:
ethernet_segments: { copy: global }
interface:
es: id
# specific attributes
_es_item:
auto: bool
id: esi_value
15 changes: 15 additions & 0 deletions netsim/extra/evpn.multihoming/eos.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% for intf in interfaces if intf.evpn._esi is defined %}
interface {{ intf.ifname }}
evpn ethernet-segment
{% if intf.evpn._esi.auto|default(false) %}
identifier auto lacp
{% else %}
{# Arista wants it in format: 0000:0000:0000:0000:0000 (generated by device quirk) #}
identifier {{intf.evpn._esi._eos_id}}
{% endif %}
!
{% endfor %}
!
router bgp {{ bgp.as }}
address-family evpn
route type ethernet-segment route-target auto
18 changes: 18 additions & 0 deletions netsim/extra/evpn.multihoming/frr.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

{### STILL NOT WORKING - to be better investigated ###}

# ESI-LAG config in FRR
cat >/tmp/esi_lag_config <<CONFIG
evpn mh startup-delay 1
!
{% for intf in interfaces if intf.type == 'lag' and intf.evpn._esi is defined %}
interface {{ intf.ifname }}
evpn mh es-id {{intf.evpn._esi.id}}
!
{% endfor %}
!
do write
CONFIG
vtysh -f /tmp/esi_lag_config
vtysh -c 'clear bgp *'
Loading