summaryrefslogtreecommitdiffstats
path: root/tests/topotests/bgp_evpn_mh
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/topotests/bgp_evpn_mh/evpn-mh-topo-tests.pdfbin0 -> 90963 bytes
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd11/evpn.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd11/pim.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd11/zebra.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd12/evpn.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd12/pim.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd12/zebra.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd21/evpn.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd21/pim.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd21/zebra.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd22/evpn.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd22/pim.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/hostd22/zebra.conf0
-rw-r--r--tests/topotests/bgp_evpn_mh/spine1/evpn.conf17
-rw-r--r--tests/topotests/bgp_evpn_mh/spine1/pim.conf18
-rw-r--r--tests/topotests/bgp_evpn_mh/spine1/zebra.conf15
-rw-r--r--tests/topotests/bgp_evpn_mh/spine2/evpn.conf17
-rw-r--r--tests/topotests/bgp_evpn_mh/spine2/pim.conf18
-rw-r--r--tests/topotests/bgp_evpn_mh/spine2/zebra.conf15
-rw-r--r--tests/topotests/bgp_evpn_mh/test_evpn_mh.py809
-rw-r--r--tests/topotests/bgp_evpn_mh/torm11/evpn.conf21
-rw-r--r--tests/topotests/bgp_evpn_mh/torm11/pim.conf13
-rw-r--r--tests/topotests/bgp_evpn_mh/torm11/zebra.conf27
-rw-r--r--tests/topotests/bgp_evpn_mh/torm12/evpn.conf21
-rw-r--r--tests/topotests/bgp_evpn_mh/torm12/pim.conf13
-rw-r--r--tests/topotests/bgp_evpn_mh/torm12/zebra.conf28
-rw-r--r--tests/topotests/bgp_evpn_mh/torm21/evpn.conf21
-rw-r--r--tests/topotests/bgp_evpn_mh/torm21/pim.conf13
-rw-r--r--tests/topotests/bgp_evpn_mh/torm21/zebra.conf29
-rw-r--r--tests/topotests/bgp_evpn_mh/torm22/evpn.conf20
-rw-r--r--tests/topotests/bgp_evpn_mh/torm22/pim.conf13
-rw-r--r--tests/topotests/bgp_evpn_mh/torm22/zebra.conf29
32 files changed, 1157 insertions, 0 deletions
diff --git a/tests/topotests/bgp_evpn_mh/evpn-mh-topo-tests.pdf b/tests/topotests/bgp_evpn_mh/evpn-mh-topo-tests.pdf
new file mode 100644
index 0000000..8858e21
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/evpn-mh-topo-tests.pdf
Binary files differ
diff --git a/tests/topotests/bgp_evpn_mh/hostd11/evpn.conf b/tests/topotests/bgp_evpn_mh/hostd11/evpn.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd11/evpn.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd11/pim.conf b/tests/topotests/bgp_evpn_mh/hostd11/pim.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd11/pim.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd11/zebra.conf b/tests/topotests/bgp_evpn_mh/hostd11/zebra.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd11/zebra.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd12/evpn.conf b/tests/topotests/bgp_evpn_mh/hostd12/evpn.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd12/evpn.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd12/pim.conf b/tests/topotests/bgp_evpn_mh/hostd12/pim.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd12/pim.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd12/zebra.conf b/tests/topotests/bgp_evpn_mh/hostd12/zebra.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd12/zebra.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd21/evpn.conf b/tests/topotests/bgp_evpn_mh/hostd21/evpn.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd21/evpn.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd21/pim.conf b/tests/topotests/bgp_evpn_mh/hostd21/pim.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd21/pim.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd21/zebra.conf b/tests/topotests/bgp_evpn_mh/hostd21/zebra.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd21/zebra.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd22/evpn.conf b/tests/topotests/bgp_evpn_mh/hostd22/evpn.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd22/evpn.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd22/pim.conf b/tests/topotests/bgp_evpn_mh/hostd22/pim.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd22/pim.conf
diff --git a/tests/topotests/bgp_evpn_mh/hostd22/zebra.conf b/tests/topotests/bgp_evpn_mh/hostd22/zebra.conf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/hostd22/zebra.conf
diff --git a/tests/topotests/bgp_evpn_mh/spine1/evpn.conf b/tests/topotests/bgp_evpn_mh/spine1/evpn.conf
new file mode 100644
index 0000000..2e26f60
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine1/evpn.conf
@@ -0,0 +1,17 @@
+frr defaults datacenter
+!
+router bgp 65001
+ bgp router-id 192.168.100.13
+ no bgp ebgp-requires-policy
+ neighbor 192.168.1.2 remote-as external
+ neighbor 192.168.2.2 remote-as external
+ neighbor 192.168.3.2 remote-as external
+ neighbor 192.168.4.2 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.1.2 activate
+ neighbor 192.168.2.2 activate
+ neighbor 192.168.3.2 activate
+ neighbor 192.168.4.2 activate
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/spine1/pim.conf b/tests/topotests/bgp_evpn_mh/spine1/pim.conf
new file mode 100644
index 0000000..68e686e
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine1/pim.conf
@@ -0,0 +1,18 @@
+ip pim rp 192.168.100.13
+ip pim spt-switchover infinity-and-beyond
+!
+int lo
+ ip pim
+!
+int spine1-eth0
+ ip pim
+!
+int spine1-eth1
+ ip pim
+!
+int spine1-eth2
+ ip pim
+!
+int spine1-eth3
+ ip pim
+!
diff --git a/tests/topotests/bgp_evpn_mh/spine1/zebra.conf b/tests/topotests/bgp_evpn_mh/spine1/zebra.conf
new file mode 100644
index 0000000..80e9e5a
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine1/zebra.conf
@@ -0,0 +1,15 @@
+int spine1-eth0
+ ip addr 192.168.1.1/24
+!
+int spine1-eth1
+ ip addr 192.168.2.1/24
+!
+int spine1-eth2
+ ip addr 192.168.3.1/24
+!
+int spine1-eth3
+ ip addr 192.168.4.1/24
+!
+int lo
+ ip addr 192.168.100.13/32
+ ip addr 192.168.100.100/32
diff --git a/tests/topotests/bgp_evpn_mh/spine2/evpn.conf b/tests/topotests/bgp_evpn_mh/spine2/evpn.conf
new file mode 100644
index 0000000..ec2e789
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine2/evpn.conf
@@ -0,0 +1,17 @@
+frr defaults datacenter
+!
+router bgp 65001
+ bgp router-id 192.168.100.14
+ no bgp ebgp-requires-policy
+ neighbor 192.168.5.2 remote-as external
+ neighbor 192.168.6.2 remote-as external
+ neighbor 192.168.7.2 remote-as external
+ neighbor 192.168.8.2 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.5.2 activate
+ neighbor 192.168.6.2 activate
+ neighbor 192.168.7.2 activate
+ neighbor 192.168.8.2 activate
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/spine2/pim.conf b/tests/topotests/bgp_evpn_mh/spine2/pim.conf
new file mode 100644
index 0000000..c156624
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine2/pim.conf
@@ -0,0 +1,18 @@
+ip pim rp 192.168.100.13
+ip pim spt-switchover infinity-and-beyond
+!
+int lo
+ ip pim
+!
+int spine2-eth0
+ ip pim
+!
+int spine2-eth1
+ ip pim
+!
+int spine2-eth2
+ ip pim
+!
+int spine2-eth3
+ ip pim
+!
diff --git a/tests/topotests/bgp_evpn_mh/spine2/zebra.conf b/tests/topotests/bgp_evpn_mh/spine2/zebra.conf
new file mode 100644
index 0000000..1cd1df8
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/spine2/zebra.conf
@@ -0,0 +1,15 @@
+int spine2-eth0
+ ip addr 192.168.5.1/24
+!
+int spine2-eth1
+ ip addr 192.168.6.1/24
+!
+int spine2-eth2
+ ip addr 192.168.7.1/24
+!
+int spine2-eth3
+ ip addr 192.168.8.1/24
+!
+int lo
+ ip addr 192.168.100.14/32
+ ip addr 192.168.100.100/32
diff --git a/tests/topotests/bgp_evpn_mh/test_evpn_mh.py b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
new file mode 100644
index 0000000..b0e4381
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/test_evpn_mh.py
@@ -0,0 +1,809 @@
+#!/usr/bin/env python
+
+#
+# test_evpn_mh.py
+#
+# Copyright (c) 2020 by
+# Cumulus Networks, Inc.
+# Anuradha Karuppiah
+#
+# Permission to use, copy, modify, and/or distribute this software
+# for any purpose with or without fee is hereby granted, provided
+# that the above copyright notice and this permission notice appear
+# in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+# OF THIS SOFTWARE.
+#
+
+"""
+test_evpn_mh.py: Testing EVPN multihoming
+
+"""
+
+import os
+import sys
+import subprocess
+from functools import partial
+
+import pytest
+import json
+import platform
+from functools import partial
+
+pytestmark = [pytest.mark.bgpd, pytest.mark.pimd]
+
+# Save the Current Working Directory to find configuration files.
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+# Import topogen and topotest helpers
+from lib import topotest
+
+# Required to instantiate the topology builder class.
+from lib.topogen import Topogen, TopoRouter, get_topogen
+
+pytestmark = [pytest.mark.bgpd, pytest.mark.pimd]
+
+#####################################################
+##
+## Network Topology Definition
+##
+## See topology picture at evpn-mh-topo-tests.pdf
+#####################################################
+
+
+def build_topo(tgen):
+ """
+ EVPN Multihoming Topology -
+ 1. Two level CLOS
+ 2. Two spine switches - spine1, spine2
+ 3. Two racks with Top-of-Rack switches per rack - tormx1, tormx2
+ 4. Two dual attached hosts per-rack - hostdx1, hostdx2
+ """
+
+ tgen.add_router("spine1")
+ tgen.add_router("spine2")
+ tgen.add_router("torm11")
+ tgen.add_router("torm12")
+ tgen.add_router("torm21")
+ tgen.add_router("torm22")
+ tgen.add_router("hostd11")
+ tgen.add_router("hostd12")
+ tgen.add_router("hostd21")
+ tgen.add_router("hostd22")
+
+ # On main router
+ # First switch is for a dummy interface (for local network)
+
+ ##################### spine1 ########################
+ # spine1-eth0 is connected to torm11-eth0
+ switch = tgen.add_switch("sw1")
+ switch.add_link(tgen.gears["spine1"])
+ switch.add_link(tgen.gears["torm11"])
+
+ # spine1-eth1 is connected to torm12-eth0
+ switch = tgen.add_switch("sw2")
+ switch.add_link(tgen.gears["spine1"])
+ switch.add_link(tgen.gears["torm12"])
+
+ # spine1-eth2 is connected to torm21-eth0
+ switch = tgen.add_switch("sw3")
+ switch.add_link(tgen.gears["spine1"])
+ switch.add_link(tgen.gears["torm21"])
+
+ # spine1-eth3 is connected to torm22-eth0
+ switch = tgen.add_switch("sw4")
+ switch.add_link(tgen.gears["spine1"])
+ switch.add_link(tgen.gears["torm22"])
+
+ ##################### spine2 ########################
+ # spine2-eth0 is connected to torm11-eth1
+ switch = tgen.add_switch("sw5")
+ switch.add_link(tgen.gears["spine2"])
+ switch.add_link(tgen.gears["torm11"])
+
+ # spine2-eth1 is connected to torm12-eth1
+ switch = tgen.add_switch("sw6")
+ switch.add_link(tgen.gears["spine2"])
+ switch.add_link(tgen.gears["torm12"])
+
+ # spine2-eth2 is connected to torm21-eth1
+ switch = tgen.add_switch("sw7")
+ switch.add_link(tgen.gears["spine2"])
+ switch.add_link(tgen.gears["torm21"])
+
+ # spine2-eth3 is connected to torm22-eth1
+ switch = tgen.add_switch("sw8")
+ switch.add_link(tgen.gears["spine2"])
+ switch.add_link(tgen.gears["torm22"])
+
+ ##################### torm11 ########################
+ # torm11-eth2 is connected to hostd11-eth0
+ switch = tgen.add_switch("sw9")
+ switch.add_link(tgen.gears["torm11"])
+ switch.add_link(tgen.gears["hostd11"])
+
+ # torm11-eth3 is connected to hostd12-eth0
+ switch = tgen.add_switch("sw10")
+ switch.add_link(tgen.gears["torm11"])
+ switch.add_link(tgen.gears["hostd12"])
+
+ ##################### torm12 ########################
+ # torm12-eth2 is connected to hostd11-eth1
+ switch = tgen.add_switch("sw11")
+ switch.add_link(tgen.gears["torm12"])
+ switch.add_link(tgen.gears["hostd11"])
+
+ # torm12-eth3 is connected to hostd12-eth1
+ switch = tgen.add_switch("sw12")
+ switch.add_link(tgen.gears["torm12"])
+ switch.add_link(tgen.gears["hostd12"])
+
+ ##################### torm21 ########################
+ # torm21-eth2 is connected to hostd21-eth0
+ switch = tgen.add_switch("sw13")
+ switch.add_link(tgen.gears["torm21"])
+ switch.add_link(tgen.gears["hostd21"])
+
+ # torm21-eth3 is connected to hostd22-eth0
+ switch = tgen.add_switch("sw14")
+ switch.add_link(tgen.gears["torm21"])
+ switch.add_link(tgen.gears["hostd22"])
+
+ ##################### torm22 ########################
+ # torm22-eth2 is connected to hostd21-eth1
+ switch = tgen.add_switch("sw15")
+ switch.add_link(tgen.gears["torm22"])
+ switch.add_link(tgen.gears["hostd21"])
+
+ # torm22-eth3 is connected to hostd22-eth1
+ switch = tgen.add_switch("sw16")
+ switch.add_link(tgen.gears["torm22"])
+ switch.add_link(tgen.gears["hostd22"])
+
+
+#####################################################
+##
+## Tests starting
+##
+#####################################################
+
+tor_ips = {
+ "torm11": "192.168.100.15",
+ "torm12": "192.168.100.16",
+ "torm21": "192.168.100.17",
+ "torm22": "192.168.100.18",
+}
+
+svi_ips = {
+ "torm11": "45.0.0.2",
+ "torm12": "45.0.0.3",
+ "torm21": "45.0.0.4",
+ "torm22": "45.0.0.5",
+}
+
+tor_ips_rack_1 = {"torm11": "192.168.100.15", "torm12": "192.168.100.16"}
+
+tor_ips_rack_2 = {"torm21": "192.168.100.17", "torm22": "192.168.100.18"}
+
+host_es_map = {
+ "hostd11": "03:44:38:39:ff:ff:01:00:00:01",
+ "hostd12": "03:44:38:39:ff:ff:01:00:00:02",
+ "hostd21": "03:44:38:39:ff:ff:02:00:00:01",
+ "hostd22": "03:44:38:39:ff:ff:02:00:00:02",
+}
+
+
+def config_bond(node, bond_name, bond_members, bond_ad_sys_mac, br):
+ """
+ Used to setup bonds on the TORs and hosts for MH
+ """
+ node.run("ip link add dev %s type bond mode 802.3ad" % bond_name)
+ node.run("ip link set dev %s type bond lacp_rate 1" % bond_name)
+ node.run("ip link set dev %s type bond miimon 100" % bond_name)
+ node.run("ip link set dev %s type bond xmit_hash_policy layer3+4" % bond_name)
+ node.run("ip link set dev %s type bond min_links 1" % bond_name)
+ node.run(
+ "ip link set dev %s type bond ad_actor_system %s" % (bond_name, bond_ad_sys_mac)
+ )
+
+ for bond_member in bond_members:
+ node.run("ip link set dev %s down" % bond_member)
+ node.run("ip link set dev %s master %s" % (bond_member, bond_name))
+ node.run("ip link set dev %s up" % bond_member)
+
+ node.run("ip link set dev %s up" % bond_name)
+
+ # if bridge is specified add the bond as a bridge member
+ if br:
+ node.run(" ip link set dev %s master bridge" % bond_name)
+ node.run("/sbin/bridge link set dev %s priority 8" % bond_name)
+ node.run("/sbin/bridge vlan del vid 1 dev %s" % bond_name)
+ node.run("/sbin/bridge vlan del vid 1 untagged pvid dev %s" % bond_name)
+ node.run("/sbin/bridge vlan add vid 1000 dev %s" % bond_name)
+ node.run("/sbin/bridge vlan add vid 1000 untagged pvid dev %s" % bond_name)
+
+
+def config_mcast_tunnel_termination_device(node):
+ """
+ The kernel requires a device to terminate VxLAN multicast tunnels
+ when EVPN-PIM is used for flooded traffic
+ """
+ node.run("ip link add dev ipmr-lo type dummy")
+ node.run("ip link set dev ipmr-lo mtu 16000")
+ node.run("ip link set dev ipmr-lo mode dormant")
+ node.run("ip link set dev ipmr-lo up")
+
+
+def config_bridge(node):
+ """
+ Create a VLAN aware bridge
+ """
+ node.run("ip link add dev bridge type bridge stp_state 0")
+ node.run("ip link set dev bridge type bridge vlan_filtering 1")
+ node.run("ip link set dev bridge mtu 9216")
+ node.run("ip link set dev bridge type bridge ageing_time 1800")
+ node.run("ip link set dev bridge type bridge mcast_snooping 0")
+ node.run("ip link set dev bridge type bridge vlan_stats_enabled 1")
+ node.run("ip link set dev bridge up")
+ node.run("/sbin/bridge vlan add vid 1000 dev bridge")
+
+
+def config_vxlan(node, node_ip):
+ """
+ Create a VxLAN device for VNI 1000 and add it to the bridge.
+ VLAN-1000 is mapped to VNI-1000.
+ """
+ node.run("ip link add dev vx-1000 type vxlan id 1000 dstport 4789")
+ node.run("ip link set dev vx-1000 type vxlan nolearning")
+ node.run("ip link set dev vx-1000 type vxlan local %s" % node_ip)
+ node.run("ip link set dev vx-1000 type vxlan ttl 64")
+ node.run("ip link set dev vx-1000 mtu 9152")
+ node.run("ip link set dev vx-1000 type vxlan dev ipmr-lo group 239.1.1.100")
+ node.run("ip link set dev vx-1000 up")
+
+ # bridge attrs
+ node.run("ip link set dev vx-1000 master bridge")
+ node.run("/sbin/bridge link set dev vx-1000 neigh_suppress on")
+ node.run("/sbin/bridge link set dev vx-1000 learning off")
+ node.run("/sbin/bridge link set dev vx-1000 priority 8")
+ node.run("/sbin/bridge vlan del vid 1 dev vx-1000")
+ node.run("/sbin/bridge vlan del vid 1 untagged pvid dev vx-1000")
+ node.run("/sbin/bridge vlan add vid 1000 dev vx-1000")
+ node.run("/sbin/bridge vlan add vid 1000 untagged pvid dev vx-1000")
+
+
+def config_svi(node, svi_pip):
+ """
+ Create an SVI for VLAN 1000
+ """
+ node.run("ip link add link bridge name vlan1000 type vlan id 1000 protocol 802.1q")
+ node.run("ip addr add %s/24 dev vlan1000" % svi_pip)
+ node.run("ip link set dev vlan1000 up")
+ node.run("/sbin/sysctl net.ipv4.conf.vlan1000.arp_accept=1")
+ node.run("ip link add link vlan1000 name vlan1000-v0 type macvlan mode private")
+ node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.accept_dad=0")
+ node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.dad_transmits")
+ node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.dad_transmits=0")
+ node.run("ip link set dev vlan1000-v0 address 00:00:5e:00:01:01")
+ node.run("ip link set dev vlan1000-v0 up")
+ # metric 1024 is not working
+ node.run("ip addr add 45.0.0.1/24 dev vlan1000-v0")
+
+
+def config_tor(tor_name, tor, tor_ip, svi_pip):
+ """
+ Create the bond/vxlan-bridge on the TOR which acts as VTEP and EPN-PE
+ """
+ # create a device for terminating VxLAN multicast tunnels
+ config_mcast_tunnel_termination_device(tor)
+
+ # create a vlan aware bridge
+ config_bridge(tor)
+
+ # create vxlan device and add it to bridge
+ config_vxlan(tor, tor_ip)
+
+ # create hostbonds and add them to the bridge
+ if "torm1" in tor_name:
+ sys_mac = "44:38:39:ff:ff:01"
+ else:
+ sys_mac = "44:38:39:ff:ff:02"
+ bond_member = tor_name + "-eth2"
+ config_bond(tor, "hostbond1", [bond_member], sys_mac, "bridge")
+
+ bond_member = tor_name + "-eth3"
+ config_bond(tor, "hostbond2", [bond_member], sys_mac, "bridge")
+
+ # create SVI
+ config_svi(tor, svi_pip)
+
+
+def config_tors(tgen, tors):
+ for tor_name in tors:
+ tor = tgen.gears[tor_name]
+ config_tor(tor_name, tor, tor_ips.get(tor_name), svi_ips.get(tor_name))
+
+
+def compute_host_ip_mac(host_name):
+ host_id = host_name.split("hostd")[1]
+ host_ip = "45.0.0." + host_id + "/24"
+ host_mac = "00:00:00:00:00:" + host_id
+
+ return host_ip, host_mac
+
+
+def config_host(host_name, host):
+ """
+ Create the dual-attached bond on host nodes for MH
+ """
+ bond_members = []
+ bond_members.append(host_name + "-eth0")
+ bond_members.append(host_name + "-eth1")
+ bond_name = "torbond"
+ config_bond(host, bond_name, bond_members, "00:00:00:00:00:00", None)
+
+ host_ip, host_mac = compute_host_ip_mac(host_name)
+ host.run("ip addr add %s dev %s" % (host_ip, bond_name))
+ host.run("ip link set dev %s address %s" % (bond_name, host_mac))
+
+
+def config_hosts(tgen, hosts):
+ for host_name in hosts:
+ host = tgen.gears[host_name]
+ config_host(host_name, host)
+
+
+def setup_module(module):
+ "Setup topology"
+ tgen = Topogen(build_topo, module.__name__)
+ tgen.start_topology()
+
+ krel = platform.release()
+ if topotest.version_cmp(krel, "4.19") < 0:
+ tgen.errors = "kernel 4.19 needed for multihoming tests"
+ pytest.skip(tgen.errors)
+
+ tors = []
+ tors.append("torm11")
+ tors.append("torm12")
+ tors.append("torm21")
+ tors.append("torm22")
+ config_tors(tgen, tors)
+
+ hosts = []
+ hosts.append("hostd11")
+ hosts.append("hostd12")
+ hosts.append("hostd21")
+ hosts.append("hostd22")
+ config_hosts(tgen, hosts)
+
+ # tgen.mininet_cli()
+ # This is a sample of configuration loading.
+ router_list = tgen.routers()
+ for rname, router in router_list.items():
+ router.load_config(
+ TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
+ )
+ router.load_config(
+ TopoRouter.RD_PIM, os.path.join(CWD, "{}/pim.conf".format(rname))
+ )
+ router.load_config(
+ TopoRouter.RD_BGP, os.path.join(CWD, "{}/evpn.conf".format(rname))
+ )
+ tgen.start_router()
+ # tgen.mininet_cli()
+
+
+def teardown_module(_mod):
+ "Teardown the pytest environment"
+ tgen = get_topogen()
+
+ # This function tears down the whole topology.
+ tgen.stop_topology()
+
+
+def check_local_es(esi, vtep_ips, dut_name, down_vteps):
+ """
+ Check if ES peers are setup correctly on local ESs
+ """
+ peer_ips = []
+ if "torm1" in dut_name:
+ tor_ips_rack = tor_ips_rack_1
+ else:
+ tor_ips_rack = tor_ips_rack_2
+
+ for tor_name, tor_ip in tor_ips_rack.items():
+ if dut_name not in tor_name:
+ peer_ips.append(tor_ip)
+
+ # remove down VTEPs from the peer check list
+ peer_set = set(peer_ips)
+ down_vtep_set = set(down_vteps)
+ peer_set = peer_set - down_vtep_set
+
+ vtep_set = set(vtep_ips)
+ diff = peer_set.symmetric_difference(vtep_set)
+
+ return (esi, diff) if diff else None
+
+
+def check_remote_es(esi, vtep_ips, dut_name, down_vteps):
+ """
+ Verify list of PEs associated with a remote ES
+ """
+ remote_ips = []
+
+ if "torm1" in dut_name:
+ tor_ips_rack = tor_ips_rack_2
+ else:
+ tor_ips_rack = tor_ips_rack_1
+
+ for tor_name, tor_ip in tor_ips_rack.items():
+ remote_ips.append(tor_ip)
+
+ # remove down VTEPs from the remote check list
+ remote_set = set(remote_ips)
+ down_vtep_set = set(down_vteps)
+ remote_set = remote_set - down_vtep_set
+
+ vtep_set = set(vtep_ips)
+ diff = remote_set.symmetric_difference(vtep_set)
+
+ return (esi, diff) if diff else None
+
+
+def check_es(dut):
+ """
+ Verify list of PEs associated all ESs, local and remote
+ """
+ bgp_es = dut.vtysh_cmd("show bgp l2vp evpn es json")
+ bgp_es_json = json.loads(bgp_es)
+
+ result = None
+
+ expected_es_set = set([v for k, v in host_es_map.items()])
+ curr_es_set = []
+
+ # check is ES content is correct
+ for es in bgp_es_json:
+ esi = es["esi"]
+ curr_es_set.append(esi)
+ types = es["type"]
+ vtep_ips = []
+ for vtep in es.get("vteps", []):
+ vtep_ips.append(vtep["vtep_ip"])
+
+ if "local" in types:
+ result = check_local_es(esi, vtep_ips, dut.name, [])
+ else:
+ result = check_remote_es(esi, vtep_ips, dut.name, [])
+
+ if result:
+ return result
+
+ # check if all ESs are present
+ curr_es_set = set(curr_es_set)
+ result = curr_es_set.symmetric_difference(expected_es_set)
+
+ return result if result else None
+
+
+def check_one_es(dut, esi, down_vteps):
+ """
+ Verify list of PEs associated all ESs, local and remote
+ """
+ bgp_es = dut.vtysh_cmd("show bgp l2vp evpn es %s json" % esi)
+ es = json.loads(bgp_es)
+
+ if not es:
+ return "esi %s not found" % esi
+
+ esi = es["esi"]
+ types = es["type"]
+ vtep_ips = []
+ for vtep in es.get("vteps", []):
+ vtep_ips.append(vtep["vtep_ip"])
+
+ if "local" in types:
+ result = check_local_es(esi, vtep_ips, dut.name, down_vteps)
+ else:
+ result = check_remote_es(esi, vtep_ips, dut.name, down_vteps)
+
+ return result
+
+
+def test_evpn_es():
+ """
+ Two ES are setup on each rack. This test checks if -
+ 1. ES peer has been added to the local ES (via Type-1/EAD route)
+ 2. The remote ESs are setup with the right list of PEs (via Type-1)
+ """
+
+ tgen = get_topogen()
+
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ dut_name = "torm11"
+ dut = tgen.gears[dut_name]
+ test_fn = partial(check_es, dut)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+
+ assertmsg = '"{}" ES content incorrect'.format(dut_name)
+ assert result is None, assertmsg
+ # tgen.mininet_cli()
+
+
+def test_evpn_ead_update():
+ """
+ Flap a host link one the remote rack and check if the EAD updates
+ are sent/processed for the corresponding ESI
+ """
+ tgen = get_topogen()
+
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ # dut on rack1 and host link flap on rack2
+ dut_name = "torm11"
+ dut = tgen.gears[dut_name]
+
+ remote_tor_name = "torm21"
+ remote_tor = tgen.gears[remote_tor_name]
+
+ host_name = "hostd21"
+ host = tgen.gears[host_name]
+ esi = host_es_map.get(host_name)
+
+ # check if the VTEP list is right to start with
+ down_vteps = []
+ test_fn = partial(check_one_es, dut, esi, down_vteps)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" ES content incorrect'.format(dut_name)
+ assert result is None, assertmsg
+
+ # down a remote host link and check if the EAD withdraw is rxed
+ # Note: LACP is not working as expected so I am temporarily shutting
+ # down the link on the remote TOR instead of the remote host
+ remote_tor.run("ip link set dev %s-%s down" % (remote_tor_name, "eth2"))
+ down_vteps.append(tor_ips.get(remote_tor_name))
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" ES incorrect after remote link down'.format(dut_name)
+ assert result is None, assertmsg
+
+ # bring up remote host link and check if the EAD update is rxed
+ down_vteps.remove(tor_ips.get(remote_tor_name))
+ remote_tor.run("ip link set dev %s-%s up" % (remote_tor_name, "eth2"))
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" ES incorrect after remote link flap'.format(dut_name)
+ assert result is None, assertmsg
+
+ # tgen.mininet_cli()
+
+
+def ping_anycast_gw(tgen):
+ # ping the anycast gw from the local and remote hosts to populate
+ # the mac address on the PEs
+ python3_path = tgen.net.get_exec_path(["python3", "python"])
+ script_path = os.path.abspath(os.path.join(CWD, "../lib/scapy_sendpkt.py"))
+ intf = "torbond"
+ ipaddr = "45.0.0.1"
+ ping_cmd = [
+ python3_path,
+ script_path,
+ "--imports=Ether,ARP",
+ "--interface=" + intf,
+ 'Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="{}")'.format(ipaddr),
+ ]
+ for name in ("hostd11", "hostd21"):
+ host = tgen.net.hosts[name]
+ _, stdout, _ = host.cmd_status(ping_cmd, warn=False, stderr=subprocess.STDOUT)
+ stdout = stdout.strip()
+ if stdout:
+ host.logger.debug(
+ "%s: arping on %s for %s returned: %s", name, intf, ipaddr, stdout
+ )
+
+
+def check_mac(dut, vni, mac, m_type, esi, intf, ping_gw=False, tgen=None):
+ """
+ checks if mac is present and if desination matches the one provided
+ """
+
+ if ping_gw:
+ ping_anycast_gw(tgen)
+
+ out = dut.vtysh_cmd("show evpn mac vni %d mac %s json" % (vni, mac))
+
+ mac_js = json.loads(out)
+ for mac, info in mac_js.items():
+ tmp_esi = info.get("esi", "")
+ tmp_m_type = info.get("type", "")
+ tmp_intf = info.get("intf", "") if tmp_m_type == "local" else ""
+ if tmp_esi == esi and tmp_m_type == m_type and intf == intf:
+ return None
+
+ return "invalid vni %d mac %s out %s" % (vni, mac, mac_js)
+
+
+def test_evpn_mac():
+ """
+ 1. Add a MAC on hostd11 and check if the MAC is synced between
+ torm11 and torm12. And installed as a local MAC.
+ 2. Add a MAC on hostd21 and check if the MAC is installed as a
+ remote MAC on torm11 and torm12
+ """
+
+ tgen = get_topogen()
+
+ local_host = tgen.gears["hostd11"]
+ remote_host = tgen.gears["hostd21"]
+ tors = []
+ tors.append(tgen.gears["torm11"])
+ tors.append(tgen.gears["torm12"])
+
+ vni = 1000
+
+ # check if the rack-1 host MAC is present on all rack-1 PEs
+ # and points to local access port
+ m_type = "local"
+ _, mac = compute_host_ip_mac(local_host.name)
+ esi = host_es_map.get(local_host.name)
+ intf = "hostbond1"
+
+ for tor in tors:
+ test_fn = partial(check_mac, tor, vni, mac, m_type, esi, intf, True, tgen)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" local MAC content incorrect'.format(tor.name)
+ assert result is None, assertmsg
+
+ # check if the rack-2 host MAC is present on all rack-1 PEs
+ # and points to the remote ES destination
+ m_type = "remote"
+ _, mac = compute_host_ip_mac(remote_host.name)
+ esi = host_es_map.get(remote_host.name)
+ intf = ""
+
+ for tor in tors:
+ test_fn = partial(check_mac, tor, vni, mac, m_type, esi, intf)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" remote MAC content incorrect'.format(tor.name)
+ assert result is None, assertmsg
+
+
+def check_df_role(dut, esi, role):
+ """
+ Return error string if the df role on the dut is different
+ """
+ es_json = dut.vtysh_cmd("show evpn es %s json" % esi)
+ es = json.loads(es_json)
+
+ if not es:
+ return "esi %s not found" % esi
+
+ flags = es.get("flags", [])
+ curr_role = "nonDF" if "nonDF" in flags else "DF"
+
+ if curr_role != role:
+ return "%s is %s for %s" % (dut.name, curr_role, esi)
+
+ return None
+
+
+def test_evpn_df():
+ """
+ 1. Check the DF role on all the PEs on rack-1.
+ 2. Increase the DF preference on the non-DF and check if it becomes
+ the DF winner.
+ """
+
+ tgen = get_topogen()
+
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ # We will run the tests on just one ES
+ esi = host_es_map.get("hostd11")
+ intf = "hostbond1"
+
+ tors = []
+ tors.append(tgen.gears["torm11"])
+ tors.append(tgen.gears["torm12"])
+ df_node = "torm11"
+
+ # check roles on rack-1
+ for tor in tors:
+ role = "DF" if tor.name == df_node else "nonDF"
+ test_fn = partial(check_df_role, tor, esi, role)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" DF role incorrect'.format(tor.name)
+ assert result is None, assertmsg
+
+ # change df preference on the nonDF to make it the df
+ torm12 = tgen.gears["torm12"]
+ torm12.vtysh_cmd("conf\ninterface %s\nevpn mh es-df-pref %d" % (intf, 60000))
+ df_node = "torm12"
+
+ # re-check roles on rack-1; we should have a new winner
+ for tor in tors:
+ role = "DF" if tor.name == df_node else "nonDF"
+ test_fn = partial(check_df_role, tor, esi, role)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" DF role incorrect'.format(tor.name)
+ assert result is None, assertmsg
+
+ # tgen.mininet_cli()
+
+
+def check_protodown_rc(dut, protodown_rc):
+ """
+ check if specified protodown reason code is set
+ """
+
+ out = dut.vtysh_cmd("show evpn json")
+
+ evpn_js = json.loads(out)
+ tmp_rc = evpn_js.get("protodownReasons", [])
+
+ if protodown_rc:
+ if protodown_rc not in tmp_rc:
+ return "protodown %s missing in %s" % (protodown_rc, tmp_rc)
+ else:
+ if tmp_rc:
+ return "unexpected protodown rc %s" % (tmp_rc)
+
+ return None
+
+
+def test_evpn_uplink_tracking():
+ """
+ 1. Wait for access ports to come out of startup-delay
+ 2. disable uplinks and check if access ports have been protodowned
+ 3. enable uplinks and check if access ports have been moved out
+ of protodown
+ """
+
+ tgen = get_topogen()
+
+ dut_name = "torm11"
+ dut = tgen.gears[dut_name]
+
+ # wait for protodown rc to clear after startup
+ test_fn = partial(check_protodown_rc, dut, None)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" protodown rc incorrect'.format(dut_name)
+ assert result is None, assertmsg
+
+ # disable the uplinks
+ dut.run("ip link set %s-eth0 down" % dut_name)
+ dut.run("ip link set %s-eth1 down" % dut_name)
+
+ # check if the access ports have been protodowned
+ test_fn = partial(check_protodown_rc, dut, "uplinkDown")
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" protodown rc incorrect'.format(dut_name)
+ assert result is None, assertmsg
+
+ # enable the uplinks
+ dut.run("ip link set %s-eth0 up" % dut_name)
+ dut.run("ip link set %s-eth1 up" % dut_name)
+
+ # check if the access ports have been moved out of protodown
+ test_fn = partial(check_protodown_rc, dut, None)
+ _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3)
+ assertmsg = '"{}" protodown rc incorrect'.format(dut_name)
+ assert result is None, assertmsg
+
+
+if __name__ == "__main__":
+ args = ["-s"] + sys.argv[1:]
+ sys.exit(pytest.main(args))
diff --git a/tests/topotests/bgp_evpn_mh/torm11/evpn.conf b/tests/topotests/bgp_evpn_mh/torm11/evpn.conf
new file mode 100644
index 0000000..2c1c695
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm11/evpn.conf
@@ -0,0 +1,21 @@
+!
+frr defaults datacenter
+!
+! debug bgp evpn mh es
+! debug bgp evpn mh route
+! debug bgp zebra
+!
+!
+router bgp 65002
+ bgp router-id 192.168.100.15
+ no bgp ebgp-requires-policy
+ neighbor 192.168.1.1 remote-as external
+ neighbor 192.168.5.1 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.1.1 activate
+ neighbor 192.168.5.1 activate
+ advertise-all-vni
+ advertise-svi-ip
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm11/pim.conf b/tests/topotests/bgp_evpn_mh/torm11/pim.conf
new file mode 100644
index 0000000..fbba735
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm11/pim.conf
@@ -0,0 +1,13 @@
+!
+ip pim rp 192.168.100.13 239.1.1.0/24
+ip pim spt-switchover infinity-and-beyond
+!
+interface lo
+ ip igmp
+ ip pim
+!
+interface torm11-eth0
+ ip pim
+!
+interface torm11-eth1
+ ip pim
diff --git a/tests/topotests/bgp_evpn_mh/torm11/zebra.conf b/tests/topotests/bgp_evpn_mh/torm11/zebra.conf
new file mode 100644
index 0000000..a88370d
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm11/zebra.conf
@@ -0,0 +1,27 @@
+! debug zebra evpn mh es
+! debug zebra evpn mh mac
+! debug zebra evpn mh neigh
+! debug zebra evpn mh nh
+! debug zebra vxlan
+!
+evpn mh startup-delay 1
+!
+int torm11-eth0
+ ip addr 192.168.1.2/24
+ evpn mh uplink
+!
+int torm11-eth1
+ ip addr 192.168.5.2/24
+ evpn mh uplink
+!
+int lo
+ ip addr 192.168.100.15/32
+!
+interface hostbond1
+ evpn mh es-id 1
+ evpn mh es-sys-mac 44:38:39:ff:ff:01
+!
+interface hostbond2
+ evpn mh es-id 2
+ evpn mh es-sys-mac 44:38:39:ff:ff:01
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm12/evpn.conf b/tests/topotests/bgp_evpn_mh/torm12/evpn.conf
new file mode 100644
index 0000000..8b0ce1d
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm12/evpn.conf
@@ -0,0 +1,21 @@
+!
+frr defaults datacenter
+!
+! debug bgp evpn mh es
+! debug bgp evpn mh route
+! debug bgp zebra
+!
+!
+router bgp 65003
+ bgp router-id 192.168.100.16
+ no bgp ebgp-requires-policy
+ neighbor 192.168.2.1 remote-as external
+ neighbor 192.168.6.1 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.2.1 activate
+ neighbor 192.168.6.1 activate
+ advertise-all-vni
+ advertise-svi-ip
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm12/pim.conf b/tests/topotests/bgp_evpn_mh/torm12/pim.conf
new file mode 100644
index 0000000..3dd63b4
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm12/pim.conf
@@ -0,0 +1,13 @@
+!
+ip pim rp 192.168.100.13 239.1.1.0/24
+ip pim spt-switchover infinity-and-beyond
+!
+interface lo
+ ip igmp
+ ip pim
+!
+interface torm12-eth0
+ ip pim
+!
+interface torm12-eth1
+ ip pim
diff --git a/tests/topotests/bgp_evpn_mh/torm12/zebra.conf b/tests/topotests/bgp_evpn_mh/torm12/zebra.conf
new file mode 100644
index 0000000..9532762
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm12/zebra.conf
@@ -0,0 +1,28 @@
+! debug zebra evpn mh es
+! debug zebra evpn mh mac
+! debug zebra evpn mh neigh
+! debug zebra evpn mh nh
+! debug zebra vxlan
+!
+evpn mh startup-delay 1
+!
+int torm12-eth0
+ ip addr 192.168.2.2/24
+ evpn mh uplink
+!
+int torm12-eth1
+ ip addr 192.168.6.2/24
+ evpn mh uplink
+!
+!
+int lo
+ ip addr 192.168.100.16/32
+!
+interface hostbond1
+ evpn mh es-id 1
+ evpn mh es-sys-mac 44:38:39:ff:ff:01
+!
+interface hostbond2
+ evpn mh es-id 2
+ evpn mh es-sys-mac 44:38:39:ff:ff:01
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm21/evpn.conf b/tests/topotests/bgp_evpn_mh/torm21/evpn.conf
new file mode 100644
index 0000000..5247dc1
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm21/evpn.conf
@@ -0,0 +1,21 @@
+!
+frr defaults datacenter
+!
+! debug bgp evpn mh es
+! debug bgp evpn mh route
+! debug bgp zebra
+!
+!
+router bgp 65004
+ bgp router-id 192.168.100.17
+ no bgp ebgp-requires-policy
+ neighbor 192.168.3.1 remote-as external
+ neighbor 192.168.7.1 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.3.1 activate
+ neighbor 192.168.7.1 activate
+ advertise-all-vni
+ advertise-svi-ip
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm21/pim.conf b/tests/topotests/bgp_evpn_mh/torm21/pim.conf
new file mode 100644
index 0000000..71aa91a
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm21/pim.conf
@@ -0,0 +1,13 @@
+!
+ip pim rp 192.168.100.13 239.1.1.0/24
+ip pim spt-switchover infinity-and-beyond
+!
+interface lo
+ ip igmp
+ ip pim
+!
+interface torm21-eth0
+ ip pim
+!
+interface torm21-eth1
+ ip pim
diff --git a/tests/topotests/bgp_evpn_mh/torm21/zebra.conf b/tests/topotests/bgp_evpn_mh/torm21/zebra.conf
new file mode 100644
index 0000000..6c75df7
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm21/zebra.conf
@@ -0,0 +1,29 @@
+! debug zebra evpn mh es
+! debug zebra evpn mh mac
+! debug zebra evpn mh neigh
+! debug zebra evpn mh nh
+! debug zebra vxlan
+!
+evpn mh startup-delay 1
+!
+int torm21-eth0
+ ip addr 192.168.3.2/24
+ evpn mh uplink
+!
+!
+int torm21-eth1
+ ip addr 192.168.7.2/24
+ evpn mh uplink
+!
+!
+int lo
+ ip addr 192.168.100.17/32
+!
+interface hostbond1
+ evpn mh es-id 1
+ evpn mh es-sys-mac 44:38:39:ff:ff:02
+!
+interface hostbond2
+ evpn mh es-id 2
+ evpn mh es-sys-mac 44:38:39:ff:ff:02
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm22/evpn.conf b/tests/topotests/bgp_evpn_mh/torm22/evpn.conf
new file mode 100644
index 0000000..ec56360
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm22/evpn.conf
@@ -0,0 +1,20 @@
+!
+frr defaults datacenter
+!
+! debug bgp evpn mh es
+! debug bgp evpn mh route
+! debug bgp zebra
+!
+router bgp 65005
+ bgp router-id 192.168.100.18
+ no bgp ebgp-requires-policy
+ neighbor 192.168.4.1 remote-as external
+ neighbor 192.168.8.1 remote-as external
+ redistribute connected
+ address-family l2vpn evpn
+ neighbor 192.168.4.1 activate
+ neighbor 192.168.8.1 activate
+ advertise-all-vni
+ advertise-svi-ip
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_evpn_mh/torm22/pim.conf b/tests/topotests/bgp_evpn_mh/torm22/pim.conf
new file mode 100644
index 0000000..46f330f
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm22/pim.conf
@@ -0,0 +1,13 @@
+!
+ip pim rp 192.168.100.13 239.1.1.0/24
+ip pim spt-switchover infinity-and-beyond
+!
+interface lo
+ ip igmp
+ ip pim
+!
+interface torm22-eth0
+ ip pim
+!
+interface torm22-eth1
+ ip pim
diff --git a/tests/topotests/bgp_evpn_mh/torm22/zebra.conf b/tests/topotests/bgp_evpn_mh/torm22/zebra.conf
new file mode 100644
index 0000000..4c94966
--- /dev/null
+++ b/tests/topotests/bgp_evpn_mh/torm22/zebra.conf
@@ -0,0 +1,29 @@
+! debug zebra evpn mh es
+! debug zebra evpn mh mac
+! debug zebra evpn mh neigh
+! debug zebra evpn mh nh
+! debug zebra vxlan
+!
+evpn mh startup-delay 1
+!
+int torm22-eth0
+ ip addr 192.168.4.2/24
+ evpn mh uplink
+!
+!
+int torm22-eth1
+ ip addr 192.168.8.2/24
+ evpn mh uplink
+!
+!
+int lo
+ ip addr 192.168.100.18/32
+!
+interface hostbond1
+ evpn mh es-id 1
+ evpn mh es-sys-mac 44:38:39:ff:ff:02
+!
+interface hostbond2
+ evpn mh es-id 2
+ evpn mh es-sys-mac 44:38:39:ff:ff:02
+!