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