]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topotest.py
isis-topo1: make isis topology match .dot file
[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,
a2a1134c 489 'ldpd': 0, 'eigrpd': 0, 'nhrpd': 0, 'staticd': 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
dce382d4 564 def stopRouter(self, wait=True, assertOnError=True, minErrorVersion='5.1'):
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 597 errors = self.checkRouterCores(reportOnce=True)
436aa319
LB
598 if self.checkRouterVersion('<', minErrorVersion):
599 #ignore errors in old versions
600 errors = ""
83c26937
LB
601 if assertOnError and len(errors) > 0:
602 assert "Errors found - details follow:" == 0, errors
603 return errors
f76774ec 604
594b1259
MW
605 def removeIPs(self):
606 for interface in self.intfNames():
607 self.cmd('ip address flush', interface)
8dd5077d
PG
608
609 def checkCapability(self, daemon, param):
610 if param is not None:
611 daemon_path = os.path.join(self.daemondir, daemon)
612 daemon_search_option = param.replace('-','')
613 output = self.cmd('{0} -h | grep {1}'.format(
614 daemon_path, daemon_search_option))
615 if daemon_search_option not in output:
616 return False
617 return True
618
619 def loadConf(self, daemon, source=None, param=None):
594b1259
MW
620 # print "Daemons before:", self.daemons
621 if daemon in self.daemons.keys():
622 self.daemons[daemon] = 1
8dd5077d
PG
623 if param is not None:
624 self.daemons_options[daemon] = param
594b1259
MW
625 if source is None:
626 self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon))
627 self.waitOutput()
628 else:
629 self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon))
630 self.waitOutput()
631 self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon))
632 self.waitOutput()
633 self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon))
634 self.waitOutput()
2c805e6c 635 if (daemon == 'zebra') and (self.daemons['staticd'] == 0):
a2a1134c
MW
636 # Add staticd with zebra - if it exists
637 staticd_path = os.path.join(self.daemondir, 'staticd')
638 if os.path.isfile(staticd_path):
639 self.daemons['staticd'] = 1
2c805e6c
MW
640 self.daemons_options['staticd'] = ''
641 # Auto-Started staticd has no config, so it will read from zebra config
594b1259 642 else:
222ea88b 643 logger.info('No daemon {} known'.format(daemon))
594b1259 644 # print "Daemons after:", self.daemons
e1dfa45e 645
9711fc7e 646 def startRouter(self, tgen=None):
594b1259 647 # Disable integrated-vtysh-config
a93477ec 648 self.cmd('echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' % self.routertype)
594b1259 649 self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype))
13e1fc49 650 # TODO remove the following lines after all tests are migrated to Topogen.
594b1259 651 # Try to find relevant old logfiles in /tmp and delete them
e1dfa45e 652 map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
594b1259 653 # Remove old core files
e1dfa45e 654 map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
594b1259
MW
655 # Remove IP addresses from OS first - we have them in zebra.conf
656 self.removeIPs()
657 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
658 # No error - but return message and skip all the tests
659 if self.daemons['ldpd'] == 1:
2ab85530
RZ
660 ldpd_path = os.path.join(self.daemondir, 'ldpd')
661 if not os.path.isfile(ldpd_path):
222ea88b 662 logger.info("LDP Test, but no ldpd compiled or installed")
594b1259 663 return "LDP Test, but no ldpd compiled or installed"
dd4eca4d 664
45619ee3 665 if version_cmp(platform.release(), '4.5') < 0:
222ea88b 666 logger.info("LDP Test need Linux Kernel 4.5 minimum")
45619ee3 667 return "LDP Test need Linux Kernel 4.5 minimum"
9711fc7e
LB
668 # Check if have mpls
669 if tgen != None:
670 self.hasmpls = tgen.hasmpls
671 if self.hasmpls != True:
672 logger.info("LDP/MPLS Tests will be skipped, platform missing module(s)")
673 else:
674 # Test for MPLS Kernel modules available
675 self.hasmpls = False
676 if os.system('/sbin/modprobe mpls-router') != 0:
677 logger.info('MPLS tests will not run (missing mpls-router kernel module)')
678 elif os.system('/sbin/modprobe mpls-iptunnel') != 0:
679 logger.info('MPLS tests will not run (missing mpls-iptunnel kernel module)')
680 else:
681 self.hasmpls = True
682 if self.hasmpls != True:
683 return "LDP/MPLS Tests need mpls kernel modules"
594b1259 684 self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels')
44a592b2
MW
685
686 if self.daemons['eigrpd'] == 1:
687 eigrpd_path = os.path.join(self.daemondir, 'eigrpd')
688 if not os.path.isfile(eigrpd_path):
222ea88b 689 logger.info("EIGRP Test, but no eigrpd compiled or installed")
44a592b2
MW
690 return "EIGRP Test, but no eigrpd compiled or installed"
691
99561211
MW
692 self.restartRouter()
693 return ""
e1dfa45e 694
99561211 695 def restartRouter(self):
e1dfa45e
LB
696 # Starts actual daemons without init (ie restart)
697 # cd to per node directory
698 self.cmd('cd {}/{}'.format(self.logdir, self.name))
b0f0d980 699 self.cmd('umask 000')
2a59a86b
LB
700 #Re-enable to allow for report per run
701 self.reportCores = True
fb80b81b
LB
702 if self.version == None:
703 self.version = self.cmd(os.path.join(self.daemondir, 'bgpd')+' -v').split()[2]
704 logger.info('{}: running version: {}'.format(self.name,self.version))
594b1259
MW
705 # Start Zebra first
706 if self.daemons['zebra'] == 1:
2ab85530 707 zebra_path = os.path.join(self.daemondir, 'zebra')
8dd5077d 708 zebra_option = self.daemons_options['zebra']
e1dfa45e 709 self.cmd('{0} {1} > zebra.out 2> zebra.err &'.format(
8dd5077d 710 zebra_path, zebra_option, self.logdir, self.name
2ab85530 711 ))
594b1259 712 self.waitOutput()
6c131bd3 713 logger.debug('{}: {} zebra started'.format(self, self.routertype))
63038f4b 714 sleep(1, '{}: waiting for zebra to start'.format(self.name))
a2a1134c
MW
715 # Start staticd next if required
716 if self.daemons['staticd'] == 1:
717 staticd_path = os.path.join(self.daemondir, 'staticd')
718 staticd_option = self.daemons_options['staticd']
719 self.cmd('{0} {1} > staticd.out 2> staticd.err &'.format(
720 staticd_path, staticd_option, self.logdir, self.name
721 ))
722 self.waitOutput()
723 logger.debug('{}: {} staticd started'.format(self, self.routertype))
724 sleep(1, '{}: waiting for staticd to start'.format(self.name))
725 # Fix Link-Local Addresses
594b1259
MW
726 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
727 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')
728 # Now start all the other daemons
729 for daemon in self.daemons:
2ab85530 730 # Skip disabled daemons and zebra
a2a1134c 731 if self.daemons[daemon] == 0 or daemon == 'zebra' or daemon == 'staticd':
2ab85530 732 continue
2ab85530 733 daemon_path = os.path.join(self.daemondir, daemon)
e1dfa45e 734 self.cmd('{0} > {3}.out 2> {3}.err &'.format(
13e1fc49 735 daemon_path, self.logdir, self.name, daemon
2ab85530
RZ
736 ))
737 self.waitOutput()
6c131bd3 738 logger.debug('{}: {} {} started'.format(self, self.routertype, daemon))
99561211
MW
739 def getStdErr(self, daemon):
740 return self.getLog('err', daemon)
741 def getStdOut(self, daemon):
742 return self.getLog('out', daemon)
743 def getLog(self, log, daemon):
e1dfa45e 744 return self.cmd('cat {}/{}/{}.{}'.format(self.logdir, self.name, daemon, log))
f76774ec 745
2a59a86b
LB
746 def checkRouterCores(self, reportLeaks=True, reportOnce=False):
747 if reportOnce and not self.reportCores:
748 return
749 reportMade = False
83c26937 750 traces = ""
f76774ec
LB
751 for daemon in self.daemons:
752 if (self.daemons[daemon] == 1):
753 # Look for core file
754 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
755 self.logdir, self.name, daemon))
756 if (len(corefiles) > 0):
757 daemon_path = os.path.join(self.daemondir, daemon)
758 backtrace = subprocess.check_output([
759 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
760 ], shell=True)
761 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
762 sys.stderr.write("%s" % backtrace)
83c26937 763 traces = traces + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s" % (self.name, daemon, backtrace)
2a59a86b 764 reportMade = True
f76774ec
LB
765 elif reportLeaks:
766 log = self.getStdErr(daemon)
767 if "memstats" in log:
768 sys.stderr.write("%s: %s has memory leaks:\n" % (self.name, daemon))
83c26937 769 traces = traces + "\n%s: %s has memory leaks:\n" % (self.name, daemon)
f76774ec
LB
770 log = re.sub("core_handler: ", "", log)
771 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n ## \1", log)
772 log = re.sub("memstats: ", " ", log)
773 sys.stderr.write(log)
2a59a86b 774 reportMade = True
f76774ec
LB
775 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
776 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
777 sys.stderr.write("%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon))
83c26937 778 traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon)
2a59a86b
LB
779 reportMade = True
780 if reportMade:
781 self.reportCores = False
83c26937 782 return traces
f76774ec 783
594b1259 784 def checkRouterRunning(self):
597cabb7
MW
785 "Check if router daemons are running and collect crashinfo they don't run"
786
594b1259
MW
787 global fatal_error
788
789 daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"')
4942f298
MW
790 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
791 if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"):
792 return "%s: vtysh killed by AddressSanitizer" % (self.name)
793
594b1259
MW
794 for daemon in self.daemons:
795 if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
796 sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
797 # Look for core file
e1dfa45e 798 corefiles = glob.glob('{}/{}/{}_core*.dmp'.format(
13e1fc49 799 self.logdir, self.name, daemon))
594b1259 800 if (len(corefiles) > 0):
2ab85530
RZ
801 daemon_path = os.path.join(self.daemondir, daemon)
802 backtrace = subprocess.check_output([
803 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
804 ], shell=True)
594b1259
MW
805 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
806 sys.stderr.write("%s\n" % backtrace)
807 else:
808 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
e1dfa45e 809 if os.path.isfile('{}/{}/{}.log'.format(self.logdir, self.name, daemon)):
13e1fc49 810 log_tail = subprocess.check_output([
e1dfa45e 811 "tail -n20 {}/{}/{}.log 2> /dev/null".format(
13e1fc49
RZ
812 self.logdir, self.name, daemon)
813 ], shell=True)
594b1259
MW
814 sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon))
815 sys.stderr.write("%s\n" % log_tail)
4942f298 816
597cabb7 817 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
4942f298 818 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
84379e8e
MW
819 return "%s: Daemon %s not running - killed by AddressSanitizer" % (self.name, daemon)
820
594b1259
MW
821 return "%s: Daemon %s not running" % (self.name, daemon)
822 return ""
fb80b81b
LB
823
824 def checkRouterVersion(self, cmpop, version):
825 """
826 Compares router version using operation `cmpop` with `version`.
827 Valid `cmpop` values:
828 * `>=`: has the same version or greater
829 * '>': has greater version
830 * '=': has the same version
831 * '<': has a lesser version
832 * '<=': has the same version or lesser
833
834 Usage example: router.checkRouterVersion('>', '1.0')
835 """
836 rversion = self.version
837 if rversion is None:
838 return False
839
840 result = version_cmp(rversion, version)
841 if cmpop == '>=':
842 return result >= 0
843 if cmpop == '>':
844 return result > 0
845 if cmpop == '=':
846 return result == 0
847 if cmpop == '<':
848 return result < 0
849 if cmpop == '<':
850 return result < 0
851 if cmpop == '<=':
852 return result <= 0
853
594b1259
MW
854 def get_ipv6_linklocal(self):
855 "Get LinkLocal Addresses from interfaces"
856
857 linklocal = []
858
859 ifaces = self.cmd('ip -6 address')
860 # Fix newlines (make them all the same)
861 ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines()
862 interface=""
863 ll_per_if_count=0
864 for line in ifaces:
865 m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line)
866 if m:
867 interface = m.group(1)
868 ll_per_if_count = 0
869 m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line)
870 if m:
871 local = m.group(1)
872 ll_per_if_count += 1
873 if (ll_per_if_count > 1):
874 linklocal += [["%s-%s" % (interface, ll_per_if_count), local]]
875 else:
876 linklocal += [[interface, local]]
877 return linklocal
80eeefb7
MW
878 def daemon_available(self, daemon):
879 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
880
2ab85530
RZ
881 daemon_path = os.path.join(self.daemondir, daemon)
882 if not os.path.isfile(daemon_path):
80eeefb7
MW
883 return False
884 if (daemon == 'ldpd'):
b431b554
MW
885 if version_cmp(platform.release(), '4.5') < 0:
886 return False
887 if self.cmd('/sbin/modprobe -n mpls-router' ) != "":
80eeefb7 888 return False
b431b554
MW
889 if self.cmd('/sbin/modprobe -n mpls-iptunnel') != "":
890 return False
891
80eeefb7
MW
892 return True
893 def get_routertype(self):
894 "Return the type of Router (frr or quagga)"
895
896 return self.routertype
50c40bde
MW
897 def report_memory_leaks(self, filename_prefix, testscript):
898 "Report Memory Leaks to file prefixed with given string"
899
900 leakfound = False
901 filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt"
902 for daemon in self.daemons:
903 if (self.daemons[daemon] == 1):
904 log = self.getStdErr(daemon)
905 if "memstats" in log:
906 # Found memory leak
6c131bd3
RZ
907 logger.info('\nRouter {} {} StdErr Log:\n{}'.format(
908 self.name, daemon, log))
50c40bde
MW
909 if not leakfound:
910 leakfound = True
911 # Check if file already exists
912 fileexists = os.path.isfile(filename)
913 leakfile = open(filename, "a")
914 if not fileexists:
915 # New file - add header
916 leakfile.write("# Memory Leak Detection for topotest %s\n\n" % testscript)
917 leakfile.write("## Router %s\n" % self.name)
918 leakfile.write("### Process %s\n" % daemon)
919 log = re.sub("core_handler: ", "", log)
920 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n#### \1\n", log)
921 log = re.sub("memstats: ", " ", log)
922 leakfile.write(log)
923 leakfile.write("\n")
924 if leakfound:
925 leakfile.close()
80eeefb7 926
594b1259
MW
927
928class LegacySwitch(OVSSwitch):
929 "A Legacy Switch without OpenFlow"
930
931 def __init__(self, name, **params):
932 OVSSwitch.__init__(self, name, failMode='standalone', **params)
933 self.switchIP = None