]>
Commit | Line | Data |
---|---|---|
df98b92c AK |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # test_evpn_mh.py | |
5 | # | |
6 | # Copyright (c) 2020 by | |
7 | # Cumulus Networks, Inc. | |
8 | # Anuradha Karuppiah | |
9 | # | |
10 | # Permission to use, copy, modify, and/or distribute this software | |
11 | # for any purpose with or without fee is hereby granted, provided | |
12 | # that the above copyright notice and this permission notice appear | |
13 | # in all copies. | |
14 | # | |
15 | # THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES | |
16 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
17 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR | |
18 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY | |
19 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | |
20 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | |
21 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE | |
22 | # OF THIS SOFTWARE. | |
23 | # | |
24 | ||
25 | """ | |
26 | test_evpn_mh.py: Testing EVPN multihoming | |
27 | ||
28 | """ | |
29 | ||
30 | import os | |
31 | import re | |
32 | import sys | |
49581587 CH |
33 | import subprocess |
34 | from functools import partial | |
35 | ||
df98b92c AK |
36 | import pytest |
37 | import json | |
38 | import platform | |
39 | from functools import partial | |
40 | ||
98ca91e1 | 41 | pytestmark = [pytest.mark.bgpd, pytest.mark.pimd] |
7ed8fcff | 42 | |
df98b92c AK |
43 | # Save the Current Working Directory to find configuration files. |
44 | CWD = os.path.dirname(os.path.realpath(__file__)) | |
45 | sys.path.append(os.path.join(CWD, "../")) | |
46 | ||
47 | # pylint: disable=C0413 | |
48 | # Import topogen and topotest helpers | |
49 | from lib import topotest | |
8db751b8 CH |
50 | # Required to instantiate the topology builder class. |
51 | from lib.micronet_compat import Topo | |
df98b92c AK |
52 | from lib.topogen import Topogen, TopoRouter, get_topogen |
53 | from lib.topolog import logger | |
54 | ||
bf3a0a9a DS |
55 | pytestmark = [pytest.mark.bgpd, pytest.mark.pimd] |
56 | ||
df98b92c AK |
57 | ##################################################### |
58 | ## | |
59 | ## Network Topology Definition | |
60 | ## | |
61 | ## See topology picture at evpn-mh-topo-tests.pdf | |
62 | ##################################################### | |
63 | ||
64 | ||
65 | class NetworkTopo(Topo): | |
701a0192 | 66 | """ |
df98b92c AK |
67 | EVPN Multihoming Topology - |
68 | 1. Two level CLOS | |
69 | 2. Two spine switches - spine1, spine2 | |
70 | 3. Two racks with Top-of-Rack switches per rack - tormx1, tormx2 | |
71 | 4. Two dual attached hosts per-rack - hostdx1, hostdx2 | |
701a0192 | 72 | """ |
df98b92c AK |
73 | |
74 | def build(self, **_opts): | |
75 | "Build function" | |
76 | ||
77 | tgen = get_topogen(self) | |
78 | ||
79 | tgen.add_router("spine1") | |
80 | tgen.add_router("spine2") | |
81 | tgen.add_router("torm11") | |
82 | tgen.add_router("torm12") | |
83 | tgen.add_router("torm21") | |
84 | tgen.add_router("torm22") | |
85 | tgen.add_router("hostd11") | |
86 | tgen.add_router("hostd12") | |
87 | tgen.add_router("hostd21") | |
88 | tgen.add_router("hostd22") | |
89 | ||
90 | # On main router | |
91 | # First switch is for a dummy interface (for local network) | |
92 | ||
df98b92c AK |
93 | ##################### spine1 ######################## |
94 | # spine1-eth0 is connected to torm11-eth0 | |
95 | switch = tgen.add_switch("sw1") | |
96 | switch.add_link(tgen.gears["spine1"]) | |
97 | switch.add_link(tgen.gears["torm11"]) | |
98 | ||
99 | # spine1-eth1 is connected to torm12-eth0 | |
100 | switch = tgen.add_switch("sw2") | |
101 | switch.add_link(tgen.gears["spine1"]) | |
102 | switch.add_link(tgen.gears["torm12"]) | |
103 | ||
104 | # spine1-eth2 is connected to torm21-eth0 | |
105 | switch = tgen.add_switch("sw3") | |
106 | switch.add_link(tgen.gears["spine1"]) | |
107 | switch.add_link(tgen.gears["torm21"]) | |
108 | ||
109 | # spine1-eth3 is connected to torm22-eth0 | |
110 | switch = tgen.add_switch("sw4") | |
111 | switch.add_link(tgen.gears["spine1"]) | |
112 | switch.add_link(tgen.gears["torm22"]) | |
113 | ||
114 | ##################### spine2 ######################## | |
115 | # spine2-eth0 is connected to torm11-eth1 | |
116 | switch = tgen.add_switch("sw5") | |
117 | switch.add_link(tgen.gears["spine2"]) | |
118 | switch.add_link(tgen.gears["torm11"]) | |
119 | ||
120 | # spine2-eth1 is connected to torm12-eth1 | |
121 | switch = tgen.add_switch("sw6") | |
122 | switch.add_link(tgen.gears["spine2"]) | |
123 | switch.add_link(tgen.gears["torm12"]) | |
124 | ||
125 | # spine2-eth2 is connected to torm21-eth1 | |
126 | switch = tgen.add_switch("sw7") | |
127 | switch.add_link(tgen.gears["spine2"]) | |
128 | switch.add_link(tgen.gears["torm21"]) | |
129 | ||
130 | # spine2-eth3 is connected to torm22-eth1 | |
131 | switch = tgen.add_switch("sw8") | |
132 | switch.add_link(tgen.gears["spine2"]) | |
133 | switch.add_link(tgen.gears["torm22"]) | |
134 | ||
135 | ##################### torm11 ######################## | |
136 | # torm11-eth2 is connected to hostd11-eth0 | |
137 | switch = tgen.add_switch("sw9") | |
138 | switch.add_link(tgen.gears["torm11"]) | |
139 | switch.add_link(tgen.gears["hostd11"]) | |
140 | ||
141 | # torm11-eth3 is connected to hostd12-eth0 | |
142 | switch = tgen.add_switch("sw10") | |
143 | switch.add_link(tgen.gears["torm11"]) | |
144 | switch.add_link(tgen.gears["hostd12"]) | |
145 | ||
146 | ##################### torm12 ######################## | |
147 | # torm12-eth2 is connected to hostd11-eth1 | |
148 | switch = tgen.add_switch("sw11") | |
149 | switch.add_link(tgen.gears["torm12"]) | |
150 | switch.add_link(tgen.gears["hostd11"]) | |
151 | ||
152 | # torm12-eth3 is connected to hostd12-eth1 | |
153 | switch = tgen.add_switch("sw12") | |
154 | switch.add_link(tgen.gears["torm12"]) | |
155 | switch.add_link(tgen.gears["hostd12"]) | |
156 | ||
157 | ##################### torm21 ######################## | |
158 | # torm21-eth2 is connected to hostd21-eth0 | |
159 | switch = tgen.add_switch("sw13") | |
160 | switch.add_link(tgen.gears["torm21"]) | |
161 | switch.add_link(tgen.gears["hostd21"]) | |
162 | ||
163 | # torm21-eth3 is connected to hostd22-eth0 | |
164 | switch = tgen.add_switch("sw14") | |
165 | switch.add_link(tgen.gears["torm21"]) | |
166 | switch.add_link(tgen.gears["hostd22"]) | |
167 | ||
168 | ##################### torm22 ######################## | |
169 | # torm22-eth2 is connected to hostd21-eth1 | |
170 | switch = tgen.add_switch("sw15") | |
171 | switch.add_link(tgen.gears["torm22"]) | |
172 | switch.add_link(tgen.gears["hostd21"]) | |
173 | ||
174 | # torm22-eth3 is connected to hostd22-eth1 | |
175 | switch = tgen.add_switch("sw16") | |
176 | switch.add_link(tgen.gears["torm22"]) | |
177 | switch.add_link(tgen.gears["hostd22"]) | |
178 | ||
179 | ||
180 | ##################################################### | |
181 | ## | |
182 | ## Tests starting | |
183 | ## | |
184 | ##################################################### | |
185 | ||
701a0192 | 186 | tor_ips = { |
187 | "torm11": "192.168.100.15", | |
188 | "torm12": "192.168.100.16", | |
189 | "torm21": "192.168.100.17", | |
190 | "torm22": "192.168.100.18", | |
191 | } | |
192 | ||
193 | svi_ips = { | |
194 | "torm11": "45.0.0.2", | |
195 | "torm12": "45.0.0.3", | |
196 | "torm21": "45.0.0.4", | |
197 | "torm22": "45.0.0.5", | |
198 | } | |
df98b92c | 199 | |
701a0192 | 200 | tor_ips_rack_1 = {"torm11": "192.168.100.15", "torm12": "192.168.100.16"} |
df98b92c | 201 | |
701a0192 | 202 | tor_ips_rack_2 = {"torm21": "192.168.100.17", "torm22": "192.168.100.18"} |
df98b92c | 203 | |
701a0192 | 204 | host_es_map = { |
205 | "hostd11": "03:44:38:39:ff:ff:01:00:00:01", | |
206 | "hostd12": "03:44:38:39:ff:ff:01:00:00:02", | |
207 | "hostd21": "03:44:38:39:ff:ff:02:00:00:01", | |
208 | "hostd22": "03:44:38:39:ff:ff:02:00:00:02", | |
209 | } | |
df98b92c | 210 | |
df98b92c AK |
211 | |
212 | def config_bond(node, bond_name, bond_members, bond_ad_sys_mac, br): | |
701a0192 | 213 | """ |
df98b92c | 214 | Used to setup bonds on the TORs and hosts for MH |
701a0192 | 215 | """ |
df98b92c AK |
216 | node.run("ip link add dev %s type bond mode 802.3ad" % bond_name) |
217 | node.run("ip link set dev %s type bond lacp_rate 1" % bond_name) | |
218 | node.run("ip link set dev %s type bond miimon 100" % bond_name) | |
219 | node.run("ip link set dev %s type bond xmit_hash_policy layer3+4" % bond_name) | |
220 | node.run("ip link set dev %s type bond min_links 1" % bond_name) | |
701a0192 | 221 | node.run( |
222 | "ip link set dev %s type bond ad_actor_system %s" % (bond_name, bond_ad_sys_mac) | |
223 | ) | |
df98b92c AK |
224 | |
225 | for bond_member in bond_members: | |
226 | node.run("ip link set dev %s down" % bond_member) | |
227 | node.run("ip link set dev %s master %s" % (bond_member, bond_name)) | |
228 | node.run("ip link set dev %s up" % bond_member) | |
229 | ||
230 | node.run("ip link set dev %s up" % bond_name) | |
231 | ||
232 | # if bridge is specified add the bond as a bridge member | |
233 | if br: | |
234 | node.run(" ip link set dev %s master bridge" % bond_name) | |
235 | node.run("/sbin/bridge link set dev %s priority 8" % bond_name) | |
236 | node.run("/sbin/bridge vlan del vid 1 dev %s" % bond_name) | |
237 | node.run("/sbin/bridge vlan del vid 1 untagged pvid dev %s" % bond_name) | |
238 | node.run("/sbin/bridge vlan add vid 1000 dev %s" % bond_name) | |
701a0192 | 239 | node.run("/sbin/bridge vlan add vid 1000 untagged pvid dev %s" % bond_name) |
df98b92c AK |
240 | |
241 | ||
242 | def config_mcast_tunnel_termination_device(node): | |
701a0192 | 243 | """ |
df98b92c AK |
244 | The kernel requires a device to terminate VxLAN multicast tunnels |
245 | when EVPN-PIM is used for flooded traffic | |
701a0192 | 246 | """ |
df98b92c AK |
247 | node.run("ip link add dev ipmr-lo type dummy") |
248 | node.run("ip link set dev ipmr-lo mtu 16000") | |
249 | node.run("ip link set dev ipmr-lo mode dormant") | |
250 | node.run("ip link set dev ipmr-lo up") | |
251 | ||
252 | ||
253 | def config_bridge(node): | |
701a0192 | 254 | """ |
df98b92c | 255 | Create a VLAN aware bridge |
701a0192 | 256 | """ |
df98b92c AK |
257 | node.run("ip link add dev bridge type bridge stp_state 0") |
258 | node.run("ip link set dev bridge type bridge vlan_filtering 1") | |
259 | node.run("ip link set dev bridge mtu 9216") | |
260 | node.run("ip link set dev bridge type bridge ageing_time 1800") | |
261 | node.run("ip link set dev bridge type bridge mcast_snooping 0") | |
262 | node.run("ip link set dev bridge type bridge vlan_stats_enabled 1") | |
263 | node.run("ip link set dev bridge up") | |
264 | node.run("/sbin/bridge vlan add vid 1000 dev bridge") | |
265 | ||
266 | ||
267 | def config_vxlan(node, node_ip): | |
701a0192 | 268 | """ |
df98b92c AK |
269 | Create a VxLAN device for VNI 1000 and add it to the bridge. |
270 | VLAN-1000 is mapped to VNI-1000. | |
701a0192 | 271 | """ |
df98b92c AK |
272 | node.run("ip link add dev vx-1000 type vxlan id 1000 dstport 4789") |
273 | node.run("ip link set dev vx-1000 type vxlan nolearning") | |
274 | node.run("ip link set dev vx-1000 type vxlan local %s" % node_ip) | |
275 | node.run("ip link set dev vx-1000 type vxlan ttl 64") | |
276 | node.run("ip link set dev vx-1000 mtu 9152") | |
277 | node.run("ip link set dev vx-1000 type vxlan dev ipmr-lo group 239.1.1.100") | |
278 | node.run("ip link set dev vx-1000 up") | |
279 | ||
280 | # bridge attrs | |
281 | node.run("ip link set dev vx-1000 master bridge") | |
282 | node.run("/sbin/bridge link set dev vx-1000 neigh_suppress on") | |
283 | node.run("/sbin/bridge link set dev vx-1000 learning off") | |
284 | node.run("/sbin/bridge link set dev vx-1000 priority 8") | |
285 | node.run("/sbin/bridge vlan del vid 1 dev vx-1000") | |
286 | node.run("/sbin/bridge vlan del vid 1 untagged pvid dev vx-1000") | |
287 | node.run("/sbin/bridge vlan add vid 1000 dev vx-1000") | |
288 | node.run("/sbin/bridge vlan add vid 1000 untagged pvid dev vx-1000") | |
289 | ||
290 | ||
291 | def config_svi(node, svi_pip): | |
701a0192 | 292 | """ |
df98b92c | 293 | Create an SVI for VLAN 1000 |
701a0192 | 294 | """ |
df98b92c AK |
295 | node.run("ip link add link bridge name vlan1000 type vlan id 1000 protocol 802.1q") |
296 | node.run("ip addr add %s/24 dev vlan1000" % svi_pip) | |
297 | node.run("ip link set dev vlan1000 up") | |
298 | node.run("/sbin/sysctl net.ipv4.conf.vlan1000.arp_accept=1") | |
299 | node.run("ip link add link vlan1000 name vlan1000-v0 type macvlan mode private") | |
300 | node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.accept_dad=0") | |
301 | node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.dad_transmits") | |
302 | node.run("/sbin/sysctl net.ipv6.conf.vlan1000-v0.dad_transmits=0") | |
303 | node.run("ip link set dev vlan1000-v0 address 00:00:5e:00:01:01") | |
304 | node.run("ip link set dev vlan1000-v0 up") | |
305 | # metric 1024 is not working | |
306 | node.run("ip addr add 45.0.0.1/24 dev vlan1000-v0") | |
307 | ||
308 | ||
309 | def config_tor(tor_name, tor, tor_ip, svi_pip): | |
701a0192 | 310 | """ |
df98b92c | 311 | Create the bond/vxlan-bridge on the TOR which acts as VTEP and EPN-PE |
701a0192 | 312 | """ |
df98b92c AK |
313 | # create a device for terminating VxLAN multicast tunnels |
314 | config_mcast_tunnel_termination_device(tor) | |
315 | ||
316 | # create a vlan aware bridge | |
317 | config_bridge(tor) | |
318 | ||
319 | # create vxlan device and add it to bridge | |
320 | config_vxlan(tor, tor_ip) | |
321 | ||
322 | # create hostbonds and add them to the bridge | |
323 | if "torm1" in tor_name: | |
324 | sys_mac = "44:38:39:ff:ff:01" | |
325 | else: | |
326 | sys_mac = "44:38:39:ff:ff:02" | |
327 | bond_member = tor_name + "-eth2" | |
328 | config_bond(tor, "hostbond1", [bond_member], sys_mac, "bridge") | |
329 | ||
330 | bond_member = tor_name + "-eth3" | |
331 | config_bond(tor, "hostbond2", [bond_member], sys_mac, "bridge") | |
332 | ||
333 | # create SVI | |
334 | config_svi(tor, svi_pip) | |
335 | ||
336 | ||
337 | def config_tors(tgen, tors): | |
338 | for tor_name in tors: | |
339 | tor = tgen.gears[tor_name] | |
340 | config_tor(tor_name, tor, tor_ips.get(tor_name), svi_ips.get(tor_name)) | |
341 | ||
701a0192 | 342 | |
df98b92c AK |
343 | def compute_host_ip_mac(host_name): |
344 | host_id = host_name.split("hostd")[1] | |
701a0192 | 345 | host_ip = "45.0.0." + host_id + "/24" |
df98b92c AK |
346 | host_mac = "00:00:00:00:00:" + host_id |
347 | ||
348 | return host_ip, host_mac | |
349 | ||
701a0192 | 350 | |
df98b92c | 351 | def config_host(host_name, host): |
701a0192 | 352 | """ |
df98b92c | 353 | Create the dual-attached bond on host nodes for MH |
701a0192 | 354 | """ |
df98b92c AK |
355 | bond_members = [] |
356 | bond_members.append(host_name + "-eth0") | |
357 | bond_members.append(host_name + "-eth1") | |
358 | bond_name = "torbond" | |
359 | config_bond(host, bond_name, bond_members, "00:00:00:00:00:00", None) | |
360 | ||
361 | host_ip, host_mac = compute_host_ip_mac(host_name) | |
362 | host.run("ip addr add %s dev %s" % (host_ip, bond_name)) | |
363 | host.run("ip link set dev %s address %s" % (bond_name, host_mac)) | |
364 | ||
365 | ||
366 | def config_hosts(tgen, hosts): | |
367 | for host_name in hosts: | |
368 | host = tgen.gears[host_name] | |
369 | config_host(host_name, host) | |
370 | ||
7ed8fcff | 371 | |
df98b92c AK |
372 | def setup_module(module): |
373 | "Setup topology" | |
374 | tgen = Topogen(NetworkTopo, module.__name__) | |
375 | tgen.start_topology() | |
376 | ||
377 | krel = platform.release() | |
378 | if topotest.version_cmp(krel, "4.19") < 0: | |
379 | tgen.errors = "kernel 4.19 needed for multihoming tests" | |
380 | pytest.skip(tgen.errors) | |
381 | ||
382 | tors = [] | |
383 | tors.append("torm11") | |
384 | tors.append("torm12") | |
385 | tors.append("torm21") | |
386 | tors.append("torm22") | |
387 | config_tors(tgen, tors) | |
388 | ||
389 | hosts = [] | |
390 | hosts.append("hostd11") | |
391 | hosts.append("hostd12") | |
392 | hosts.append("hostd21") | |
393 | hosts.append("hostd22") | |
394 | config_hosts(tgen, hosts) | |
395 | ||
396 | # tgen.mininet_cli() | |
397 | # This is a sample of configuration loading. | |
398 | router_list = tgen.routers() | |
e5f0ed14 | 399 | for rname, router in router_list.items(): |
df98b92c AK |
400 | router.load_config( |
401 | TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname)) | |
402 | ) | |
403 | router.load_config( | |
404 | TopoRouter.RD_PIM, os.path.join(CWD, "{}/pim.conf".format(rname)) | |
405 | ) | |
406 | router.load_config( | |
407 | TopoRouter.RD_BGP, os.path.join(CWD, "{}/evpn.conf".format(rname)) | |
408 | ) | |
409 | tgen.start_router() | |
410 | # tgen.mininet_cli() | |
411 | ||
412 | ||
413 | def teardown_module(_mod): | |
414 | "Teardown the pytest environment" | |
415 | tgen = get_topogen() | |
416 | ||
417 | # This function tears down the whole topology. | |
418 | tgen.stop_topology() | |
419 | ||
420 | ||
421 | def check_local_es(esi, vtep_ips, dut_name, down_vteps): | |
701a0192 | 422 | """ |
df98b92c | 423 | Check if ES peers are setup correctly on local ESs |
701a0192 | 424 | """ |
df98b92c AK |
425 | peer_ips = [] |
426 | if "torm1" in dut_name: | |
427 | tor_ips_rack = tor_ips_rack_1 | |
428 | else: | |
429 | tor_ips_rack = tor_ips_rack_2 | |
430 | ||
e5f0ed14 | 431 | for tor_name, tor_ip in tor_ips_rack.items(): |
df98b92c AK |
432 | if dut_name not in tor_name: |
433 | peer_ips.append(tor_ip) | |
434 | ||
435 | # remove down VTEPs from the peer check list | |
436 | peer_set = set(peer_ips) | |
437 | down_vtep_set = set(down_vteps) | |
438 | peer_set = peer_set - down_vtep_set | |
439 | ||
440 | vtep_set = set(vtep_ips) | |
441 | diff = peer_set.symmetric_difference(vtep_set) | |
442 | ||
443 | return (esi, diff) if diff else None | |
444 | ||
445 | ||
446 | def check_remote_es(esi, vtep_ips, dut_name, down_vteps): | |
701a0192 | 447 | """ |
df98b92c | 448 | Verify list of PEs associated with a remote ES |
701a0192 | 449 | """ |
df98b92c AK |
450 | remote_ips = [] |
451 | ||
452 | if "torm1" in dut_name: | |
453 | tor_ips_rack = tor_ips_rack_2 | |
454 | else: | |
455 | tor_ips_rack = tor_ips_rack_1 | |
456 | ||
e5f0ed14 | 457 | for tor_name, tor_ip in tor_ips_rack.items(): |
df98b92c AK |
458 | remote_ips.append(tor_ip) |
459 | ||
460 | # remove down VTEPs from the remote check list | |
461 | remote_set = set(remote_ips) | |
462 | down_vtep_set = set(down_vteps) | |
463 | remote_set = remote_set - down_vtep_set | |
464 | ||
465 | vtep_set = set(vtep_ips) | |
466 | diff = remote_set.symmetric_difference(vtep_set) | |
467 | ||
468 | return (esi, diff) if diff else None | |
469 | ||
701a0192 | 470 | |
df98b92c | 471 | def check_es(dut): |
701a0192 | 472 | """ |
df98b92c | 473 | Verify list of PEs associated all ESs, local and remote |
701a0192 | 474 | """ |
df98b92c AK |
475 | bgp_es = dut.vtysh_cmd("show bgp l2vp evpn es json") |
476 | bgp_es_json = json.loads(bgp_es) | |
477 | ||
478 | result = None | |
479 | ||
e5f0ed14 | 480 | expected_es_set = set([v for k, v in host_es_map.items()]) |
df98b92c AK |
481 | curr_es_set = [] |
482 | ||
483 | # check is ES content is correct | |
484 | for es in bgp_es_json: | |
485 | esi = es["esi"] | |
486 | curr_es_set.append(esi) | |
487 | types = es["type"] | |
488 | vtep_ips = [] | |
6bf6282d | 489 | for vtep in es.get("vteps", []): |
df98b92c AK |
490 | vtep_ips.append(vtep["vtep_ip"]) |
491 | ||
492 | if "local" in types: | |
11761ab0 | 493 | result = check_local_es(esi, vtep_ips, dut.name, []) |
df98b92c | 494 | else: |
11761ab0 | 495 | result = check_remote_es(esi, vtep_ips, dut.name, []) |
df98b92c AK |
496 | |
497 | if result: | |
498 | return result | |
499 | ||
500 | # check if all ESs are present | |
501 | curr_es_set = set(curr_es_set) | |
502 | result = curr_es_set.symmetric_difference(expected_es_set) | |
503 | ||
504 | return result if result else None | |
505 | ||
701a0192 | 506 | |
df98b92c | 507 | def check_one_es(dut, esi, down_vteps): |
701a0192 | 508 | """ |
df98b92c | 509 | Verify list of PEs associated all ESs, local and remote |
701a0192 | 510 | """ |
df98b92c AK |
511 | bgp_es = dut.vtysh_cmd("show bgp l2vp evpn es %s json" % esi) |
512 | es = json.loads(bgp_es) | |
513 | ||
514 | if not es: | |
515 | return "esi %s not found" % esi | |
516 | ||
517 | esi = es["esi"] | |
518 | types = es["type"] | |
519 | vtep_ips = [] | |
6bf6282d | 520 | for vtep in es.get("vteps", []): |
df98b92c AK |
521 | vtep_ips.append(vtep["vtep_ip"]) |
522 | ||
523 | if "local" in types: | |
524 | result = check_local_es(esi, vtep_ips, dut.name, down_vteps) | |
525 | else: | |
526 | result = check_remote_es(esi, vtep_ips, dut.name, down_vteps) | |
527 | ||
528 | return result | |
529 | ||
701a0192 | 530 | |
df98b92c | 531 | def test_evpn_es(): |
701a0192 | 532 | """ |
df98b92c AK |
533 | Two ES are setup on each rack. This test checks if - |
534 | 1. ES peer has been added to the local ES (via Type-1/EAD route) | |
535 | 2. The remote ESs are setup with the right list of PEs (via Type-1) | |
701a0192 | 536 | """ |
df98b92c AK |
537 | |
538 | tgen = get_topogen() | |
539 | ||
540 | if tgen.routers_have_failure(): | |
541 | pytest.skip(tgen.errors) | |
542 | ||
543 | dut_name = "torm11" | |
544 | dut = tgen.gears[dut_name] | |
545 | test_fn = partial(check_es, dut) | |
546 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
547 | ||
548 | assertmsg = '"{}" ES content incorrect'.format(dut_name) | |
549 | assert result is None, assertmsg | |
550 | # tgen.mininet_cli() | |
551 | ||
701a0192 | 552 | |
df98b92c | 553 | def test_evpn_ead_update(): |
701a0192 | 554 | """ |
df98b92c AK |
555 | Flap a host link one the remote rack and check if the EAD updates |
556 | are sent/processed for the corresponding ESI | |
701a0192 | 557 | """ |
df98b92c AK |
558 | tgen = get_topogen() |
559 | ||
560 | if tgen.routers_have_failure(): | |
561 | pytest.skip(tgen.errors) | |
562 | ||
563 | # dut on rack1 and host link flap on rack2 | |
564 | dut_name = "torm11" | |
565 | dut = tgen.gears[dut_name] | |
566 | ||
567 | remote_tor_name = "torm21" | |
568 | remote_tor = tgen.gears[remote_tor_name] | |
569 | ||
570 | host_name = "hostd21" | |
571 | host = tgen.gears[host_name] | |
572 | esi = host_es_map.get(host_name) | |
573 | ||
574 | # check if the VTEP list is right to start with | |
575 | down_vteps = [] | |
576 | test_fn = partial(check_one_es, dut, esi, down_vteps) | |
577 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
578 | assertmsg = '"{}" ES content incorrect'.format(dut_name) | |
579 | assert result is None, assertmsg | |
580 | ||
581 | # down a remote host link and check if the EAD withdraw is rxed | |
582 | # Note: LACP is not working as expected so I am temporarily shutting | |
583 | # down the link on the remote TOR instead of the remote host | |
584 | remote_tor.run("ip link set dev %s-%s down" % (remote_tor_name, "eth2")) | |
585 | down_vteps.append(tor_ips.get(remote_tor_name)) | |
586 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
587 | assertmsg = '"{}" ES incorrect after remote link down'.format(dut_name) | |
588 | assert result is None, assertmsg | |
589 | ||
590 | # bring up remote host link and check if the EAD update is rxed | |
591 | down_vteps.remove(tor_ips.get(remote_tor_name)) | |
592 | remote_tor.run("ip link set dev %s-%s up" % (remote_tor_name, "eth2")) | |
593 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
594 | assertmsg = '"{}" ES incorrect after remote link flap'.format(dut_name) | |
595 | assert result is None, assertmsg | |
596 | ||
597 | # tgen.mininet_cli() | |
598 | ||
701a0192 | 599 | |
6bf6282d | 600 | def ping_anycast_gw(tgen): |
6bf6282d AK |
601 | # ping the anycast gw from the local and remote hosts to populate |
602 | # the mac address on the PEs | |
49581587 | 603 | python3_path = tgen.net.get_exec_path(["python3", "python"]) |
002e6825 CH |
604 | script_path = os.path.abspath(os.path.join(CWD, "../lib/scapy_sendpkt.py")) |
605 | intf = "torbond" | |
606 | ipaddr = "45.0.0.1" | |
607 | ping_cmd = [ | |
49581587 | 608 | python3_path, |
002e6825 CH |
609 | script_path, |
610 | "--imports=Ether,ARP", | |
611 | "--interface=" + intf, | |
49581587 | 612 | 'Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="{}")'.format(ipaddr) |
002e6825 CH |
613 | ] |
614 | for name in ("hostd11", "hostd21"): | |
49581587 CH |
615 | host = tgen.net.hosts[name] |
616 | _, stdout, _ = host.cmd_status(ping_cmd, warn=False, stderr=subprocess.STDOUT) | |
002e6825 CH |
617 | stdout = stdout.strip() |
618 | if stdout: | |
619 | host.logger.debug("%s: arping on %s for %s returned: %s", name, intf, ipaddr, stdout) | |
6bf6282d | 620 | |
5980ad0a | 621 | |
6bf6282d | 622 | def check_mac(dut, vni, mac, m_type, esi, intf, ping_gw=False, tgen=None): |
701a0192 | 623 | """ |
df98b92c | 624 | checks if mac is present and if desination matches the one provided |
701a0192 | 625 | """ |
df98b92c | 626 | |
6bf6282d AK |
627 | if ping_gw: |
628 | ping_anycast_gw(tgen) | |
629 | ||
df98b92c AK |
630 | out = dut.vtysh_cmd("show evpn mac vni %d mac %s json" % (vni, mac)) |
631 | ||
632 | mac_js = json.loads(out) | |
e5f0ed14 | 633 | for mac, info in mac_js.items(): |
df98b92c | 634 | tmp_esi = info.get("esi", "") |
701a0192 | 635 | tmp_m_type = info.get("type", "") |
df98b92c AK |
636 | tmp_intf = info.get("intf", "") if tmp_m_type == "local" else "" |
637 | if tmp_esi == esi and tmp_m_type == m_type and intf == intf: | |
638 | return None | |
639 | ||
640 | return "invalid vni %d mac %s out %s" % (vni, mac, mac_js) | |
641 | ||
701a0192 | 642 | |
df98b92c | 643 | def test_evpn_mac(): |
701a0192 | 644 | """ |
df98b92c AK |
645 | 1. Add a MAC on hostd11 and check if the MAC is synced between |
646 | torm11 and torm12. And installed as a local MAC. | |
647 | 2. Add a MAC on hostd21 and check if the MAC is installed as a | |
648 | remote MAC on torm11 and torm12 | |
701a0192 | 649 | """ |
df98b92c AK |
650 | |
651 | tgen = get_topogen() | |
652 | ||
653 | local_host = tgen.gears["hostd11"] | |
654 | remote_host = tgen.gears["hostd21"] | |
655 | tors = [] | |
656 | tors.append(tgen.gears["torm11"]) | |
657 | tors.append(tgen.gears["torm12"]) | |
658 | ||
df98b92c AK |
659 | vni = 1000 |
660 | ||
661 | # check if the rack-1 host MAC is present on all rack-1 PEs | |
662 | # and points to local access port | |
663 | m_type = "local" | |
664 | _, mac = compute_host_ip_mac(local_host.name) | |
665 | esi = host_es_map.get(local_host.name) | |
666 | intf = "hostbond1" | |
667 | ||
668 | for tor in tors: | |
6bf6282d | 669 | test_fn = partial(check_mac, tor, vni, mac, m_type, esi, intf, True, tgen) |
df98b92c AK |
670 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) |
671 | assertmsg = '"{}" local MAC content incorrect'.format(tor.name) | |
672 | assert result is None, assertmsg | |
673 | ||
674 | # check if the rack-2 host MAC is present on all rack-1 PEs | |
675 | # and points to the remote ES destination | |
676 | m_type = "remote" | |
677 | _, mac = compute_host_ip_mac(remote_host.name) | |
678 | esi = host_es_map.get(remote_host.name) | |
679 | intf = "" | |
680 | ||
681 | for tor in tors: | |
682 | test_fn = partial(check_mac, tor, vni, mac, m_type, esi, intf) | |
683 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
684 | assertmsg = '"{}" remote MAC content incorrect'.format(tor.name) | |
685 | assert result is None, assertmsg | |
686 | ||
9fa6ec14 | 687 | |
732a1ac9 | 688 | def check_df_role(dut, esi, role): |
9fa6ec14 | 689 | """ |
732a1ac9 | 690 | Return error string if the df role on the dut is different |
9fa6ec14 | 691 | """ |
732a1ac9 AK |
692 | es_json = dut.vtysh_cmd("show evpn es %s json" % esi) |
693 | es = json.loads(es_json) | |
694 | ||
695 | if not es: | |
696 | return "esi %s not found" % esi | |
697 | ||
698 | flags = es.get("flags", []) | |
699 | curr_role = "nonDF" if "nonDF" in flags else "DF" | |
700 | ||
701 | if curr_role != role: | |
702 | return "%s is %s for %s" % (dut.name, curr_role, esi) | |
703 | ||
704 | return None | |
705 | ||
9fa6ec14 | 706 | |
732a1ac9 | 707 | def test_evpn_df(): |
9fa6ec14 | 708 | """ |
732a1ac9 AK |
709 | 1. Check the DF role on all the PEs on rack-1. |
710 | 2. Increase the DF preference on the non-DF and check if it becomes | |
711 | the DF winner. | |
9fa6ec14 | 712 | """ |
732a1ac9 AK |
713 | |
714 | tgen = get_topogen() | |
715 | ||
716 | if tgen.routers_have_failure(): | |
717 | pytest.skip(tgen.errors) | |
718 | ||
719 | # We will run the tests on just one ES | |
720 | esi = host_es_map.get("hostd11") | |
721 | intf = "hostbond1" | |
722 | ||
723 | tors = [] | |
724 | tors.append(tgen.gears["torm11"]) | |
725 | tors.append(tgen.gears["torm12"]) | |
726 | df_node = "torm11" | |
727 | ||
728 | # check roles on rack-1 | |
729 | for tor in tors: | |
730 | role = "DF" if tor.name == df_node else "nonDF" | |
731 | test_fn = partial(check_df_role, tor, esi, role) | |
732 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
733 | assertmsg = '"{}" DF role incorrect'.format(tor.name) | |
734 | assert result is None, assertmsg | |
735 | ||
736 | # change df preference on the nonDF to make it the df | |
737 | torm12 = tgen.gears["torm12"] | |
738 | torm12.vtysh_cmd("conf\ninterface %s\nevpn mh es-df-pref %d" % (intf, 60000)) | |
739 | df_node = "torm12" | |
740 | ||
741 | # re-check roles on rack-1; we should have a new winner | |
742 | for tor in tors: | |
743 | role = "DF" if tor.name == df_node else "nonDF" | |
744 | test_fn = partial(check_df_role, tor, esi, role) | |
745 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
746 | assertmsg = '"{}" DF role incorrect'.format(tor.name) | |
747 | assert result is None, assertmsg | |
748 | ||
749 | # tgen.mininet_cli() | |
701a0192 | 750 | |
9fa6ec14 | 751 | |
f93333e9 | 752 | def check_protodown_rc(dut, protodown_rc): |
9fa6ec14 | 753 | """ |
f93333e9 | 754 | check if specified protodown reason code is set |
9fa6ec14 | 755 | """ |
f93333e9 AK |
756 | |
757 | out = dut.vtysh_cmd("show evpn json") | |
758 | ||
759 | evpn_js = json.loads(out) | |
760 | tmp_rc = evpn_js.get("protodownReasons", []) | |
761 | ||
762 | if protodown_rc: | |
763 | if protodown_rc not in tmp_rc: | |
764 | return "protodown %s missing in %s" % (protodown_rc, tmp_rc) | |
765 | else: | |
766 | if tmp_rc: | |
767 | return "unexpected protodown rc %s" % (tmp_rc) | |
768 | ||
769 | return None | |
770 | ||
9fa6ec14 | 771 | |
f93333e9 | 772 | def test_evpn_uplink_tracking(): |
9fa6ec14 | 773 | """ |
f93333e9 AK |
774 | 1. Wait for access ports to come out of startup-delay |
775 | 2. disable uplinks and check if access ports have been protodowned | |
776 | 3. enable uplinks and check if access ports have been moved out | |
777 | of protodown | |
9fa6ec14 | 778 | """ |
f93333e9 AK |
779 | |
780 | tgen = get_topogen() | |
781 | ||
782 | dut_name = "torm11" | |
783 | dut = tgen.gears[dut_name] | |
784 | ||
785 | # wait for protodown rc to clear after startup | |
786 | test_fn = partial(check_protodown_rc, dut, None) | |
787 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
788 | assertmsg = '"{}" protodown rc incorrect'.format(dut_name) | |
789 | assert result is None, assertmsg | |
790 | ||
791 | # disable the uplinks | |
792 | dut.run("ip link set %s-eth0 down" % dut_name) | |
793 | dut.run("ip link set %s-eth1 down" % dut_name) | |
794 | ||
795 | # check if the access ports have been protodowned | |
796 | test_fn = partial(check_protodown_rc, dut, "uplinkDown") | |
797 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
798 | assertmsg = '"{}" protodown rc incorrect'.format(dut_name) | |
799 | assert result is None, assertmsg | |
800 | ||
801 | # enable the uplinks | |
802 | dut.run("ip link set %s-eth0 up" % dut_name) | |
803 | dut.run("ip link set %s-eth1 up" % dut_name) | |
804 | ||
805 | # check if the access ports have been moved out of protodown | |
806 | test_fn = partial(check_protodown_rc, dut, None) | |
807 | _, result = topotest.run_and_expect(test_fn, None, count=20, wait=3) | |
808 | assertmsg = '"{}" protodown rc incorrect'.format(dut_name) | |
809 | assert result is None, assertmsg | |
810 | ||
9fa6ec14 | 811 | |
df98b92c AK |
812 | if __name__ == "__main__": |
813 | args = ["-s"] + sys.argv[1:] | |
814 | sys.exit(pytest.main(args)) |