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