]>
git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topotest.py
5 # Library of helper functions for NetDEF Topology Tests
7 # Copyright (c) 2016 by
8 # Network Device Education Foundation, Inc. ("NetDEF")
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
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
36 from lib
. topolog
import logger
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
45 from time
import sleep
47 class json_cmp_result ( object ):
48 "json_cmp result class for better assertion messages"
53 def add_error ( self
, error
):
54 "Append error message to the result"
55 self
. errors
. append ( error
)
58 "Returns True if there were errors, otherwise False."
59 return len ( self
. errors
) > 0
62 def json_cmp ( d1
, d2
, reason
= False ):
64 JSON compare function. Receives two parameters:
66 * `d2`: json subset which we expect
68 Returns `None` when all keys that `d1` has matches `d2`,
69 otherwise a string containing what failed.
71 Note: key absence can be tested by adding a key with value `None`.
73 squeue
= [( d1
, d2
, 'json' )]
74 result
= json_cmp_result ()
77 s1
, s2
= set ( nd1
), set ( nd2
)
79 # Expect all required fields to exist.
80 s2_req
= set ([ key
for key
in nd2
if nd2
[ key
] is not None ])
83 result
. add_error ( 'expected key(s) {} in {} (have {})' . format (
84 str ( list ( diff
)), parent
, str ( list ( s1
))))
86 for key
in s2
. intersection ( s1
):
87 # Test for non existence of key in d2
89 result
. add_error ( '"{}" should not exist in {} (have {})' . format (
90 key
, parent
, str ( s1
)))
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 ({})):
96 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
97 '(have {}, expected {})' . format ( type ( nd1
[ key
]), type ( nd2
[ key
])))
99 nparent
= '{}["{}"]' . format ( parent
, key
)
100 squeue
. append (( nd1
[ key
], nd2
[ key
], nparent
))
103 if isinstance ( nd2
[ key
], type ([])):
104 if not isinstance ( nd1
[ key
], type ([])):
106 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
107 '(have {}, expected {})' . format ( type ( nd1
[ key
]), type ( nd2
[ key
])))
110 if len ( nd2
[ key
]) > len ( nd1
[ key
]):
112 '{}["{}"] too few items ' . format ( parent
, key
) +
113 '(have ({}) "{}", expected ({}) "{}")' . format (
114 len ( nd1
[ key
]), str ( nd1
[ key
]), len ( nd2
[ key
]), str ( nd2
[ key
])))
117 # List all unmatched items errors
119 for expected
in nd2
[ key
]:
121 for value
in nd1
[ key
]:
122 if json_cmp ({ 'json' : value
}, { 'json' : expected
}) is None :
129 unmatched
. append ( expected
)
131 # If there are unmatched items, error out.
134 '{}["{}"] value is different (have "{}", expected "{}")' . format (
135 parent
, key
, str ( nd1
[ key
]), str ( nd2
[ key
])))
138 # Compare JSON values
139 if nd1
[ key
] != nd2
[ key
]:
141 '{}["{}"] value is different (have "{}", expected "{}")' . format (
142 parent
, key
, str ( nd1
[ key
]), str ( nd2
[ key
])))
145 if result
. has_errors ():
150 def run_and_expect ( func
, what
, count
= 20 , wait
= 3 ):
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.
156 Returns (True, func-return) on success or
157 (False, func-return) on failure.
165 return ( True , result
)
166 return ( False , result
)
170 "Converting Integer to DPID"
174 dpid
= '0' *( 16 - len ( dpid
))+ dpid
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.' )
182 "Check whether pid exists in the current process table."
188 except OSError as err
:
189 if err
. errno
== errno
. ESRCH
:
190 # ESRCH == No such process
192 elif err
. errno
== errno
. EPERM
:
193 # EPERM clearly means there's a process to deny access to
196 # According to "man 2 kill" possible error values are
197 # (EINVAL, EPERM, ESRCH)
202 def get_textdiff ( text1
, text2
, title1
= "" , title2
= "" ):
203 "Returns empty string if same or formatted diff"
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
])
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
)
217 def get_file ( content
):
219 Generates a temporary file in '/tmp' with `content` and returns the file name.
221 fde
= tempfile
. NamedTemporaryFile ( mode
= 'w' , delete
= False )
227 def normalize_text ( text
):
229 Strips formating spaces/tabs and carriage returns.
231 text
= re
. sub ( r
'[ \t]+' , ' ' , text
)
232 text
= re
. sub ( r
'\r' , '' , text
)
235 def version_cmp ( v1
, v2
):
237 Compare two version strings and returns:
239 * `-1`: if `v1` is less than `v2`
240 * `0`: if `v1` is equal to `v2`
241 * `1`: if `v1` is greater than `v2`
243 Raises `ValueError` if versions are not well formated.
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" )
252 v1g
= v1m
. group ( 'whole' ). split ( '.' )
253 v2g
= v2m
. group ( 'whole' ). split ( '.' )
255 # Get the longest version string
260 # Reverse list because we are going to pop the tail
263 for _
in range ( vnum
):
290 def checkAddressSanitizerError ( output
, router
, component
):
291 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
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 " )
311 def addRouter ( topo
, name
):
312 "Adding a FRRouter (or Quagga) to Topology"
314 MyPrivateDirs
= [ '/etc/frr' ,
319 return topo
. addNode ( name
, cls
= Router
, privateDirs
= MyPrivateDirs
)
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
)
327 matches
= re
. search ( r
'([^ ]+) = ([^\s]+)' , cmdret
)
330 if matches
. group ( 1 ) != sysctl
:
332 if matches
. group ( 2 ) != valuestr
:
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
341 class LinuxRouter ( Node
):
342 "A Node with IPv4/IPv6 forwarding enabled."
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 )
351 Terminate generic LinuxRouter Mininet instance
353 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
354 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
355 super ( LinuxRouter
, self
). terminate ()
358 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
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 ,
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'
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
))
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'
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
))
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
)
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
)
401 self
._ config
_ frr
(** params
)
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' )
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 )
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
))
424 # Delete Running Quagga or FRR Daemons
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())
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
)
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
451 self
. cmd ( 'touch /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
454 self
. cmd ( 'cp %s /etc/ %s / %s .conf' % ( source
, self
. routertype
, daemon
))
456 self
. cmd ( 'chmod 640 /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
458 self
. cmd ( 'chown %s : %s /etc/ %s / %s .conf' % ( self
. routertype
, self
. routertype
, self
. routertype
, daemon
))
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 : %s vty /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
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 ())
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"
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
495 def restartRouter ( self
):
496 # Starts actuall daemons without init (ie restart)
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
504 logger
. debug ( '{}: {} zebra started' . format ( self
, self
. routertype
))
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 %0 2x $((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' :
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
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"
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
)
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
))
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 ])
548 sys
. stderr
. write ( " \n %s : %s crashed. Core file found - Backtrace follows: \n " % ( self
. name
, daemon
))
549 sys
. stderr
. write ( " %s \n " % backtrace
)
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
)
557 sys
. stderr
. write ( " \n From %s %s %s log file: \n " % ( self
. routertype
, self
. name
, daemon
))
558 sys
. stderr
. write ( " %s \n " % log_tail
)
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
)
564 return " %s : Daemon %s not running" % ( self
. name
, daemon
)
566 def get_ipv6_linklocal ( self
):
567 "Get LinkLocal Addresses from interfaces"
571 ifaces
= self
. cmd ( 'ip -6 address' )
572 # Fix newlines (make them all the same)
573 ifaces
= ( ' \n ' . join ( ifaces
. splitlines ()) + ' \n ' ). splitlines ()
577 m
= re
. search ( '[0-9]+: ([^:@]+)[@if0-9:]+ <' , line
)
579 interface
= m
. group ( 1 )
581 m
= re
. search ( 'inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link' , line
)
585 if ( ll_per_if_count
> 1 ):
586 linklocal
+= [[ " %s-%s " % ( interface
, ll_per_if_count
), local
]]
588 linklocal
+= [[ interface
, local
]]
590 def daemon_available ( self
, daemon
):
591 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
593 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
594 if not os
. path
. isfile ( daemon_path
):
596 if ( daemon
== 'ldpd' ):
597 kernel_version
= re
. search ( r
'([0-9]+)\.([0-9]+).*' , platform
. release ())
599 if ( float ( kernel_version
. group ( 1 )) < 4 or
600 ( float ( kernel_version
. group ( 1 )) == 4 and float ( kernel_version
. group ( 2 )) < 5 )):
605 def get_routertype ( self
):
606 "Return the type of Router (frr or quagga)"
608 return self
. routertype
609 def report_memory_leaks ( self
, filename_prefix
, testscript
):
610 "Report Memory Leaks to file prefixed with given string"
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
:
619 logger
. info ( ' \n Router {} {} StdErr Log: \n {}' . format (
620 self
. name
, daemon
, log
))
623 # Check if file already exists
624 fileexists
= os
. path
. isfile ( filename
)
625 leakfile
= open ( filename
, "a" )
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
)
640 class LegacySwitch ( OVSSwitch
):
641 "A Legacy Switch without OpenFlow"
643 def __init__ ( self
, name
, ** params
):
644 OVSSwitch
.__ init
__ ( self
, name
, failMode
= 'standalone' , ** params
)