]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topotest.py
Merge pull request #3899 from ton31337/fix/remove_private_as_with_local_as
[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
2f726781
MW
432def proto_name_to_number(protocol):
433 return {
434 'bgp': '186',
435 'isis': '187',
436 'ospf': '188',
437 'rip': '189',
438 'ripng': '190',
439 'nhrp': '191',
440 'eigrp': '192',
441 'ldp': '193',
442 'sharp': '194',
443 'pbr': '195',
444 'static': '196'
445 }.get(protocol, protocol) # default return same as input
446
447
99a7a912
RZ
448def ip4_route(node):
449 """
450 Gets a structured return of the command 'ip route'. It can be used in
451 conjuction with json_cmp() to provide accurate assert explanations.
452
453 Return example:
454 {
455 '10.0.1.0/24': {
456 'dev': 'eth0',
457 'via': '172.16.0.1',
458 'proto': '188',
459 },
460 '10.0.2.0/24': {
461 'dev': 'eth1',
462 'proto': 'kernel',
463 }
464 }
465 """
466 output = normalize_text(node.run('ip route')).splitlines()
467 result = {}
468 for line in output:
469 columns = line.split(' ')
470 route = result[columns[0]] = {}
471 prev = None
472 for column in columns:
473 if prev == 'dev':
474 route['dev'] = column
475 if prev == 'via':
476 route['via'] = column
477 if prev == 'proto':
2f726781
MW
478 # translate protocol names back to numbers
479 route['proto'] = proto_name_to_number(column)
99a7a912
RZ
480 if prev == 'metric':
481 route['metric'] = column
482 if prev == 'scope':
483 route['scope'] = column
484 prev = column
485
486 return result
487
488def ip6_route(node):
489 """
490 Gets a structured return of the command 'ip -6 route'. It can be used in
491 conjuction with json_cmp() to provide accurate assert explanations.
492
493 Return example:
494 {
495 '2001:db8:1::/64': {
496 'dev': 'eth0',
497 'proto': '188',
498 },
499 '2001:db8:2::/64': {
500 'dev': 'eth1',
501 'proto': 'kernel',
502 }
503 }
504 """
505 output = normalize_text(node.run('ip -6 route')).splitlines()
506 result = {}
507 for line in output:
508 columns = line.split(' ')
509 route = result[columns[0]] = {}
510 prev = None
511 for column in columns:
512 if prev == 'dev':
513 route['dev'] = column
514 if prev == 'via':
515 route['via'] = column
516 if prev == 'proto':
2f726781
MW
517 # translate protocol names back to numbers
518 route['proto'] = proto_name_to_number(column)
99a7a912
RZ
519 if prev == 'metric':
520 route['metric'] = column
521 if prev == 'pref':
522 route['pref'] = column
523 prev = column
524
525 return result
526
570f25d8
RZ
527def sleep(amount, reason=None):
528 """
529 Sleep wrapper that registers in the log the amount of sleep
530 """
531 if reason is None:
532 logger.info('Sleeping for {} seconds'.format(amount))
533 else:
534 logger.info(reason + ' ({} seconds)'.format(amount))
535
536 time.sleep(amount)
537
4942f298
MW
538def checkAddressSanitizerError(output, router, component):
539 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
540
541 addressSantizerError = re.search('(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ', output)
542 if addressSantizerError:
543 sys.stderr.write("%s: %s triggered an exception by AddressSanitizer\n" % (router, component))
544 # Sanitizer Error found in log
545 pidMark = addressSantizerError.group(1)
546 addressSantizerLog = re.search('%s(.*)%s' % (pidMark, pidMark), output, re.DOTALL)
547 if addressSantizerLog:
548 callingTest = os.path.basename(sys._current_frames().values()[0].f_back.f_back.f_globals['__file__'])
549 callingProc = sys._getframe(2).f_code.co_name
550 with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile:
551 sys.stderr.write('\n'.join(addressSantizerLog.group(1).splitlines()) + '\n')
552 addrSanFile.write("## Error: %s\n\n" % addressSantizerError.group(2))
553 addrSanFile.write("### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" % (callingTest, callingProc, router))
554 addrSanFile.write(' '+ '\n '.join(addressSantizerLog.group(1).splitlines()) + '\n')
555 addrSanFile.write("\n---------------\n")
556 return True
6c131bd3 557 return False
4942f298 558
594b1259 559def addRouter(topo, name):
80eeefb7 560 "Adding a FRRouter (or Quagga) to Topology"
594b1259
MW
561
562 MyPrivateDirs = ['/etc/frr',
563 '/etc/quagga',
564 '/var/run/frr',
565 '/var/run/quagga',
566 '/var/log']
4b1d6d4d
DS
567 if sys.platform.startswith("linux"):
568 return topo.addNode(name, cls=LinuxRouter, privateDirs=MyPrivateDirs)
569 elif sys.platform.startswith("freebsd"):
570 return topo.addNode(name, cls=FreeBSDRouter, privateDirs=MyPrivateDirs)
594b1259 571
797e8dcf
RZ
572def set_sysctl(node, sysctl, value):
573 "Set a sysctl value and return None on success or an error string"
574 valuestr = '{}'.format(value)
575 command = "sysctl {0}={1}".format(sysctl, valuestr)
576 cmdret = node.cmd(command)
577
578 matches = re.search(r'([^ ]+) = ([^\s]+)', cmdret)
579 if matches is None:
580 return cmdret
581 if matches.group(1) != sysctl:
582 return cmdret
583 if matches.group(2) != valuestr:
584 return cmdret
585
586 return None
587
588def assert_sysctl(node, sysctl, value):
589 "Set and assert that the sysctl is set with the specified value."
590 assert set_sysctl(node, sysctl, value) is None
591
594b1259
MW
592
593class Router(Node):
594 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
595
2ab85530
RZ
596 def __init__(self, name, **params):
597 super(Router, self).__init__(name, **params)
0d5e41c6
RZ
598 self.logdir = params.get('logdir')
599
600 # If this topology is using old API and doesn't have logdir
601 # specified, then attempt to generate an unique logdir.
602 if self.logdir is None:
603 cur_test = os.environ['PYTEST_CURRENT_TEST']
604 self.logdir = ('/tmp/topotests/' +
605 cur_test[0:cur_test.find(".py")].replace('/', '.'))
606
607 # If the logdir is not created, then create it and set the
608 # appropriated permissions.
609 if not os.path.isdir(self.logdir):
610 os.system('mkdir -p ' + self.logdir + '/' + name)
611 os.system('chmod -R go+rw /tmp/topotests')
612
2ab85530 613 self.daemondir = None
447f2d5a 614 self.hasmpls = False
2ab85530
RZ
615 self.routertype = 'frr'
616 self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0,
617 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0,
4d45d6d3 618 'ldpd': 0, 'eigrpd': 0, 'nhrpd': 0, 'staticd': 0,
849d1ed9 619 'bfdd': 0, 'sharpd': 0}
8dd5077d 620 self.daemons_options = {'zebra': ''}
2a59a86b 621 self.reportCores = True
fb80b81b 622 self.version = None
2ab85530 623
edd2bdf6
RZ
624 def _config_frr(self, **params):
625 "Configure FRR binaries"
626 self.daemondir = params.get('frrdir')
627 if self.daemondir is None:
628 self.daemondir = '/usr/lib/frr'
629
630 zebra_path = os.path.join(self.daemondir, 'zebra')
631 if not os.path.isfile(zebra_path):
632 raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path))
633
634 def _config_quagga(self, **params):
635 "Configure Quagga binaries"
636 self.daemondir = params.get('quaggadir')
637 if self.daemondir is None:
638 self.daemondir = '/usr/lib/quagga'
639
640 zebra_path = os.path.join(self.daemondir, 'zebra')
641 if not os.path.isfile(zebra_path):
642 raise Exception("Quagga zebra binary doesn't exist at {}".format(zebra_path))
643
2ab85530
RZ
644 # pylint: disable=W0221
645 # Some params are only meaningful for the parent class.
594b1259
MW
646 def config(self, **params):
647 super(Router, self).config(**params)
648
2ab85530
RZ
649 # User did not specify the daemons directory, try to autodetect it.
650 self.daemondir = params.get('daemondir')
651 if self.daemondir is None:
edd2bdf6
RZ
652 self.routertype = params.get('routertype', 'frr')
653 if self.routertype == 'quagga':
654 self._config_quagga(**params)
655 else:
656 self._config_frr(**params)
594b1259 657 else:
2ab85530
RZ
658 # Test the provided path
659 zpath = os.path.join(self.daemondir, 'zebra')
660 if not os.path.isfile(zpath):
661 raise Exception('No zebra binary found in {}'.format(zpath))
662 # Allow user to specify routertype when the path was specified.
663 if params.get('routertype') is not None:
664 self.routertype = self.params.get('routertype')
665
594b1259
MW
666 self.cmd('ulimit -c unlimited')
667 # Set ownership of config files
2ab85530
RZ
668 self.cmd('chown {0}:{0}vty /etc/{0}'.format(self.routertype))
669
594b1259
MW
670 def terminate(self):
671 # Delete Running Quagga or FRR Daemons
99561211
MW
672 self.stopRouter()
673 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
674 # for d in StringIO.StringIO(rundaemons):
675 # self.cmd('kill -7 `cat %s`' % d.rstrip())
676 # self.waitOutput()
594b1259 677 # Disable forwarding
797e8dcf
RZ
678 set_sysctl(self, 'net.ipv4.ip_forward', 0)
679 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
594b1259 680 super(Router, self).terminate()
b0f0d980
LB
681 os.system('chmod -R go+rw /tmp/topotests')
682
dce382d4 683 def stopRouter(self, wait=True, assertOnError=True, minErrorVersion='5.1'):
99561211
MW
684 # Stop Running Quagga or FRR Daemons
685 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
83c26937 686 errors = ""
e600b2d9 687 if re.search(r"No such file or directory", rundaemons):
83c26937 688 return errors
99561211 689 if rundaemons is not None:
3a568b9c 690 numRunning = 0
7551168c
MW
691 for d in StringIO.StringIO(rundaemons):
692 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
693 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
065bd557
RZ
694 logger.info('{}: stopping {}'.format(
695 self.name,
696 os.path.basename(d.rstrip().rsplit(".", 1)[0])
697 ))
7551168c
MW
698 self.cmd('kill -TERM %s' % daemonpid)
699 self.waitOutput()
3a568b9c
LB
700 if pid_exists(int(daemonpid)):
701 numRunning += 1
702 if wait and numRunning > 0:
065bd557 703 sleep(2, '{}: waiting for daemons stopping'.format(self.name))
3a568b9c
LB
704 # 2nd round of kill if daemons didn't exit
705 for d in StringIO.StringIO(rundaemons):
706 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
707 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
065bd557
RZ
708 logger.info('{}: killing {}'.format(
709 self.name,
710 os.path.basename(d.rstrip().rsplit(".", 1)[0])
711 ))
3a568b9c
LB
712 self.cmd('kill -7 %s' % daemonpid)
713 self.waitOutput()
e600b2d9 714 self.cmd('rm -- {}'.format(d.rstrip()))
f76774ec 715 if wait:
83c26937 716 errors = self.checkRouterCores(reportOnce=True)
436aa319
LB
717 if self.checkRouterVersion('<', minErrorVersion):
718 #ignore errors in old versions
719 errors = ""
83c26937
LB
720 if assertOnError and len(errors) > 0:
721 assert "Errors found - details follow:" == 0, errors
722 return errors
f76774ec 723
594b1259
MW
724 def removeIPs(self):
725 for interface in self.intfNames():
726 self.cmd('ip address flush', interface)
8dd5077d
PG
727
728 def checkCapability(self, daemon, param):
729 if param is not None:
730 daemon_path = os.path.join(self.daemondir, daemon)
731 daemon_search_option = param.replace('-','')
732 output = self.cmd('{0} -h | grep {1}'.format(
733 daemon_path, daemon_search_option))
734 if daemon_search_option not in output:
735 return False
736 return True
737
738 def loadConf(self, daemon, source=None, param=None):
594b1259
MW
739 # print "Daemons before:", self.daemons
740 if daemon in self.daemons.keys():
741 self.daemons[daemon] = 1
8dd5077d
PG
742 if param is not None:
743 self.daemons_options[daemon] = param
594b1259
MW
744 if source is None:
745 self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon))
746 self.waitOutput()
747 else:
748 self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon))
749 self.waitOutput()
750 self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon))
751 self.waitOutput()
752 self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon))
753 self.waitOutput()
2c805e6c 754 if (daemon == 'zebra') and (self.daemons['staticd'] == 0):
a2a1134c
MW
755 # Add staticd with zebra - if it exists
756 staticd_path = os.path.join(self.daemondir, 'staticd')
757 if os.path.isfile(staticd_path):
758 self.daemons['staticd'] = 1
2c805e6c
MW
759 self.daemons_options['staticd'] = ''
760 # Auto-Started staticd has no config, so it will read from zebra config
594b1259 761 else:
222ea88b 762 logger.info('No daemon {} known'.format(daemon))
594b1259 763 # print "Daemons after:", self.daemons
e1dfa45e 764
9711fc7e 765 def startRouter(self, tgen=None):
594b1259 766 # Disable integrated-vtysh-config
a93477ec 767 self.cmd('echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' % self.routertype)
594b1259 768 self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype))
13e1fc49 769 # TODO remove the following lines after all tests are migrated to Topogen.
594b1259 770 # Try to find relevant old logfiles in /tmp and delete them
e1dfa45e 771 map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
594b1259 772 # Remove old core files
e1dfa45e 773 map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
594b1259
MW
774 # Remove IP addresses from OS first - we have them in zebra.conf
775 self.removeIPs()
776 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
777 # No error - but return message and skip all the tests
778 if self.daemons['ldpd'] == 1:
2ab85530
RZ
779 ldpd_path = os.path.join(self.daemondir, 'ldpd')
780 if not os.path.isfile(ldpd_path):
222ea88b 781 logger.info("LDP Test, but no ldpd compiled or installed")
594b1259 782 return "LDP Test, but no ldpd compiled or installed"
dd4eca4d 783
45619ee3 784 if version_cmp(platform.release(), '4.5') < 0:
222ea88b 785 logger.info("LDP Test need Linux Kernel 4.5 minimum")
45619ee3 786 return "LDP Test need Linux Kernel 4.5 minimum"
9711fc7e
LB
787 # Check if have mpls
788 if tgen != None:
789 self.hasmpls = tgen.hasmpls
790 if self.hasmpls != True:
791 logger.info("LDP/MPLS Tests will be skipped, platform missing module(s)")
792 else:
793 # Test for MPLS Kernel modules available
794 self.hasmpls = False
f2d6ce41 795 if not module_present('mpls-router'):
9711fc7e 796 logger.info('MPLS tests will not run (missing mpls-router kernel module)')
f2d6ce41 797 elif not module_present('mpls-iptunnel'):
9711fc7e
LB
798 logger.info('MPLS tests will not run (missing mpls-iptunnel kernel module)')
799 else:
800 self.hasmpls = True
801 if self.hasmpls != True:
802 return "LDP/MPLS Tests need mpls kernel modules"
a93eb16a 803 self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels')
44a592b2
MW
804
805 if self.daemons['eigrpd'] == 1:
806 eigrpd_path = os.path.join(self.daemondir, 'eigrpd')
807 if not os.path.isfile(eigrpd_path):
222ea88b 808 logger.info("EIGRP Test, but no eigrpd compiled or installed")
44a592b2
MW
809 return "EIGRP Test, but no eigrpd compiled or installed"
810
4d45d6d3
RZ
811 if self.daemons['bfdd'] == 1:
812 bfdd_path = os.path.join(self.daemondir, 'bfdd')
813 if not os.path.isfile(bfdd_path):
814 logger.info("BFD Test, but no bfdd compiled or installed")
815 return "BFD Test, but no bfdd compiled or installed"
816
99561211
MW
817 self.restartRouter()
818 return ""
e1dfa45e 819
99561211 820 def restartRouter(self):
e1dfa45e
LB
821 # Starts actual daemons without init (ie restart)
822 # cd to per node directory
823 self.cmd('cd {}/{}'.format(self.logdir, self.name))
b0f0d980 824 self.cmd('umask 000')
2a59a86b
LB
825 #Re-enable to allow for report per run
826 self.reportCores = True
fb80b81b
LB
827 if self.version == None:
828 self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2]
829 logger.info('{}: running version: {}'.format(self.name,self.version))
594b1259
MW
830 # Start Zebra first
831 if self.daemons['zebra'] == 1:
2ab85530 832 zebra_path = os.path.join(self.daemondir, 'zebra')
8dd5077d 833 zebra_option = self.daemons_options['zebra']
e1dfa45e 834 self.cmd('{0} {1} > zebra.out 2> zebra.err &'.format(
8dd5077d 835 zebra_path, zebra_option, self.logdir, self.name
2ab85530 836 ))
594b1259 837 self.waitOutput()
6c131bd3 838 logger.debug('{}: {} zebra started'.format(self, self.routertype))
63038f4b 839 sleep(1, '{}: waiting for zebra to start'.format(self.name))
a2a1134c
MW
840 # Start staticd next if required
841 if self.daemons['staticd'] == 1:
842 staticd_path = os.path.join(self.daemondir, 'staticd')
843 staticd_option = self.daemons_options['staticd']
844 self.cmd('{0} {1} > staticd.out 2> staticd.err &'.format(
845 staticd_path, staticd_option, self.logdir, self.name
846 ))
847 self.waitOutput()
848 logger.debug('{}: {} staticd started'.format(self, self.routertype))
a2a1134c 849 # Fix Link-Local Addresses
594b1259
MW
850 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
851 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')
852 # Now start all the other daemons
853 for daemon in self.daemons:
2ab85530 854 # Skip disabled daemons and zebra
a2a1134c 855 if self.daemons[daemon] == 0 or daemon == 'zebra' or daemon == 'staticd':
2ab85530 856 continue
2ab85530 857 daemon_path = os.path.join(self.daemondir, daemon)
86c21ac7 858 self.cmd('{0} {1} > {2}.out 2> {2}.err &'.format(
859 daemon_path, self.daemons_options.get(daemon, ''), daemon
860 ))
2ab85530 861 self.waitOutput()
6c131bd3 862 logger.debug('{}: {} {} started'.format(self, self.routertype, daemon))
99561211
MW
863 def getStdErr(self, daemon):
864 return self.getLog('err', daemon)
865 def getStdOut(self, daemon):
866 return self.getLog('out', daemon)
867 def getLog(self, log, daemon):
e1dfa45e 868 return self.cmd('cat {}/{}/{}.{}'.format(self.logdir, self.name, daemon, log))
f76774ec 869
2a59a86b
LB
870 def checkRouterCores(self, reportLeaks=True, reportOnce=False):
871 if reportOnce and not self.reportCores:
872 return
873 reportMade = False
83c26937 874 traces = ""
f76774ec
LB
875 for daemon in self.daemons:
876 if (self.daemons[daemon] == 1):
877 # Look for core file
878 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
879 self.logdir, self.name, daemon))
880 if (len(corefiles) > 0):
881 daemon_path = os.path.join(self.daemondir, daemon)
882 backtrace = subprocess.check_output([
883 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
884 ], shell=True)
885 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
886 sys.stderr.write("%s" % backtrace)
83c26937 887 traces = traces + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s" % (self.name, daemon, backtrace)
2a59a86b 888 reportMade = True
f76774ec
LB
889 elif reportLeaks:
890 log = self.getStdErr(daemon)
891 if "memstats" in log:
892 sys.stderr.write("%s: %s has memory leaks:\n" % (self.name, daemon))
83c26937 893 traces = traces + "\n%s: %s has memory leaks:\n" % (self.name, daemon)
f76774ec
LB
894 log = re.sub("core_handler: ", "", log)
895 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n ## \1", log)
896 log = re.sub("memstats: ", " ", log)
897 sys.stderr.write(log)
2a59a86b 898 reportMade = True
f76774ec
LB
899 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
900 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
901 sys.stderr.write("%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon))
83c26937 902 traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon)
2a59a86b
LB
903 reportMade = True
904 if reportMade:
905 self.reportCores = False
83c26937 906 return traces
f76774ec 907
594b1259 908 def checkRouterRunning(self):
597cabb7
MW
909 "Check if router daemons are running and collect crashinfo they don't run"
910
594b1259
MW
911 global fatal_error
912
913 daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"')
4942f298
MW
914 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
915 if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"):
916 return "%s: vtysh killed by AddressSanitizer" % (self.name)
917
594b1259
MW
918 for daemon in self.daemons:
919 if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
920 sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
d2132114
DS
921 if daemon is "staticd":
922 sys.stderr.write("You may have a copy of staticd installed but are attempting to test against\n")
923 sys.stderr.write("a version of FRR that does not have staticd, please cleanup the install dir\n")
924
594b1259 925 # Look for core file
e1dfa45e 926 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
13e1fc49 927 self.logdir, self.name, daemon))
594b1259 928 if (len(corefiles) > 0):
2ab85530
RZ
929 daemon_path = os.path.join(self.daemondir, daemon)
930 backtrace = subprocess.check_output([
931 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
932 ], shell=True)
594b1259
MW
933 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
934 sys.stderr.write("%s\n" % backtrace)
935 else:
936 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
e1dfa45e 937 if os.path.isfile('{}/{}/{}.log'.format(self.logdir, self.name, daemon)):
13e1fc49 938 log_tail = subprocess.check_output([
e1dfa45e 939 "tail -n20 {}/{}/{}.log 2> /dev/null".format(
13e1fc49
RZ
940 self.logdir, self.name, daemon)
941 ], shell=True)
594b1259
MW
942 sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon))
943 sys.stderr.write("%s\n" % log_tail)
4942f298 944
597cabb7 945 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
4942f298 946 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
84379e8e
MW
947 return "%s: Daemon %s not running - killed by AddressSanitizer" % (self.name, daemon)
948
594b1259
MW
949 return "%s: Daemon %s not running" % (self.name, daemon)
950 return ""
fb80b81b
LB
951
952 def checkRouterVersion(self, cmpop, version):
953 """
954 Compares router version using operation `cmpop` with `version`.
955 Valid `cmpop` values:
956 * `>=`: has the same version or greater
957 * '>': has greater version
958 * '=': has the same version
959 * '<': has a lesser version
960 * '<=': has the same version or lesser
961
962 Usage example: router.checkRouterVersion('>', '1.0')
963 """
6bfe4b8b
MW
964
965 # Make sure we have version information first
966 if self.version == None:
967 self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2]
968 logger.info('{}: running version: {}'.format(self.name,self.version))
969
fb80b81b
LB
970 rversion = self.version
971 if rversion is None:
972 return False
973
974 result = version_cmp(rversion, version)
975 if cmpop == '>=':
976 return result >= 0
977 if cmpop == '>':
978 return result > 0
979 if cmpop == '=':
980 return result == 0
981 if cmpop == '<':
982 return result < 0
983 if cmpop == '<':
984 return result < 0
985 if cmpop == '<=':
986 return result <= 0
987
594b1259
MW
988 def get_ipv6_linklocal(self):
989 "Get LinkLocal Addresses from interfaces"
990
991 linklocal = []
992
993 ifaces = self.cmd('ip -6 address')
994 # Fix newlines (make them all the same)
995 ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines()
996 interface=""
997 ll_per_if_count=0
998 for line in ifaces:
999 m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line)
1000 if m:
1001 interface = m.group(1)
1002 ll_per_if_count = 0
1003 m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line)
1004 if m:
1005 local = m.group(1)
1006 ll_per_if_count += 1
1007 if (ll_per_if_count > 1):
1008 linklocal += [["%s-%s" % (interface, ll_per_if_count), local]]
1009 else:
1010 linklocal += [[interface, local]]
1011 return linklocal
80eeefb7
MW
1012 def daemon_available(self, daemon):
1013 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
1014
2ab85530
RZ
1015 daemon_path = os.path.join(self.daemondir, daemon)
1016 if not os.path.isfile(daemon_path):
80eeefb7
MW
1017 return False
1018 if (daemon == 'ldpd'):
b431b554
MW
1019 if version_cmp(platform.release(), '4.5') < 0:
1020 return False
f2d6ce41 1021 if not module_present('mpls-router', load=False):
80eeefb7 1022 return False
f2d6ce41 1023 if not module_present('mpls-iptunnel', load=False):
b431b554 1024 return False
80eeefb7 1025 return True
f2d6ce41 1026
80eeefb7
MW
1027 def get_routertype(self):
1028 "Return the type of Router (frr or quagga)"
1029
1030 return self.routertype
50c40bde
MW
1031 def report_memory_leaks(self, filename_prefix, testscript):
1032 "Report Memory Leaks to file prefixed with given string"
1033
1034 leakfound = False
1035 filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt"
1036 for daemon in self.daemons:
1037 if (self.daemons[daemon] == 1):
1038 log = self.getStdErr(daemon)
1039 if "memstats" in log:
1040 # Found memory leak
6c131bd3
RZ
1041 logger.info('\nRouter {} {} StdErr Log:\n{}'.format(
1042 self.name, daemon, log))
50c40bde
MW
1043 if not leakfound:
1044 leakfound = True
1045 # Check if file already exists
1046 fileexists = os.path.isfile(filename)
1047 leakfile = open(filename, "a")
1048 if not fileexists:
1049 # New file - add header
1050 leakfile.write("# Memory Leak Detection for topotest %s\n\n" % testscript)
1051 leakfile.write("## Router %s\n" % self.name)
1052 leakfile.write("### Process %s\n" % daemon)
1053 log = re.sub("core_handler: ", "", log)
1054 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n#### \1\n", log)
1055 log = re.sub("memstats: ", " ", log)
1056 leakfile.write(log)
1057 leakfile.write("\n")
1058 if leakfound:
1059 leakfile.close()
80eeefb7 1060
7cc96035 1061class LinuxRouter(Router):
4b1d6d4d 1062 "A Linux Router Node with IPv4/IPv6 forwarding enabled."
7cc96035
DS
1063
1064 def __init__(self, name, **params):
1065 Router.__init__(self, name, **params)
1066
1067 def config(self, **params):
1068 Router.config(self, **params)
1069 # Enable forwarding on the router
1070 assert_sysctl(self, 'net.ipv4.ip_forward', 1)
1071 assert_sysctl(self, 'net.ipv6.conf.all.forwarding', 1)
d29fb5bd
DS
1072 # Enable coredumps
1073 assert_sysctl(self, 'kernel.core_uses_pid', 1)
1074 assert_sysctl(self, 'fs.suid_dumpable', 1)
1075 #this applies to the kernel not the namespace...
1076 #original on ubuntu 17.x, but apport won't save as in namespace
1077 # |/usr/share/apport/apport %p %s %c %d %P
1078 corefile = '%e_core-sig_%s-pid_%p.dmp'
1079 assert_sysctl(self, 'kernel.core_pattern', corefile)
1080
7cc96035
DS
1081 def terminate(self):
1082 """
1083 Terminate generic LinuxRouter Mininet instance
1084 """
1085 set_sysctl(self, 'net.ipv4.ip_forward', 0)
1086 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
1087 Router.terminate(self)
594b1259 1088
4b1d6d4d
DS
1089class FreeBSDRouter(Router):
1090 "A FreeBSD Router Node with IPv4/IPv6 forwarding enabled."
1091
1092 def __init__(eslf, name, **params):
1093 Router.__init__(Self, name, **params)
1094
1095
594b1259
MW
1096class LegacySwitch(OVSSwitch):
1097 "A Legacy Switch without OpenFlow"
1098
1099 def __init__(self, name, **params):
1100 OVSSwitch.__init__(self, name, failMode='standalone', **params)
1101 self.switchIP = None