]> git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topotest.py
topotest: add version comparison function
[mirror_frr.git] / tests / topotests / lib / topotest.py
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
25 import os
26 import errno
27 import re
28 import sys
29 import glob
30 import StringIO
31 import subprocess
32 import tempfile
33 import platform
34 import difflib
35
36 from lib.topolog import logger
37
38 from mininet.topo import Topo
39 from mininet.net import Mininet
40 from mininet.node import Node, OVSSwitch, Host
41 from mininet.log import setLogLevel, info
42 from mininet.cli import CLI
43 from mininet.link import Intf
44
45 from time import sleep
46
47 class 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"
55 self.errors.append(error)
56
57 def has_errors(self):
58 "Returns True if there were errors, otherwise False."
59 return len(self.errors) > 0
60
61
62 def json_cmp(d1, d2, reason=False):
63 """
64 JSON compare function. Receives two parameters:
65 * `d1`: json value
66 * `d2`: json subset which we expect
67
68 Returns `None` when all keys that `d1` has matches `d2`,
69 otherwise a string containing what failed.
70
71 Note: key absence can be tested by adding a key with value `None`.
72 """
73 squeue = [(d1, d2, 'json')]
74 result = json_cmp_result()
75 for s in squeue:
76 nd1, nd2, parent = s
77 s1, s2 = set(nd1), set(nd2)
78
79 # Expect all required fields to exist.
80 s2_req = set([key for key in nd2 if nd2[key] is not None])
81 diff = s2_req - s1
82 if diff != set({}):
83 result.add_error('expected key(s) {} in {} (have {})'.format(
84 str(list(diff)), parent, str(list(s1))))
85
86 for key in s2.intersection(s1):
87 # Test for non existence of key in d2
88 if nd2[key] is None:
89 result.add_error('"{}" should not exist in {} (have {})'.format(
90 key, parent, str(s1)))
91 continue
92 # If nd1 key is a dict, we have to recurse in it later.
93 if isinstance(nd2[key], type({})):
94 if not isinstance(nd1[key], type({})):
95 result.add_error(
96 '{}["{}"] has different type than expected '.format(parent, key) +
97 '(have {}, expected {})'.format(type(nd1[key]), type(nd2[key])))
98 continue
99 nparent = '{}["{}"]'.format(parent, key)
100 squeue.append((nd1[key], nd2[key], nparent))
101 continue
102 # Check list items
103 if isinstance(nd2[key], type([])):
104 if not isinstance(nd1[key], type([])):
105 result.add_error(
106 '{}["{}"] has different type than expected '.format(parent, key) +
107 '(have {}, expected {})'.format(type(nd1[key]), type(nd2[key])))
108 continue
109 # Check list size
110 if len(nd2[key]) > len(nd1[key]):
111 result.add_error(
112 '{}["{}"] too few items '.format(parent, key) +
113 '(have ({}) "{}", expected ({}) "{}")'.format(
114 len(nd1[key]), str(nd1[key]), len(nd2[key]), str(nd2[key])))
115 continue
116
117 # List all unmatched items errors
118 unmatched = []
119 for expected in nd2[key]:
120 matched = False
121 for value in nd1[key]:
122 if json_cmp({'json': value}, {'json': expected}) is None:
123 matched = True
124 break
125
126 if matched:
127 break
128 if not matched:
129 unmatched.append(expected)
130
131 # If there are unmatched items, error out.
132 if unmatched:
133 result.add_error(
134 '{}["{}"] value is different (have "{}", expected "{}")'.format(
135 parent, key, str(nd1[key]), str(nd2[key])))
136 continue
137
138 # Compare JSON values
139 if nd1[key] != nd2[key]:
140 result.add_error(
141 '{}["{}"] value is different (have "{}", expected "{}")'.format(
142 parent, key, str(nd1[key]), str(nd2[key])))
143 continue
144
145 if result.has_errors():
146 return result
147
148 return None
149
150 def run_and_expect(func, what, count=20, wait=3):
151 """
152 Run `func` and compare the result with `what`. Do it for `count` times
153 waiting `wait` seconds between tries. By default it tries 20 times with
154 3 seconds delay between tries.
155
156 Returns (True, func-return) on success or
157 (False, func-return) on failure.
158 """
159 while count > 0:
160 result = func()
161 if result != what:
162 sleep(wait)
163 count -= 1
164 continue
165 return (True, result)
166 return (False, result)
167
168
169 def int2dpid(dpid):
170 "Converting Integer to DPID"
171
172 try:
173 dpid = hex(dpid)[2:]
174 dpid = '0'*(16-len(dpid))+dpid
175 return dpid
176 except IndexError:
177 raise Exception('Unable to derive default datapath ID - '
178 'please either specify a dpid or use a '
179 'canonical switch name such as s23.')
180
181 def pid_exists(pid):
182 "Check whether pid exists in the current process table."
183
184 if pid <= 0:
185 return False
186 try:
187 os.kill(pid, 0)
188 except OSError as err:
189 if err.errno == errno.ESRCH:
190 # ESRCH == No such process
191 return False
192 elif err.errno == errno.EPERM:
193 # EPERM clearly means there's a process to deny access to
194 return True
195 else:
196 # According to "man 2 kill" possible error values are
197 # (EINVAL, EPERM, ESRCH)
198 raise
199 else:
200 return True
201
202 def get_textdiff(text1, text2, title1="", title2=""):
203 "Returns empty string if same or formatted diff"
204
205 diff = '\n'.join(difflib.context_diff(text1, text2,
206 fromfile=title1, tofile=title2))
207 # Clean up line endings
208 diff = os.linesep.join([s for s in diff.splitlines() if s])
209 return diff
210
211 def difflines(text1, text2, title1='', title2=''):
212 "Wrapper for get_textdiff to avoid string transformations."
213 text1 = ('\n'.join(text1.rstrip().splitlines()) + '\n').splitlines(1)
214 text2 = ('\n'.join(text2.rstrip().splitlines()) + '\n').splitlines(1)
215 return get_textdiff(text1, text2, title1, title2)
216
217 def get_file(content):
218 """
219 Generates a temporary file in '/tmp' with `content` and returns the file name.
220 """
221 fde = tempfile.NamedTemporaryFile(mode='w', delete=False)
222 fname = fde.name
223 fde.write(content)
224 fde.close()
225 return fname
226
227 def normalize_text(text):
228 """
229 Strips formating spaces/tabs and carriage returns.
230 """
231 text = re.sub(r'[ \t]+', ' ', text)
232 text = re.sub(r'\r', '', text)
233 return text
234
235 def version_cmp(v1, v2):
236 """
237 Compare two version strings and returns:
238
239 * `-1`: if `v1` is less than `v2`
240 * `0`: if `v1` is equal to `v2`
241 * `1`: if `v1` is greater than `v2`
242
243 Raises `ValueError` if versions are not well formated.
244 """
245 vregex = r'(?P<whole>\d+(\.(\d+))*)'
246 v1m = re.match(vregex, v1)
247 v2m = re.match(vregex, v2)
248 if v1m is None or v2m is None:
249 raise ValueError("got a invalid version string")
250
251 # Split values
252 v1g = v1m.group('whole').split('.')
253 v2g = v2m.group('whole').split('.')
254
255 # Get the longest version string
256 vnum = len(v1g)
257 if len(v2g) > vnum:
258 vnum = len(v2g)
259
260 # Reverse list because we are going to pop the tail
261 v1g.reverse()
262 v2g.reverse()
263 for _ in range(vnum):
264 try:
265 v1n = int(v1g.pop())
266 except IndexError:
267 while v2g:
268 v2n = int(v2g.pop())
269 if v2n > 0:
270 return -1
271 break
272
273 try:
274 v2n = int(v2g.pop())
275 except IndexError:
276 if v1n > 0:
277 return 1
278 while v1g:
279 v1n = int(v1g.pop())
280 if v1n > 0:
281 return -1
282 break
283
284 if v1n > v2n:
285 return 1
286 if v1n < v2n:
287 return -1
288 return 0
289
290 def checkAddressSanitizerError(output, router, component):
291 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
292
293 addressSantizerError = re.search('(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ', output)
294 if addressSantizerError:
295 sys.stderr.write("%s: %s triggered an exception by AddressSanitizer\n" % (router, component))
296 # Sanitizer Error found in log
297 pidMark = addressSantizerError.group(1)
298 addressSantizerLog = re.search('%s(.*)%s' % (pidMark, pidMark), output, re.DOTALL)
299 if addressSantizerLog:
300 callingTest = os.path.basename(sys._current_frames().values()[0].f_back.f_back.f_globals['__file__'])
301 callingProc = sys._getframe(2).f_code.co_name
302 with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile:
303 sys.stderr.write('\n'.join(addressSantizerLog.group(1).splitlines()) + '\n')
304 addrSanFile.write("## Error: %s\n\n" % addressSantizerError.group(2))
305 addrSanFile.write("### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" % (callingTest, callingProc, router))
306 addrSanFile.write(' '+ '\n '.join(addressSantizerLog.group(1).splitlines()) + '\n')
307 addrSanFile.write("\n---------------\n")
308 return True
309 return False
310
311 def addRouter(topo, name):
312 "Adding a FRRouter (or Quagga) to Topology"
313
314 MyPrivateDirs = ['/etc/frr',
315 '/etc/quagga',
316 '/var/run/frr',
317 '/var/run/quagga',
318 '/var/log']
319 return topo.addNode(name, cls=Router, privateDirs=MyPrivateDirs)
320
321 def set_sysctl(node, sysctl, value):
322 "Set a sysctl value and return None on success or an error string"
323 valuestr = '{}'.format(value)
324 command = "sysctl {0}={1}".format(sysctl, valuestr)
325 cmdret = node.cmd(command)
326
327 matches = re.search(r'([^ ]+) = ([^\s]+)', cmdret)
328 if matches is None:
329 return cmdret
330 if matches.group(1) != sysctl:
331 return cmdret
332 if matches.group(2) != valuestr:
333 return cmdret
334
335 return None
336
337 def assert_sysctl(node, sysctl, value):
338 "Set and assert that the sysctl is set with the specified value."
339 assert set_sysctl(node, sysctl, value) is None
340
341 class LinuxRouter(Node):
342 "A Node with IPv4/IPv6 forwarding enabled."
343
344 def config(self, **params):
345 super(LinuxRouter, self).config(**params)
346 # Enable forwarding on the router
347 assert_sysctl(self, 'net.ipv4.ip_forward', 1)
348 assert_sysctl(self, 'net.ipv6.conf.all.forwarding', 1)
349 def terminate(self):
350 """
351 Terminate generic LinuxRouter Mininet instance
352 """
353 set_sysctl(self, 'net.ipv4.ip_forward', 0)
354 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
355 super(LinuxRouter, self).terminate()
356
357 class Router(Node):
358 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
359
360 def __init__(self, name, **params):
361 super(Router, self).__init__(name, **params)
362 self.logdir = params.get('logdir', '/tmp')
363 self.daemondir = None
364 self.routertype = 'frr'
365 self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0,
366 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0,
367 'ldpd': 0}
368
369 def _config_frr(self, **params):
370 "Configure FRR binaries"
371 self.daemondir = params.get('frrdir')
372 if self.daemondir is None:
373 self.daemondir = '/usr/lib/frr'
374
375 zebra_path = os.path.join(self.daemondir, 'zebra')
376 if not os.path.isfile(zebra_path):
377 raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path))
378
379 def _config_quagga(self, **params):
380 "Configure Quagga binaries"
381 self.daemondir = params.get('quaggadir')
382 if self.daemondir is None:
383 self.daemondir = '/usr/lib/quagga'
384
385 zebra_path = os.path.join(self.daemondir, 'zebra')
386 if not os.path.isfile(zebra_path):
387 raise Exception("Quagga zebra binary doesn't exist at {}".format(zebra_path))
388
389 # pylint: disable=W0221
390 # Some params are only meaningful for the parent class.
391 def config(self, **params):
392 super(Router, self).config(**params)
393
394 # User did not specify the daemons directory, try to autodetect it.
395 self.daemondir = params.get('daemondir')
396 if self.daemondir is None:
397 self.routertype = params.get('routertype', 'frr')
398 if self.routertype == 'quagga':
399 self._config_quagga(**params)
400 else:
401 self._config_frr(**params)
402 else:
403 # Test the provided path
404 zpath = os.path.join(self.daemondir, 'zebra')
405 if not os.path.isfile(zpath):
406 raise Exception('No zebra binary found in {}'.format(zpath))
407 # Allow user to specify routertype when the path was specified.
408 if params.get('routertype') is not None:
409 self.routertype = self.params.get('routertype')
410
411 # Enable forwarding on the router
412 assert_sysctl(self, 'net.ipv4.ip_forward', 1)
413 assert_sysctl(self, 'net.ipv6.conf.all.forwarding', 1)
414 # Enable coredumps
415 assert_sysctl(self, 'kernel.core_uses_pid', 1)
416 assert_sysctl(self, 'fs.suid_dumpable', 2)
417 corefile = '{}/{}_%e_core-sig_%s-pid_%p.dmp'.format(self.logdir, self.name)
418 assert_sysctl(self, 'kernel.core_pattern', corefile)
419 self.cmd('ulimit -c unlimited')
420 # Set ownership of config files
421 self.cmd('chown {0}:{0}vty /etc/{0}'.format(self.routertype))
422
423 def terminate(self):
424 # Delete Running Quagga or FRR Daemons
425 self.stopRouter()
426 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
427 # for d in StringIO.StringIO(rundaemons):
428 # self.cmd('kill -7 `cat %s`' % d.rstrip())
429 # self.waitOutput()
430 # Disable forwarding
431 set_sysctl(self, 'net.ipv4.ip_forward', 0)
432 set_sysctl(self, 'net.ipv6.conf.all.forwarding', 0)
433 super(Router, self).terminate()
434 def stopRouter(self):
435 # Stop Running Quagga or FRR Daemons
436 rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
437 if rundaemons is not None:
438 for d in StringIO.StringIO(rundaemons):
439 daemonpid = self.cmd('cat %s' % d.rstrip()).rstrip()
440 if (daemonpid.isdigit() and pid_exists(int(daemonpid))):
441 self.cmd('kill -7 %s' % daemonpid)
442 self.waitOutput()
443 def removeIPs(self):
444 for interface in self.intfNames():
445 self.cmd('ip address flush', interface)
446 def loadConf(self, daemon, source=None):
447 # print "Daemons before:", self.daemons
448 if daemon in self.daemons.keys():
449 self.daemons[daemon] = 1
450 if source is None:
451 self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon))
452 self.waitOutput()
453 else:
454 self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon))
455 self.waitOutput()
456 self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon))
457 self.waitOutput()
458 self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon))
459 self.waitOutput()
460 else:
461 logger.warning('No daemon {} known'.format(daemon))
462 # print "Daemons after:", self.daemons
463 def startRouter(self):
464 # Disable integrated-vtysh-config
465 self.cmd('echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' % self.routertype)
466 self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype))
467 # TODO remove the following lines after all tests are migrated to Topogen.
468 # Try to find relevant old logfiles in /tmp and delete them
469 map(os.remove, glob.glob("/tmp/*%s*.log" % self.name))
470 # Remove old core files
471 map(os.remove, glob.glob("/tmp/%s*.dmp" % self.name))
472 # Remove IP addresses from OS first - we have them in zebra.conf
473 self.removeIPs()
474 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
475 # No error - but return message and skip all the tests
476 if self.daemons['ldpd'] == 1:
477 ldpd_path = os.path.join(self.daemondir, 'ldpd')
478 if not os.path.isfile(ldpd_path):
479 logger.warning("LDP Test, but no ldpd compiled or installed")
480 return "LDP Test, but no ldpd compiled or installed"
481 kernel_version = re.search(r'([0-9]+)\.([0-9]+).*', platform.release())
482
483 if kernel_version:
484 if (float(kernel_version.group(1)) < 4 or
485 (float(kernel_version.group(1)) == 4 and float(kernel_version.group(2)) < 5)):
486 logger.warning("LDP Test need Linux Kernel 4.5 minimum")
487 return "LDP Test need Linux Kernel 4.5 minimum"
488
489 self.cmd('/sbin/modprobe mpls-router')
490 self.cmd('/sbin/modprobe mpls-iptunnel')
491 self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels')
492 # Init done - now restarting daemons
493 self.restartRouter()
494 return ""
495 def restartRouter(self):
496 # Starts actuall daemons without init (ie restart)
497 # Start Zebra first
498 if self.daemons['zebra'] == 1:
499 zebra_path = os.path.join(self.daemondir, 'zebra')
500 self.cmd('{0} > {1}/{2}-zebra.out 2> {1}/{2}-zebra.err &'.format(
501 zebra_path, self.logdir, self.name
502 ))
503 self.waitOutput()
504 logger.debug('{}: {} zebra started'.format(self, self.routertype))
505 sleep(1)
506 # Fix Link-Local Addresses
507 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
508 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')
509 # Now start all the other daemons
510 for daemon in self.daemons:
511 # Skip disabled daemons and zebra
512 if self.daemons[daemon] == 0 or daemon == 'zebra':
513 continue
514
515 daemon_path = os.path.join(self.daemondir, daemon)
516 self.cmd('{0} > {1}/{2}-{3}.out 2> {1}/{2}-{3}.err &'.format(
517 daemon_path, self.logdir, self.name, daemon
518 ))
519 self.waitOutput()
520 logger.debug('{}: {} {} started'.format(self, self.routertype, daemon))
521 def getStdErr(self, daemon):
522 return self.getLog('err', daemon)
523 def getStdOut(self, daemon):
524 return self.getLog('out', daemon)
525 def getLog(self, log, daemon):
526 return self.cmd('cat {}/{}-{}.{}'.format(self.logdir, self.name, daemon, log))
527 def checkRouterRunning(self):
528 "Check if router daemons are running and collect crashinfo they don't run"
529
530 global fatal_error
531
532 daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"')
533 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
534 if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"):
535 return "%s: vtysh killed by AddressSanitizer" % (self.name)
536
537 for daemon in self.daemons:
538 if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning):
539 sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon))
540 # Look for core file
541 corefiles = glob.glob('{}/{}_{}_core*.dmp'.format(
542 self.logdir, self.name, daemon))
543 if (len(corefiles) > 0):
544 daemon_path = os.path.join(self.daemondir, daemon)
545 backtrace = subprocess.check_output([
546 "gdb {} {} --batch -ex bt 2> /dev/null".format(daemon_path, corefiles[0])
547 ], shell=True)
548 sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon))
549 sys.stderr.write("%s\n" % backtrace)
550 else:
551 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
552 if os.path.isfile('{}/{}-{}.log'.format(self.logdir, self.name, daemon)):
553 log_tail = subprocess.check_output([
554 "tail -n20 {}/{}-{}.log 2> /dev/null".format(
555 self.logdir, self.name, daemon)
556 ], shell=True)
557 sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon))
558 sys.stderr.write("%s\n" % log_tail)
559
560 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
561 if checkAddressSanitizerError(self.getStdErr(daemon), self.name, daemon):
562 return "%s: Daemon %s not running - killed by AddressSanitizer" % (self.name, daemon)
563
564 return "%s: Daemon %s not running" % (self.name, daemon)
565 return ""
566 def get_ipv6_linklocal(self):
567 "Get LinkLocal Addresses from interfaces"
568
569 linklocal = []
570
571 ifaces = self.cmd('ip -6 address')
572 # Fix newlines (make them all the same)
573 ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines()
574 interface=""
575 ll_per_if_count=0
576 for line in ifaces:
577 m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line)
578 if m:
579 interface = m.group(1)
580 ll_per_if_count = 0
581 m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line)
582 if m:
583 local = m.group(1)
584 ll_per_if_count += 1
585 if (ll_per_if_count > 1):
586 linklocal += [["%s-%s" % (interface, ll_per_if_count), local]]
587 else:
588 linklocal += [[interface, local]]
589 return linklocal
590 def daemon_available(self, daemon):
591 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
592
593 daemon_path = os.path.join(self.daemondir, daemon)
594 if not os.path.isfile(daemon_path):
595 return False
596 if (daemon == 'ldpd'):
597 kernel_version = re.search(r'([0-9]+)\.([0-9]+).*', platform.release())
598 if kernel_version:
599 if (float(kernel_version.group(1)) < 4 or
600 (float(kernel_version.group(1)) == 4 and float(kernel_version.group(2)) < 5)):
601 return False
602 else:
603 return False
604 return True
605 def get_routertype(self):
606 "Return the type of Router (frr or quagga)"
607
608 return self.routertype
609 def report_memory_leaks(self, filename_prefix, testscript):
610 "Report Memory Leaks to file prefixed with given string"
611
612 leakfound = False
613 filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt"
614 for daemon in self.daemons:
615 if (self.daemons[daemon] == 1):
616 log = self.getStdErr(daemon)
617 if "memstats" in log:
618 # Found memory leak
619 logger.info('\nRouter {} {} StdErr Log:\n{}'.format(
620 self.name, daemon, log))
621 if not leakfound:
622 leakfound = True
623 # Check if file already exists
624 fileexists = os.path.isfile(filename)
625 leakfile = open(filename, "a")
626 if not fileexists:
627 # New file - add header
628 leakfile.write("# Memory Leak Detection for topotest %s\n\n" % testscript)
629 leakfile.write("## Router %s\n" % self.name)
630 leakfile.write("### Process %s\n" % daemon)
631 log = re.sub("core_handler: ", "", log)
632 log = re.sub(r"(showing active allocations in memory group [a-zA-Z0-9]+)", r"\n#### \1\n", log)
633 log = re.sub("memstats: ", " ", log)
634 leakfile.write(log)
635 leakfile.write("\n")
636 if leakfound:
637 leakfile.close()
638
639
640 class LegacySwitch(OVSSwitch):
641 "A Legacy Switch without OpenFlow"
642
643 def __init__(self, name, **params):
644 OVSSwitch.__init__(self, name, failMode='standalone', **params)
645 self.switchIP = None