]>
Commit | Line | Data |
---|---|---|
3aefe207 RZ |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # test_isis_topo1.py | |
5 | # Part of NetDEF Topology Tests | |
6 | # | |
7 | # Copyright (c) 2017 by | |
8 | # Network Device Education Foundation, Inc. ("NetDEF") | |
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_isis_topo1.py: Test ISIS topology. | |
27 | """ | |
28 | ||
67f1e9ed | 29 | import collections |
9e2201b8 | 30 | import functools |
67f1e9ed | 31 | import json |
3aefe207 | 32 | import os |
67f1e9ed | 33 | import re |
3aefe207 RZ |
34 | import sys |
35 | import pytest | |
cf469a23 | 36 | import time |
3aefe207 RZ |
37 | |
38 | CWD = os.path.dirname(os.path.realpath(__file__)) | |
39 | sys.path.append(os.path.join(CWD, '../')) | |
40 | ||
41 | # pylint: disable=C0413 | |
42 | from lib import topotest | |
43 | from lib.topogen import Topogen, TopoRouter, get_topogen | |
44 | from lib.topolog import logger | |
45 | ||
46 | from mininet.topo import Topo | |
47 | ||
48 | ||
49 | class ISISTopo1(Topo): | |
50 | "Simple two layer ISIS topology" | |
51 | def build(self, *_args, **_opts): | |
52 | "Build function" | |
53 | tgen = get_topogen(self) | |
54 | ||
55 | # Add ISIS routers: | |
56 | # r1 r2 | |
57 | # | sw1 | sw2 | |
58 | # r3 r4 | |
59 | # | | | |
60 | # sw3 sw4 | |
61 | # \ / | |
62 | # r5 | |
63 | for routern in range(1, 6): | |
64 | tgen.add_router('r{}'.format(routern)) | |
65 | ||
66 | # r1 <- sw1 -> r3 | |
67 | sw = tgen.add_switch('sw1') | |
68 | sw.add_link(tgen.gears['r1']) | |
69 | sw.add_link(tgen.gears['r3']) | |
70 | ||
71 | # r2 <- sw2 -> r4 | |
72 | sw = tgen.add_switch('sw2') | |
73 | sw.add_link(tgen.gears['r2']) | |
74 | sw.add_link(tgen.gears['r4']) | |
75 | ||
76 | # r3 <- sw3 -> r5 | |
77 | sw = tgen.add_switch('sw3') | |
78 | sw.add_link(tgen.gears['r3']) | |
79 | sw.add_link(tgen.gears['r5']) | |
80 | ||
81 | # r4 <- sw4 -> r5 | |
82 | sw = tgen.add_switch('sw4') | |
83 | sw.add_link(tgen.gears['r4']) | |
84 | sw.add_link(tgen.gears['r5']) | |
85 | ||
86 | ||
87 | def setup_module(mod): | |
88 | "Sets up the pytest environment" | |
89 | tgen = Topogen(ISISTopo1, mod.__name__) | |
90 | tgen.start_topology() | |
91 | ||
92 | # For all registered routers, load the zebra configuration file | |
93 | for rname, router in tgen.routers().iteritems(): | |
94 | router.load_config( | |
95 | TopoRouter.RD_ZEBRA, | |
96 | os.path.join(CWD, '{}/zebra.conf'.format(rname)) | |
97 | ) | |
98 | router.load_config( | |
99 | TopoRouter.RD_ISIS, | |
100 | os.path.join(CWD, '{}/isisd.conf'.format(rname)) | |
101 | ) | |
102 | ||
103 | # After loading the configurations, this function loads configured daemons. | |
104 | tgen.start_router() | |
105 | ||
fc7752c1 RZ |
106 | has_version_20 = False |
107 | for router in tgen.routers().values(): | |
108 | if router.has_version('<', '3'): | |
109 | has_version_20 = True | |
110 | ||
111 | if has_version_20: | |
112 | logger.info('Skipping ISIS tests for FRR 2.0') | |
113 | tgen.set_error('ISIS has convergence problems with IPv6') | |
114 | ||
3aefe207 RZ |
115 | |
116 | def teardown_module(mod): | |
117 | "Teardown the pytest environment" | |
118 | tgen = get_topogen() | |
119 | ||
120 | # This function tears down the whole topology. | |
121 | tgen.stop_topology() | |
122 | ||
123 | ||
124 | def test_isis_convergence(): | |
125 | "Wait for the protocol to converge before starting to test" | |
126 | tgen = get_topogen() | |
127 | # Don't run this test if we have any failure. | |
128 | if tgen.routers_have_failure(): | |
129 | pytest.skip(tgen.errors) | |
130 | ||
cf469a23 | 131 | logger.info("waiting for ISIS protocol to converge") |
67f1e9ed RZ |
132 | # Code to generate the json files. |
133 | # for rname, router in tgen.routers().iteritems(): | |
134 | # open('/tmp/{}_topology.json'.format(rname), 'w').write( | |
135 | # json.dumps(show_isis_topology(router), indent=2, sort_keys=True) | |
136 | # ) | |
9e2201b8 RZ |
137 | |
138 | for rname, router in tgen.routers().iteritems(): | |
139 | filename = '{0}/{1}/{1}_topology.json'.format(CWD, rname) | |
140 | expected = json.loads(open(filename).read()) | |
141 | ||
142 | def compare_isis_topology(router, expected): | |
143 | "Helper function to test ISIS topology convergence." | |
cf469a23 | 144 | actual = show_isis_topology(router) |
9e2201b8 RZ |
145 | return topotest.json_cmp(actual, expected) |
146 | ||
147 | test_func = functools.partial(compare_isis_topology, router, expected) | |
148 | (result, diff) = topotest.run_and_expect(test_func, None, | |
149 | wait=0.5, count=120) | |
150 | assert result, 'ISIS did not converge on {}:\n{}'.format(rname, diff) | |
151 | ||
3aefe207 | 152 | |
e4d08d5b RZ |
153 | def test_isis_route_installation(): |
154 | "Check whether all expected routes are present" | |
155 | tgen = get_topogen() | |
156 | # Don't run this test if we have any failure. | |
157 | if tgen.routers_have_failure(): | |
158 | pytest.skip(tgen.errors) | |
159 | ||
160 | logger.info('Checking routers for installed ISIS routes') | |
161 | ||
162 | # Check for routes in 'show ip route json' | |
163 | for rname, router in tgen.routers().iteritems(): | |
164 | filename = '{0}/{1}/{1}_route.json'.format(CWD, rname) | |
165 | expected = json.loads(open(filename, 'r').read()) | |
166 | actual = router.vtysh_cmd('show ip route json', isjson=True) | |
29614f56 RZ |
167 | |
168 | # Older FRR versions don't list interfaces in some ISIS routes | |
169 | if router.has_version('<', '3.1'): | |
170 | for network, routes in expected.iteritems(): | |
171 | for route in routes: | |
172 | if route['protocol'] != 'isis': | |
173 | continue | |
174 | ||
175 | for nexthop in route['nexthops']: | |
d43cd0fd RZ |
176 | nexthop.pop('interfaceIndex', None) |
177 | nexthop.pop('interfaceName', None) | |
29614f56 | 178 | |
e4d08d5b RZ |
179 | assertmsg = "Router '{}' routes mismatch".format(rname) |
180 | assert topotest.json_cmp(actual, expected) is None, assertmsg | |
181 | ||
182 | ||
2d013cda RZ |
183 | def test_isis_linux_route_installation(): |
184 | "Check whether all expected routes are present and installed in the OS" | |
185 | tgen = get_topogen() | |
186 | # Don't run this test if we have any failure. | |
187 | if tgen.routers_have_failure(): | |
188 | pytest.skip(tgen.errors) | |
189 | ||
190 | logger.info('Checking routers for installed ISIS routes in OS') | |
191 | ||
192 | # Check for routes in `ip route` | |
193 | for rname, router in tgen.routers().iteritems(): | |
194 | filename = '{0}/{1}/{1}_route_linux.json'.format(CWD, rname) | |
195 | expected = json.loads(open(filename, 'r').read()) | |
196 | actual = topotest.ip4_route(router) | |
29614f56 RZ |
197 | |
198 | # Older FRR versions install routes using different proto | |
199 | if router.has_version('<', '3.1'): | |
200 | for network, netoptions in expected.iteritems(): | |
201 | if 'proto' in netoptions and netoptions['proto'] == '187': | |
202 | netoptions['proto'] = 'zebra' | |
203 | ||
2d013cda RZ |
204 | assertmsg = "Router '{}' OS routes mismatch".format(rname) |
205 | assert topotest.json_cmp(actual, expected) is None, assertmsg | |
88f83773 RZ |
206 | |
207 | ||
208 | def test_isis_route6_installation(): | |
209 | "Check whether all expected routes are present" | |
210 | tgen = get_topogen() | |
211 | # Don't run this test if we have any failure. | |
212 | if tgen.routers_have_failure(): | |
213 | pytest.skip(tgen.errors) | |
214 | ||
215 | logger.info('Checking routers for installed ISIS IPv6 routes') | |
216 | ||
217 | # Check for routes in 'show ip route json' | |
218 | for rname, router in tgen.routers().iteritems(): | |
219 | filename = '{0}/{1}/{1}_route6.json'.format(CWD, rname) | |
220 | expected = json.loads(open(filename, 'r').read()) | |
221 | actual = router.vtysh_cmd('show ipv6 route json', isjson=True) | |
222 | ||
223 | # Older FRR versions don't list interfaces in some ISIS routes | |
224 | if router.has_version('<', '3.1'): | |
225 | for network, routes in expected.iteritems(): | |
226 | for route in routes: | |
227 | # Older versions display different metrics for IPv6 routes | |
228 | route.pop('metric', None) | |
229 | ||
230 | if route['protocol'] != 'isis': | |
231 | continue | |
232 | ||
233 | for nexthop in route['nexthops']: | |
234 | nexthop.pop('interfaceIndex', None) | |
235 | nexthop.pop('interfaceName', None) | |
236 | ||
237 | assertmsg = "Router '{}' routes mismatch".format(rname) | |
238 | assert topotest.json_cmp(actual, expected) is None, assertmsg | |
d4368260 RZ |
239 | |
240 | ||
241 | def test_isis_linux_route6_installation(): | |
242 | "Check whether all expected routes are present and installed in the OS" | |
243 | tgen = get_topogen() | |
244 | # Don't run this test if we have any failure. | |
245 | if tgen.routers_have_failure(): | |
246 | pytest.skip(tgen.errors) | |
247 | ||
248 | logger.info('Checking routers for installed ISIS IPv6 routes in OS') | |
249 | ||
250 | # Check for routes in `ip route` | |
251 | for rname, router in tgen.routers().iteritems(): | |
252 | filename = '{0}/{1}/{1}_route6_linux.json'.format(CWD, rname) | |
253 | expected = json.loads(open(filename, 'r').read()) | |
254 | actual = topotest.ip6_route(router) | |
255 | ||
256 | # Older FRR versions install routes using different proto | |
257 | if router.has_version('<', '3.1'): | |
258 | for network, netoptions in expected.iteritems(): | |
259 | if 'proto' in netoptions and netoptions['proto'] == '187': | |
260 | netoptions['proto'] = 'zebra' | |
261 | ||
262 | assertmsg = "Router '{}' OS routes mismatch".format(rname) | |
263 | assert topotest.json_cmp(actual, expected) is None, assertmsg | |
2d013cda RZ |
264 | |
265 | ||
3aefe207 RZ |
266 | def test_memory_leak(): |
267 | "Run the memory leak test and report results." | |
268 | tgen = get_topogen() | |
269 | if not tgen.is_memleak_enabled(): | |
270 | pytest.skip('Memory leak test/report is disabled') | |
271 | ||
272 | tgen.report_memory_leaks() | |
273 | ||
274 | ||
275 | if __name__ == '__main__': | |
276 | args = ["-s"] + sys.argv[1:] | |
277 | sys.exit(pytest.main(args)) | |
67f1e9ed RZ |
278 | |
279 | ||
280 | # | |
281 | # Auxiliary functions | |
282 | # | |
283 | ||
284 | ||
285 | def dict_merge(dct, merge_dct): | |
286 | """ | |
287 | Recursive dict merge. Inspired by :meth:``dict.update()``, instead of | |
288 | updating only top-level keys, dict_merge recurses down into dicts nested | |
289 | to an arbitrary depth, updating keys. The ``merge_dct`` is merged into | |
290 | ``dct``. | |
291 | :param dct: dict onto which the merge is executed | |
292 | :param merge_dct: dct merged into dct | |
293 | :return: None | |
294 | ||
295 | Source: | |
296 | https://gist.github.com/angstwad/bf22d1822c38a92ec0a9 | |
297 | """ | |
298 | for k, v in merge_dct.iteritems(): | |
299 | if (k in dct and isinstance(dct[k], dict) | |
300 | and isinstance(merge_dct[k], collections.Mapping)): | |
301 | dict_merge(dct[k], merge_dct[k]) | |
302 | else: | |
303 | dct[k] = merge_dct[k] | |
304 | ||
305 | ||
306 | def parse_topology(lines, level): | |
307 | """ | |
308 | Parse the output of 'show isis topology level-X' into a Python dict. | |
309 | """ | |
310 | areas = {} | |
67f1e9ed | 311 | area = None |
5836fac2 | 312 | ipv = None |
67f1e9ed RZ |
313 | |
314 | for line in lines: | |
5836fac2 RZ |
315 | area_match = re.match(r"Area (.+):", line) |
316 | if area_match: | |
67f1e9ed | 317 | area = area_match.group(1) |
5836fac2 RZ |
318 | if area not in areas: |
319 | areas[area] = { | |
320 | level: { | |
321 | 'ipv4': [], | |
322 | 'ipv6': [] | |
323 | } | |
324 | } | |
325 | ipv = None | |
326 | continue | |
327 | elif area is None: | |
67f1e9ed RZ |
328 | continue |
329 | ||
5836fac2 RZ |
330 | if re.match(r"IS\-IS paths to level-. routers that speak IPv6", line): |
331 | ipv = 'ipv6' | |
67f1e9ed | 332 | continue |
5836fac2 RZ |
333 | if re.match(r"IS\-IS paths to level-. routers that speak IP", line): |
334 | ipv = 'ipv4' | |
67f1e9ed RZ |
335 | continue |
336 | ||
337 | item_match = re.match( | |
338 | r"([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)", line) | |
339 | if item_match is not None: | |
16b31141 RZ |
340 | # Skip header |
341 | if (item_match.group(1) == 'Vertex' and | |
342 | item_match.group(2) == 'Type' and | |
343 | item_match.group(3) == 'Metric' and | |
344 | item_match.group(4) == 'Next-Hop' and | |
345 | item_match.group(5) == 'Interface' and | |
346 | item_match.group(6) == 'Parent'): | |
347 | continue | |
348 | ||
5836fac2 | 349 | areas[area][level][ipv].append({ |
67f1e9ed RZ |
350 | 'vertex': item_match.group(1), |
351 | 'type': item_match.group(2), | |
352 | 'metric': item_match.group(3), | |
353 | 'next-hop': item_match.group(4), | |
354 | 'interface': item_match.group(5), | |
355 | 'parent': item_match.group(6), | |
356 | }) | |
357 | continue | |
358 | ||
359 | item_match = re.match(r"([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)", line) | |
360 | if item_match is not None: | |
5836fac2 | 361 | areas[area][level][ipv].append({ |
67f1e9ed RZ |
362 | 'vertex': item_match.group(1), |
363 | 'type': item_match.group(2), | |
364 | 'metric': item_match.group(3), | |
365 | 'parent': item_match.group(4), | |
366 | }) | |
367 | continue | |
368 | ||
369 | item_match = re.match(r"([^ ]+)", line) | |
370 | if item_match is not None: | |
5836fac2 | 371 | areas[area][level][ipv].append({'vertex': item_match.group(1)}) |
67f1e9ed RZ |
372 | continue |
373 | ||
67f1e9ed RZ |
374 | return areas |
375 | ||
376 | ||
377 | def show_isis_topology(router): | |
378 | """ | |
379 | Get the ISIS topology in a dictionary format. | |
380 | ||
381 | Sample: | |
382 | { | |
383 | 'area-name': { | |
384 | 'level-1': [ | |
385 | { | |
386 | 'vertex': 'r1' | |
387 | } | |
388 | ], | |
389 | 'level-2': [ | |
390 | { | |
391 | 'vertex': '10.0.0.1/24', | |
392 | 'type': 'IP', | |
393 | 'parent': '0', | |
394 | 'metric': 'internal' | |
395 | } | |
396 | ] | |
397 | }, | |
398 | 'area-name-2': { | |
399 | 'level-2': [ | |
400 | { | |
401 | "interface": "rX-ethY", | |
402 | "metric": "Z", | |
403 | "next-hop": "rA", | |
404 | "parent": "rC(B)", | |
405 | "type": "TE-IS", | |
406 | "vertex": "rD" | |
407 | } | |
408 | ] | |
409 | } | |
410 | } | |
411 | """ | |
412 | l1out = topotest.normalize_text( | |
413 | router.vtysh_cmd('show isis topology level-1') | |
414 | ).splitlines() | |
415 | l2out = topotest.normalize_text( | |
416 | router.vtysh_cmd('show isis topology level-2') | |
417 | ).splitlines() | |
418 | ||
419 | l1 = parse_topology(l1out, 'level-1') | |
420 | l2 = parse_topology(l2out, 'level-2') | |
421 | ||
422 | dict_merge(l1, l2) | |
423 | return l1 |