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