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