]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topotest.py
Merge pull request #4482 from opensourcerouting/warnings-20190606
[mirror_frr.git] / tests / topotests / lib / topotest.py
CommitLineData
594b1259
MW
1#!/usr/bin/env python
2
3#
4# topotest.py
5# Library of helper functions for NetDEF Topology Tests
6#
7# Copyright (c) 2016 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
7bd28cfc 25import json
594b1259 26import os
50c40bde 27import errno
594b1259
MW
28import re
29import sys
fd858290 30import functools
594b1259
MW
31import glob
32import StringIO
33import subprocess
1fca63c1 34import tempfile
594b1259 35import platform
17070436 36import difflib
570f25d8 37import time
594b1259 38
6c131bd3
RZ
39from lib.topolog import logger
40
594b1259
MW
41from mininet.topo import Topo
42from mininet.net import Mininet
43from mininet.node import Node, OVSSwitch, Host
44from mininet.log import setLogLevel, info
45from mininet.cli import CLI
46from mininet.link import Intf
47
3668ed8d
RZ
48class json_cmp_result(object):
49 "json_cmp result class for better assertion messages"
50
51 def __init__(self):
52 self.errors = []
53
54 def add_error(self, error):
55 "Append error message to the result"
2db5888d
RZ
56 for line in error.splitlines():
57 self.errors.append(line)
3668ed8d
RZ
58
59 def has_errors(self):
60 "Returns True if there were errors, otherwise False."
61 return len(self.errors) > 0
62
7fe06d55
CF
63 def __str__(self):
64 return '\n'.join(self.errors)
65
da63d5b3 66
7bd28cfc
RZ
67def json_diff(d1, d2):
68 """
69 Returns a string with the difference between JSON data.
70 """
71 json_format_opts = {
72 'indent': 4,
73 'sort_keys': True,
74 }
75 dstr1 = json.dumps(d1, **json_format_opts)
76 dstr2 = json.dumps(d2, **json_format_opts)
77 return difflines(dstr2, dstr1, title1='Expected value', title2='Current value', n=0)
09e21b44 78
a82e5f9a
RZ
79
80def _json_list_cmp(list1, list2, parent, result):
81 "Handles list type entries."
82 # Check second list2 type
83 if not isinstance(list1, type([])) or not isinstance(list2, type([])):
84 result.add_error(
85 '{} has different type than expected '.format(parent) +
86 '(have {}, expected {}):\n{}'.format(
87 type(list1), type(list2), json_diff(list1, list2)))
88 return
89
90 # Check list size
91 if len(list2) > len(list1):
92 result.add_error(
93 '{} too few items '.format(parent) +
94 '(have {}, expected {}:\n {})'.format(
95 len(list1), len(list2),
96 json_diff(list1, list2)))
97 return
98
99 # List all unmatched items errors
100 unmatched = []
101 for expected in list2:
102 matched = False
103 for value in list1:
104 if json_cmp({'json': value}, {'json': expected}) is None:
105 matched = True
106 break
107
a82e5f9a
RZ
108 if not matched:
109 unmatched.append(expected)
110
111 # If there are unmatched items, error out.
112 if unmatched:
113 result.add_error(
114 '{} value is different (\n{})'.format(
115 parent, json_diff(list1, list2)))
116
117
566567e9 118def json_cmp(d1, d2):
09e21b44
RZ
119 """
120 JSON compare function. Receives two parameters:
121 * `d1`: json value
122 * `d2`: json subset which we expect
123
124 Returns `None` when all keys that `d1` has matches `d2`,
125 otherwise a string containing what failed.
126
127 Note: key absence can be tested by adding a key with value `None`.
128 """
3668ed8d
RZ
129 squeue = [(d1, d2, 'json')]
130 result = json_cmp_result()
a82e5f9a 131
09e21b44 132 for s in squeue:
3668ed8d 133 nd1, nd2, parent = s
a82e5f9a
RZ
134
135 # Handle JSON beginning with lists.
136 if isinstance(nd1, type([])) or isinstance(nd2, type([])):
137 _json_list_cmp(nd1, nd2, parent, result)
138 if result.has_errors():
139 return result
140 else:
141 return None
09e21b44
RZ
142
143 # Expect all required fields to exist.
a82e5f9a 144 s1, s2 = set(nd1), set(nd2)
09e21b44
RZ
145 s2_req = set([key for key in nd2 if nd2[key] is not None])
146 diff = s2_req - s1
147 if diff != set({}):
08533b7b
RZ
148 result.add_error('expected key(s) {} in {} (have {}):\n{}'.format(
149 str(list(diff)), parent, str(list(s1)), json_diff(nd1, nd2)))
09e21b44
RZ
150
151 for key in s2.intersection(s1):
152 # Test for non existence of key in d2
153 if nd2[key] is None:
08533b7b
RZ
154 result.add_error('"{}" should not exist in {} (have {}):\n{}'.format(
155 key, parent, str(s1), json_diff(nd1[key], nd2[key])))
3668ed8d 156 continue
a82e5f9a 157
09e21b44
RZ
158 # If nd1 key is a dict, we have to recurse in it later.
159 if isinstance(nd2[key], type({})):
dc0d3fc5
RZ
160 if not isinstance(nd1[key], type({})):
161 result.add_error(
162 '{}["{}"] has different type than expected '.format(parent, key) +
08533b7b
RZ
163 '(have {}, expected {}):\n{}'.format(
164 type(nd1[key]), type(nd2[key]), json_diff(nd1[key], nd2[key])))
dc0d3fc5 165 continue
3668ed8d
RZ
166 nparent = '{}["{}"]'.format(parent, key)
167 squeue.append((nd1[key], nd2[key], nparent))
09e21b44 168 continue
a82e5f9a 169
dc0d3fc5
RZ
170 # Check list items
171 if isinstance(nd2[key], type([])):
a82e5f9a 172 _json_list_cmp(nd1[key], nd2[key], parent, result)
dc0d3fc5
RZ
173 continue
174
09e21b44
RZ
175 # Compare JSON values
176 if nd1[key] != nd2[key]:
3668ed8d 177 result.add_error(
7bd28cfc
RZ
178 '{}["{}"] value is different (\n{})'.format(
179 parent, key, json_diff(nd1[key], nd2[key])))
3668ed8d
RZ
180 continue
181
182 if result.has_errors():
183 return result
09e21b44
RZ
184
185 return None
186
a82e5f9a 187
5cffda18
RZ
188def router_output_cmp(router, cmd, expected):
189 """
190 Runs `cmd` in router and compares the output with `expected`.
191 """
192 return difflines(normalize_text(router.vtysh_cmd(cmd)),
193 normalize_text(expected),
194 title1="Current output",
195 title2="Expected output")
196
197
198def router_json_cmp(router, cmd, data):
199 """
200 Runs `cmd` that returns JSON data (normally the command ends with 'json')
201 and compare with `data` contents.
202 """
203 return json_cmp(router.vtysh_cmd(cmd, isjson=True), data)
204
205
1fca63c1
RZ
206def run_and_expect(func, what, count=20, wait=3):
207 """
208 Run `func` and compare the result with `what`. Do it for `count` times
209 waiting `wait` seconds between tries. By default it tries 20 times with
210 3 seconds delay between tries.
211
212 Returns (True, func-return) on success or
213 (False, func-return) on failure.
5cffda18
RZ
214
215 ---
216
217 Helper functions to use with this function:
218 - router_output_cmp
219 - router_json_cmp
1fca63c1 220 """
fd858290
RZ
221 start_time = time.time()
222 func_name = "<unknown>"
223 if func.__class__ == functools.partial:
224 func_name = func.func.__name__
225 else:
226 func_name = func.__name__
227
228 logger.info(
229 "'{}' polling started (interval {} secs, maximum wait {} secs)".format(
230 func_name, wait, int(wait * count)))
231
1fca63c1
RZ
232 while count > 0:
233 result = func()
234 if result != what:
570f25d8 235 time.sleep(wait)
1fca63c1
RZ
236 count -= 1
237 continue
fd858290
RZ
238
239 end_time = time.time()
240 logger.info("'{}' succeeded after {:.2f} seconds".format(
241 func_name, end_time - start_time))
1fca63c1 242 return (True, result)
fd858290
RZ
243
244 end_time = time.time()
245 logger.error("'{}' failed after {:.2f} seconds".format(
246 func_name, end_time - start_time))
1fca63c1
RZ
247 return (False, result)
248
249
594b1259
MW
250def int2dpid(dpid):
251 "Converting Integer to DPID"
252
253 try:
254 dpid = hex(dpid)[2:]
255 dpid = '0'*(16-len(dpid))+dpid
256 return dpid
257 except IndexError:
258 raise Exception('Unable to derive default datapath ID - '
259 'please either specify a dpid or use a '
260 'canonical switch name such as s23.')
261
50c40bde
MW
262def pid_exists(pid):
263 "Check whether pid exists in the current process table."
264
265 if pid <= 0:
266 return False
267 try:
268 os.kill(pid, 0)
269 except OSError as err:
270 if err.errno == errno.ESRCH:
271 # ESRCH == No such process
272 return False
273 elif err.errno == errno.EPERM:
274 # EPERM clearly means there's a process to deny access to
275 return True
276 else:
277 # According to "man 2 kill" possible error values are
278 # (EINVAL, EPERM, ESRCH)
279 raise
280 else:
281 return True
282
bc2872fd 283def get_textdiff(text1, text2, title1="", title2="", **opts):
17070436
MW
284 "Returns empty string if same or formatted diff"
285
91733ef8 286 diff = '\n'.join(difflib.unified_diff(text1, text2,
bc2872fd 287 fromfile=title1, tofile=title2, **opts))
17070436
MW
288 # Clean up line endings
289 diff = os.linesep.join([s for s in diff.splitlines() if s])
290 return diff
291
bc2872fd 292def difflines(text1, text2, title1='', title2='', **opts):
1fca63c1
RZ
293 "Wrapper for get_textdiff to avoid string transformations."
294 text1 = ('\n'.join(text1.rstrip().splitlines()) + '\n').splitlines(1)
295 text2 = ('\n'.join(text2.rstrip().splitlines()) + '\n').splitlines(1)
bc2872fd 296 return get_textdiff(text1, text2, title1, title2, **opts)
1fca63c1
RZ
297
298def get_file(content):
299 """
300 Generates a temporary file in '/tmp' with `content` and returns the file name.
301 """
302 fde = tempfile.NamedTemporaryFile(mode='w', delete=False)
303 fname = fde.name
304 fde.write(content)
305 fde.close()
306 return fname
307
f7840f6b
RZ
308def normalize_text(text):
309 """
9683a1bb 310 Strips formating spaces/tabs, carriage returns and trailing whitespace.
f7840f6b
RZ
311 """
312 text = re.sub(r'[ \t]+', ' ', text)
313 text = re.sub(r'\r', '', text)
9683a1bb
RZ
314
315 # Remove whitespace in the middle of text.
316 text = re.sub(r'[ \t]+\n', '\n', text)
317 # Remove whitespace at the end of the text.
318 text = text.rstrip()
319
f7840f6b
RZ
320 return text
321
cc95fbd9 322def module_present_linux(module, load):
f2d6ce41
CF
323 """
324 Returns whether `module` is present.
325
326 If `load` is true, it will try to load it via modprobe.
327 """
328 with open('/proc/modules', 'r') as modules_file:
329 if module.replace('-','_') in modules_file.read():
330 return True
331 cmd = '/sbin/modprobe {}{}'.format('' if load else '-n ',
332 module)
333 if os.system(cmd) != 0:
334 return False
335 else:
336 return True
337
cc95fbd9
DS
338def module_present_freebsd(module, load):
339 return True
340
341def module_present(module, load=True):
342 if sys.platform.startswith("linux"):
28440fd9 343 return module_present_linux(module, load)
cc95fbd9 344 elif sys.platform.startswith("freebsd"):
28440fd9 345 return module_present_freebsd(module, load)
cc95fbd9 346
4190fe1e
RZ
347def version_cmp(v1, v2):
348 """
349 Compare two version strings and returns:
350
351 * `-1`: if `v1` is less than `v2`
352 * `0`: if `v1` is equal to `v2`
353 * `1`: if `v1` is greater than `v2`
354
355 Raises `ValueError` if versions are not well formated.
356 """
357 vregex = r'(?P<whole>\d+(\.(\d+))*)'
358 v1m = re.match(vregex, v1)
359 v2m = re.match(vregex, v2)
360 if v1m is None or v2m is None:
361 raise ValueError("got a invalid version string")
362
363 # Split values
364 v1g = v1m.group('whole').split('.')
365 v2g = v2m.group('whole').split('.')
366
367 # Get the longest version string
368 vnum = len(v1g)
369 if len(v2g) > vnum:
370 vnum = len(v2g)
371
372 # Reverse list because we are going to pop the tail
373 v1g.reverse()
374 v2g.reverse()
375 for _ in range(vnum):
376 try:
377 v1n = int(v1g.pop())
378 except IndexError:
379 while v2g:
380 v2n = int(v2g.pop())
381 if v2n > 0:
382 return -1
383 break
384
385 try:
386 v2n = int(v2g.pop())
387 except IndexError:
388 if v1n > 0:
389 return 1
390 while v1g:
391 v1n = int(v1g.pop())
392 if v1n > 0:
034237db 393 return 1
4190fe1e
RZ
394 break
395
396 if v1n > v2n:
397 return 1
398 if v1n < v2n:
399 return -1
400 return 0
401
f5612168
PG
402def interface_set_status(node, ifacename, ifaceaction=False, vrf_name=None):
403 if ifaceaction:
404 str_ifaceaction = 'no shutdown'
405 else:
406 str_ifaceaction = 'shutdown'
407 if vrf_name == None:
408 cmd = 'vtysh -c \"configure terminal\" -c \"interface {0}\" -c \"{1}\"'.format(ifacename, str_ifaceaction)
409 else:
410 cmd = 'vtysh -c \"configure terminal\" -c \"interface {0} vrf {1}\" -c \"{2}\"'.format(ifacename, vrf_name, str_ifaceaction)
411 node.run(cmd)
412
b220b3c8
PG
413def ip4_route_zebra(node, vrf_name=None):
414 """
415 Gets an output of 'show ip route' command. It can be used
416 with comparing the output to a reference
417 """
418 if vrf_name == None:
419 tmp = node.vtysh_cmd('show ip route')
420 else:
421 tmp = node.vtysh_cmd('show ip route vrf {0}'.format(vrf_name))
422 output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp)
41077aa1
CF
423
424 lines = output.splitlines()
425 header_found = False
0eff5820 426 while lines and (not lines[0].strip() or not header_found):
41077aa1
CF
427 if '> - selected route' in lines[0]:
428 header_found = True
429 lines = lines[1:]
430 return '\n'.join(lines)
b220b3c8 431
e394d9aa
MS
432def ip6_route_zebra(node, vrf_name=None):
433 """
434 Retrieves the output of 'show ipv6 route [vrf vrf_name]', then
435 canonicalizes it by eliding link-locals.
436 """
437
438 if vrf_name == None:
439 tmp = node.vtysh_cmd('show ipv6 route')
440 else:
441 tmp = node.vtysh_cmd('show ipv6 route vrf {0}'.format(vrf_name))
442
443 # Mask out timestamp
444 output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp)
445
446 # Mask out the link-local addresses
447 output = re.sub(r'fe80::[^ ]+,', 'fe80::XXXX:XXXX:XXXX:XXXX,', output)
448
449 lines = output.splitlines()
450 header_found = False
451 while lines and (not lines[0].strip() or not header_found):
452 if '> - selected route' in lines[0]:
453 header_found = True
454 lines = lines[1:]
455
456 return '\n'.join(lines)
457
458
2f726781
MW
459def proto_name_to_number(protocol):
460 return {
461 'bgp': '186',
462 'isis': '187',
463 'ospf': '188',
464 'rip': '189',
465 'ripng': '190',
466 'nhrp': '191',
467 'eigrp': '192',
468 'ldp': '193',
469 'sharp': '194',
470 'pbr': '195',
471 'static': '196'
472 }.get(protocol, protocol) # default return same as input
473
474
99a7a912
RZ
475def ip4_route(node):
476 """
477 Gets a structured return of the command 'ip route'. It can be used in
478 conjuction with json_cmp() to provide accurate assert explanations.
479
480 Return example:
481 {
482 '10.0.1.0/24': {
483 'dev': 'eth0',
484 'via': '172.16.0.1',
485 'proto': '188',
486 },
487 '10.0.2.0/24': {
488 'dev': 'eth1',
489 'proto': 'kernel',
490 }
491 }
492 """
493 output = normalize_text(node.run('ip route')).splitlines()
494 result = {}
495 for line in output:
496 columns = line.split(' ')
497 route = result[columns[0]] = {}
498 prev = None
499 for column in columns:
500 if prev == 'dev':
501 route['dev'] = column
502 if prev == 'via':
503 route['via'] = column
504 if prev == 'proto':
2f726781
MW
505 # translate protocol names back to numbers
506 route['proto'] = proto_name_to_number(column)
99a7a912
RZ
507 if prev == 'metric':
508 route['metric'] = column
509 if prev == 'scope':
510 route['scope'] = column
511 prev = column
512
513 return result
514
515def ip6_route(node):
516 """
517 Gets a structured return of the command 'ip -6 route'. It can be used in
518 conjuction with json_cmp() to provide accurate assert explanations.
519
520 Return example:
521 {
522 '2001:db8:1::/64': {
523 'dev': 'eth0',
524 'proto': '188',
525 },
526 '2001:db8:2::/64': {
527 'dev': 'eth1',
528 'proto': 'kernel',
529 }
530 }
531 """
532 output = normalize_text(node.run('ip -6 route')).splitlines()
533 result = {}
534 for line in output:
535 columns = line.split(' ')
536 route = result[columns[0]] = {}
537 prev = None
538 for column in columns:
539 if prev == 'dev':
540 route['dev'] = column
541 if prev == 'via':
542 route['via'] = column
543 if prev == 'proto':
2f726781
MW
544 # translate protocol names back to numbers
545 route['proto'] = proto_name_to_number(column)
99a7a912
RZ
546 if prev == 'metric':
547 route['metric'] = column
548 if prev == 'pref':
549 route['pref'] = column
550 prev = column
551
552 return result
553
570f25d8
RZ
554def sleep(amount, reason=None):
555 """
556 Sleep wrapper that registers in the log the amount of sleep
557 """
558 if reason is None:
559 logger.info('Sleeping for {} seconds'.format(amount))
560 else:
561 logger.info(reason + ' ({} seconds)'.format(amount))
562
563 time.sleep(amount)
564
4942f298
MW
565def checkAddressSanitizerError(output, router, component):
566 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
567
568 addressSantizerError = re.search('(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ', output)
569 if addressSantizerError:
570 sys.stderr.write("%s: %s triggered an exception by AddressSanitizer\n" % (router, component))
571 # Sanitizer Error found in log
572 pidMark = addressSantizerError.group(1)
573 addressSantizerLog = re.search('%s(.*)%s' % (pidMark, pidMark), output, re.DOTALL)
574 if addressSantizerLog:
575 callingTest = os.path.basename(sys._current_frames().values()[0].f_back.f_back.f_globals['__file__'])
576 callingProc = sys._getframe(2).f_code.co_name
577 with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile:
578 sys.stderr.write('\n'.join(addressSantizerLog.group(1).splitlines()) + '\n')
579 addrSanFile.write("## Error: %s\n\n" % addressSantizerError.group(2))
580 addrSanFile.write("### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" % (callingTest, callingProc, router))
581 addrSanFile.write(' '+ '\n '.join(addressSantizerLog.group(1).splitlines()) + '\n')
582 addrSanFile.write("\n---------------\n")
583 return True
6c131bd3 584 return False
4942f298 585
594b1259 586def addRouter(topo, name):
80eeefb7 587 "Adding a FRRouter (or Quagga) to Topology"
594b1259
MW
588
589 MyPrivateDirs = ['/etc/frr',
590 '/etc/quagga',
591 '/var/run/frr',
592 '/var/run/quagga',
593 '/var/log']
4b1d6d4d
DS
594 if sys.platform.startswith("linux"):
595 return topo.addNode(name, cls=LinuxRouter, privateDirs=MyPrivateDirs)
596 elif sys.platform.startswith("freebsd"):
597 return topo.addNode(name, cls=FreeBSDRouter, privateDirs=MyPrivateDirs)
594b1259 598
797e8dcf
RZ
599def set_sysctl(node, sysctl, value):
600 "Set a sysctl value and return None on success or an error string"
601 valuestr = '{}'.format(value)
602 command = "sysctl {0}={1}".format(sysctl, valuestr)
603 cmdret = node.cmd(command)
604
605 matches = re.search(r'([^ ]+) = ([^\s]+)', cmdret)
606 if matches is None:
607 return cmdret
608 if matches.group(1) != sysctl:
609 return cmdret
610 if matches.group(2) != valuestr:
611 return cmdret
612
613 return None
614
615def assert_sysctl(node, sysctl, value):
616 "Set and assert that the sysctl is set with the specified value."
617 assert set_sysctl(node, sysctl, value) is None
618
594b1259
MW
619
620class Router(Node):
621 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
622
2ab85530
RZ
623 def __init__(self, name, **params):
624 super(Router, self).__init__(name, **params)
0d5e41c6
RZ
625 self.logdir = params.get('logdir')
626
627 # If this topology is using old API and doesn't have logdir
628 # specified, then attempt to generate an unique logdir.
629 if self.logdir is None:
630 cur_test = os.environ['PYTEST_CURRENT_TEST']
631 self.logdir = ('/tmp/topotests/' +
632 cur_test[0:cur_test.find(".py")].replace('/', '.'))
633
634 # If the logdir is not created, then create it and set the
635 # appropriated permissions.
636 if not os.path.isdir(self.logdir):
637 os.system('mkdir -p ' + self.logdir + '/' + name)
638 os.system('chmod -R go+rw /tmp/topotests')
639
2ab85530 640 self.daemondir = None
447f2d5a 641 self.hasmpls = False
2ab85530
RZ
642 self.routertype = 'frr'
643 self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0,
644 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0,
4d45d6d3 645 'ldpd': 0, 'eigrpd': 0, 'nhrpd': 0, 'staticd': 0,
849d1ed9 646 'bfdd': 0, 'sharpd': 0}
8dd5077d 647 self.daemons_options = {'zebra': ''}
2a59a86b 648 self.reportCores = True
fb80b81b 649 self.version = None
2ab85530 650
edd2bdf6
RZ
651 def _config_frr(self, **params):
652 "Configure FRR binaries"
653 self.daemondir = params.get('frrdir')
654 if self.daemondir is None:
655 self.daemondir = '/usr/lib/frr'
656
657 zebra_path = os.path.join(self.daemondir, 'zebra')
658 if not os.path.isfile(zebra_path):
659 raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path))
660
661 def _config_quagga(self, **params):
662 "Configure Quagga binaries"
663 self.daemondir = params.get('quaggadir')
664 if self.daemondir is None:
665 self.daemondir = '/usr/lib/quagga'
666
667 zebra_path = os.path.join(self.daemondir, 'zebra')
668 if not os.path.isfile(zebra_path):
669 raise Exception("Quagga zebra binary doesn't exist at {}".format(zebra_path))
670
2ab85530
RZ
671 # pylint: disable=W0221
672 # Some params are only meaningful for the parent class.
594b1259
MW
673 def config(self, **params):
674 super(Router, self).config(**params)
675
2ab85530
RZ
676 # User did not specify the daemons directory, try to autodetect it.
677 self.daemondir = params.get('daemondir')
678 if self.daemondir is None:
edd2bdf6
RZ
679 self.routertype = params.get('routertype', 'frr')
680 if self.routertype == 'quagga':
681 self._config_quagga(**params)
682 else:
683 self._config_frr(**params)
594b1259 684 else:
2ab85530
RZ
685 # Test the provided path
686 zpath = os.path.join(self.daemondir, 'zebra')
687 if not os.path.isfile(zpath):
688 raise Exception('No zebra binary found in {}'.format(zpath))
689 # Allow user to specify routertype when the path was specified.
690 if params.get('routertype') is not None:
691 self.routertype = self.params.get('routertype')
692
594b1259
MW
693 self.cmd('ulimit -c unlimited')
694 # Set ownership of config files
2ab85530
RZ
695 self.cmd('chown {0}:{0}vty /etc/{0}'.format(self.routertype))
696
594b1259
MW
697 def terminate(self):
698 # Delete Running Quagga or FRR Daemons
99561211
MW
699 self.stopRouter()
700 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
701 # for d in StringIO.StringIO(rundaemons):
702 # self.cmd('kill -7 `cat %s`' % d.rstrip())
703 # self.waitOutput()
594b1259 704 # Disable forwarding
797e8dcf
RZ
705 set_sysctl(self, 'net.ipv4.ip_forward', 0)
706 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
594b1259 707 super(Router, self).terminate()
b0f0d980
LB
708 os.system('chmod -R go+rw /tmp/topotests')
709
dce382d4 710 def stopRouter(self, wait=True, assertOnError=True, minErrorVersion='5.1'):
99561211
MW
711 # Stop Running Quagga or FRR Daemons
712 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
83c26937 713 errors = ""
e600b2d9 714 if re.search(r"No such file or directory", rundaemons):
83c26937 715 return errors
99561211 716 if rundaemons is not None:
3a568b9c 717 numRunning = 0
7551168c
MW
718 for d in StringIO.StringIO(rundaemons):
719 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
720 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
065bd557
RZ
721 logger.info('{}: stopping {}'.format(
722 self.name,
723 os.path.basename(d.rstrip().rsplit(".", 1)[0])
724 ))
7551168c
MW
725 self.cmd('kill -TERM %s' % daemonpid)
726 self.waitOutput()
3a568b9c
LB
727 if pid_exists(int(daemonpid)):
728 numRunning += 1
729 if wait and numRunning > 0:
065bd557 730 sleep(2, '{}: waiting for daemons stopping'.format(self.name))
3a568b9c
LB
731 # 2nd round of kill if daemons didn't exit
732 for d in StringIO.StringIO(rundaemons):
733 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
734 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
065bd557
RZ
735 logger.info('{}: killing {}'.format(
736 self.name,
737 os.path.basename(d.rstrip().rsplit(".", 1)[0])
738 ))
3a568b9c
LB
739 self.cmd('kill -7 %s' % daemonpid)
740 self.waitOutput()
e600b2d9 741 self.cmd('rm -- {}'.format(d.rstrip()))
f76774ec 742 if wait:
83c26937 743 errors = self.checkRouterCores(reportOnce=True)
436aa319
LB
744 if self.checkRouterVersion('<', minErrorVersion):
745 #ignore errors in old versions
746 errors = ""
83c26937
LB
747 if assertOnError and len(errors) > 0:
748 assert "Errors found - details follow:" == 0, errors
749 return errors
f76774ec 750
594b1259
MW
751 def removeIPs(self):
752 for interface in self.intfNames():
753 self.cmd('ip address flush', interface)
8dd5077d
PG
754
755 def checkCapability(self, daemon, param):
756 if param is not None:
757 daemon_path = os.path.join(self.daemondir, daemon)
758 daemon_search_option = param.replace('-','')
759 output = self.cmd('{0} -h | grep {1}'.format(
760 daemon_path, daemon_search_option))
761 if daemon_search_option not in output:
762 return False
763 return True
764
765 def loadConf(self, daemon, source=None, param=None):
594b1259
MW
766 # print "Daemons before:", self.daemons
767 if daemon in self.daemons.keys():
768 self.daemons[daemon] = 1
8dd5077d
PG
769 if param is not None:
770 self.daemons_options[daemon] = param
594b1259
MW
771 if source is None:
772 self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon))
773 self.waitOutput()
774 else:
775 self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon))
776 self.waitOutput()
777 self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon))
778 self.waitOutput()
779 self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon))
780 self.waitOutput()
2c805e6c 781 if (daemon == 'zebra') and (self.daemons['staticd'] == 0):
a2a1134c
MW
782 # Add staticd with zebra - if it exists
783 staticd_path = os.path.join(self.daemondir, 'staticd')
784 if os.path.isfile(staticd_path):
785 self.daemons['staticd'] = 1
2c805e6c
MW
786 self.daemons_options['staticd'] = ''
787 # Auto-Started staticd has no config, so it will read from zebra config
594b1259 788 else:
222ea88b 789 logger.info('No daemon {} known'.format(daemon))
594b1259 790 # print "Daemons after:", self.daemons
e1dfa45e 791
9711fc7e 792 def startRouter(self, tgen=None):
594b1259 793 # Disable integrated-vtysh-config
a93477ec 794 self.cmd('echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' % self.routertype)
594b1259 795 self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype))
13e1fc49 796 # TODO remove the following lines after all tests are migrated to Topogen.
594b1259 797 # Try to find relevant old logfiles in /tmp and delete them
e1dfa45e 798 map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
594b1259 799 # Remove old core files
e1dfa45e 800 map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
594b1259
MW
801 # Remove IP addresses from OS first - we have them in zebra.conf
802 self.removeIPs()
803 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
804 # No error - but return message and skip all the tests
805 if self.daemons['ldpd'] == 1:
2ab85530
RZ
806 ldpd_path = os.path.join(self.daemondir, 'ldpd')
807 if not os.path.isfile(ldpd_path):
222ea88b 808 logger.info("LDP Test, but no ldpd compiled or installed")
594b1259 809 return "LDP Test, but no ldpd compiled or installed"
dd4eca4d 810
45619ee3 811 if version_cmp(platform.release(), '4.5') < 0:
222ea88b 812 logger.info("LDP Test need Linux Kernel 4.5 minimum")
45619ee3 813 return "LDP Test need Linux Kernel 4.5 minimum"
9711fc7e
LB
814 # Check if have mpls
815 if tgen != None:
816 self.hasmpls = tgen.hasmpls
817 if self.hasmpls != True:
818 logger.info("LDP/MPLS Tests will be skipped, platform missing module(s)")
819 else:
820 # Test for MPLS Kernel modules available
821 self.hasmpls = False
f2d6ce41 822 if not module_present('mpls-router'):
9711fc7e 823 logger.info('MPLS tests will not run (missing mpls-router kernel module)')
f2d6ce41 824 elif not module_present('mpls-iptunnel'):
9711fc7e
LB
825 logger.info('MPLS tests will not run (missing mpls-iptunnel kernel module)')
826 else:
827 self.hasmpls = True
828 if self.hasmpls != True:
829 return "LDP/MPLS Tests need mpls kernel modules"
a93eb16a 830 self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels')
44a592b2
MW
831
832 if self.daemons['eigrpd'] == 1:
833 eigrpd_path = os.path.join(self.daemondir, 'eigrpd')
834 if not os.path.isfile(eigrpd_path):
222ea88b 835 logger.info("EIGRP Test, but no eigrpd compiled or installed")
44a592b2
MW
836 return "EIGRP Test, but no eigrpd compiled or installed"
837
4d45d6d3
RZ
838 if self.daemons['bfdd'] == 1:
839 bfdd_path = os.path.join(self.daemondir, 'bfdd')
840 if not os.path.isfile(bfdd_path):
841 logger.info("BFD Test, but no bfdd compiled or installed")
842 return "BFD Test, but no bfdd compiled or installed"
843
99561211
MW
844 self.restartRouter()
845 return ""
e1dfa45e 846
99561211 847 def restartRouter(self):
e1dfa45e
LB
848 # Starts actual daemons without init (ie restart)
849 # cd to per node directory
850 self.cmd('cd {}/{}'.format(self.logdir, self.name))
b0f0d980 851 self.cmd('umask 000')
2a59a86b
LB
852 #Re-enable to allow for report per run
853 self.reportCores = True
fb80b81b
LB
854 if self.version == None:
855 self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2]
856 logger.info('{}: running version: {}'.format(self.name,self.version))
594b1259
MW
857 # Start Zebra first
858 if self.daemons['zebra'] == 1:
2ab85530 859 zebra_path = os.path.join(self.daemondir, 'zebra')
8dd5077d 860 zebra_option = self.daemons_options['zebra']
e1dfa45e 861 self.cmd('{0} {1} > zebra.out 2> zebra.err &'.format(
8dd5077d 862 zebra_path, zebra_option, self.logdir, self.name
2ab85530 863 ))
594b1259 864 self.waitOutput()
6c131bd3 865 logger.debug('{}: {} zebra started'.format(self, self.routertype))
63038f4b 866 sleep(1, '{}: waiting for zebra to start'.format(self.name))
a2a1134c
MW
867 # Start staticd next if required
868 if self.daemons['staticd'] == 1:
869 staticd_path = os.path.join(self.daemondir, 'staticd')
870 staticd_option = self.daemons_options['staticd']
871 self.cmd('{0} {1} > staticd.out 2> staticd.err &'.format(
872 staticd_path, staticd_option, self.logdir, self.name
873 ))
874 self.waitOutput()
875 logger.debug('{}: {} staticd started'.format(self, self.routertype))
a2a1134c 876 # Fix Link-Local Addresses
594b1259
MW
877 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
878 self.cmd('for i in `ls /sys/class/net/` ; do mac=`cat /sys/class/net/$i/address`; IFS=\':\'; set $mac; unset IFS; ip address add dev $i scope link fe80::$(printf %02x $((0x$1 ^ 2)))$2:${3}ff:fe$4:$5$6/64; done')
879 # Now start all the other daemons
880 for daemon in self.daemons:
2ab85530 881 # Skip disabled daemons and zebra
a2a1134c 882 if self.daemons[daemon] == 0 or daemon == 'zebra' or daemon == 'staticd':
2ab85530 883 continue
2ab85530 884 daemon_path = os.path.join(self.daemondir, daemon)
86c21ac7 885 self.cmd('{0} {1} > {2}.out 2> {2}.err &'.format(
886 daemon_path, self.daemons_options.get(daemon, ''), daemon
887 ))
2ab85530 888 self.waitOutput()
6c131bd3 889 logger.debug('{}: {} {} started'.format(self, self.routertype, daemon))
99561211
MW
890 def getStdErr(self, daemon):
891 return self.getLog('err', daemon)
892 def getStdOut(self, daemon):
893 return self.getLog('out', daemon)
894 def getLog(self, log, daemon):
e1dfa45e 895 return self.cmd('cat {}/{}/{}.{}'.format(self.logdir, self.name, daemon, log))
f76774ec 896
2a59a86b
LB
897 def checkRouterCores(self, reportLeaks=True, reportOnce=False):
898 if reportOnce and not self.reportCores:
899 return
900 reportMade = False
83c26937 901 traces = ""
f76774ec
LB
902 for daemon in self.daemons:
903 if (self.daemons[daemon] == 1):
904 # Look for core file
905 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
906 self.logdir, self.name, daemon))
907 if (len(corefiles) > 0):
908 daemon_path = os.path.join(self.daemondir, daemon)
909 backtrace = subprocess.check_output([
910 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
911 ], shell=True)
912 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
913 sys.stderr.write("%s" % backtrace)
83c26937 914 traces = traces + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s" % (self.name, daemon, backtrace)
2a59a86b 915 reportMade = True
f76774ec
LB
916 elif reportLeaks:
917 log = self.getStdErr(daemon)
918 if "memstats" in log:
919 sys.stderr.write("%s: %s has memory leaks:\n" % (self.name, daemon))
83c26937 920 traces = traces + "\n%s: %s has memory leaks:\n" % (self.name, daemon)
f76774ec
LB
921 log = re.sub("core_handler: ", "", log)
922 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n ## \1", log)
923 log = re.sub("memstats: ", " ", log)
924 sys.stderr.write(log)
2a59a86b 925 reportMade = True
f76774ec
LB
926 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
927 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
928 sys.stderr.write("%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon))
83c26937 929 traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon)
2a59a86b
LB
930 reportMade = True
931 if reportMade:
932 self.reportCores = False
83c26937 933 return traces
f76774ec 934
594b1259 935 def checkRouterRunning(self):
597cabb7
MW
936 "Check if router daemons are running and collect crashinfo they don't run"
937
594b1259
MW
938 global fatal_error
939
940 daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"')
4942f298
MW
941 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
942 if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"):
943 return "%s: vtysh killed by AddressSanitizer" % (self.name)
944
594b1259
MW
945 for daemon in self.daemons:
946 if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
947 sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
d2132114
DS
948 if daemon is "staticd":
949 sys.stderr.write("You may have a copy of staticd installed but are attempting to test against\n")
950 sys.stderr.write("a version of FRR that does not have staticd, please cleanup the install dir\n")
951
594b1259 952 # Look for core file
e1dfa45e 953 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
13e1fc49 954 self.logdir, self.name, daemon))
594b1259 955 if (len(corefiles) > 0):
2ab85530
RZ
956 daemon_path = os.path.join(self.daemondir, daemon)
957 backtrace = subprocess.check_output([
958 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
959 ], shell=True)
594b1259
MW
960 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
961 sys.stderr.write("%s\n" % backtrace)
962 else:
963 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
e1dfa45e 964 if os.path.isfile('{}/{}/{}.log'.format(self.logdir, self.name, daemon)):
13e1fc49 965 log_tail = subprocess.check_output([
e1dfa45e 966 "tail -n20 {}/{}/{}.log 2> /dev/null".format(
13e1fc49
RZ
967 self.logdir, self.name, daemon)
968 ], shell=True)
594b1259
MW
969 sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon))
970 sys.stderr.write("%s\n" % log_tail)
4942f298 971
597cabb7 972 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
4942f298 973 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
84379e8e
MW
974 return "%s: Daemon %s not running - killed by AddressSanitizer" % (self.name, daemon)
975
594b1259
MW
976 return "%s: Daemon %s not running" % (self.name, daemon)
977 return ""
fb80b81b
LB
978
979 def checkRouterVersion(self, cmpop, version):
980 """
981 Compares router version using operation `cmpop` with `version`.
982 Valid `cmpop` values:
983 * `>=`: has the same version or greater
984 * '>': has greater version
985 * '=': has the same version
986 * '<': has a lesser version
987 * '<=': has the same version or lesser
988
989 Usage example: router.checkRouterVersion('>', '1.0')
990 """
6bfe4b8b
MW
991
992 # Make sure we have version information first
993 if self.version == None:
994 self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2]
995 logger.info('{}: running version: {}'.format(self.name,self.version))
996
fb80b81b
LB
997 rversion = self.version
998 if rversion is None:
999 return False
1000
1001 result = version_cmp(rversion, version)
1002 if cmpop == '>=':
1003 return result >= 0
1004 if cmpop == '>':
1005 return result > 0
1006 if cmpop == '=':
1007 return result == 0
1008 if cmpop == '<':
1009 return result < 0
1010 if cmpop == '<':
1011 return result < 0
1012 if cmpop == '<=':
1013 return result <= 0
1014
594b1259
MW
1015 def get_ipv6_linklocal(self):
1016 "Get LinkLocal Addresses from interfaces"
1017
1018 linklocal = []
1019
1020 ifaces = self.cmd('ip -6 address')
1021 # Fix newlines (make them all the same)
1022 ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines()
1023 interface=""
1024 ll_per_if_count=0
1025 for line in ifaces:
1026 m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line)
1027 if m:
1028 interface = m.group(1)
1029 ll_per_if_count = 0
1030 m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line)
1031 if m:
1032 local = m.group(1)
1033 ll_per_if_count += 1
1034 if (ll_per_if_count > 1):
1035 linklocal += [["%s-%s" % (interface, ll_per_if_count), local]]
1036 else:
1037 linklocal += [[interface, local]]
1038 return linklocal
80eeefb7
MW
1039 def daemon_available(self, daemon):
1040 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
1041
2ab85530
RZ
1042 daemon_path = os.path.join(self.daemondir, daemon)
1043 if not os.path.isfile(daemon_path):
80eeefb7
MW
1044 return False
1045 if (daemon == 'ldpd'):
b431b554
MW
1046 if version_cmp(platform.release(), '4.5') < 0:
1047 return False
f2d6ce41 1048 if not module_present('mpls-router', load=False):
80eeefb7 1049 return False
f2d6ce41 1050 if not module_present('mpls-iptunnel', load=False):
b431b554 1051 return False
80eeefb7 1052 return True
f2d6ce41 1053
80eeefb7
MW
1054 def get_routertype(self):
1055 "Return the type of Router (frr or quagga)"
1056
1057 return self.routertype
50c40bde
MW
1058 def report_memory_leaks(self, filename_prefix, testscript):
1059 "Report Memory Leaks to file prefixed with given string"
1060
1061 leakfound = False
1062 filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt"
1063 for daemon in self.daemons:
1064 if (self.daemons[daemon] == 1):
1065 log = self.getStdErr(daemon)
1066 if "memstats" in log:
1067 # Found memory leak
6c131bd3
RZ
1068 logger.info('\nRouter {} {} StdErr Log:\n{}'.format(
1069 self.name, daemon, log))
50c40bde
MW
1070 if not leakfound:
1071 leakfound = True
1072 # Check if file already exists
1073 fileexists = os.path.isfile(filename)
1074 leakfile = open(filename, "a")
1075 if not fileexists:
1076 # New file - add header
1077 leakfile.write("# Memory Leak Detection for topotest %s\n\n" % testscript)
1078 leakfile.write("## Router %s\n" % self.name)
1079 leakfile.write("### Process %s\n" % daemon)
1080 log = re.sub("core_handler: ", "", log)
1081 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n#### \1\n", log)
1082 log = re.sub("memstats: ", " ", log)
1083 leakfile.write(log)
1084 leakfile.write("\n")
1085 if leakfound:
1086 leakfile.close()
80eeefb7 1087
7cc96035 1088class LinuxRouter(Router):
4b1d6d4d 1089 "A Linux Router Node with IPv4/IPv6 forwarding enabled."
7cc96035
DS
1090
1091 def __init__(self, name, **params):
1092 Router.__init__(self, name, **params)
1093
1094 def config(self, **params):
1095 Router.config(self, **params)
1096 # Enable forwarding on the router
1097 assert_sysctl(self, 'net.ipv4.ip_forward', 1)
1098 assert_sysctl(self, 'net.ipv6.conf.all.forwarding', 1)
d29fb5bd
DS
1099 # Enable coredumps
1100 assert_sysctl(self, 'kernel.core_uses_pid', 1)
1101 assert_sysctl(self, 'fs.suid_dumpable', 1)
1102 #this applies to the kernel not the namespace...
1103 #original on ubuntu 17.x, but apport won't save as in namespace
1104 # |/usr/share/apport/apport %p %s %c %d %P
1105 corefile = '%e_core-sig_%s-pid_%p.dmp'
1106 assert_sysctl(self, 'kernel.core_pattern', corefile)
1107
7cc96035
DS
1108 def terminate(self):
1109 """
1110 Terminate generic LinuxRouter Mininet instance
1111 """
1112 set_sysctl(self, 'net.ipv4.ip_forward', 0)
1113 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
1114 Router.terminate(self)
594b1259 1115
4b1d6d4d
DS
1116class FreeBSDRouter(Router):
1117 "A FreeBSD Router Node with IPv4/IPv6 forwarding enabled."
1118
1119 def __init__(eslf, name, **params):
1120 Router.__init__(Self, name, **params)
1121
1122
594b1259
MW
1123class LegacySwitch(OVSSwitch):
1124 "A Legacy Switch without OpenFlow"
1125
1126 def __init__(self, name, **params):
1127 OVSSwitch.__init__(self, name, failMode='standalone', **params)
1128 self.switchIP = None