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