]>
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
38 from lib
. topolog
import logger
40 from mininet
. topo
import Topo
41 from mininet
. net
import Mininet
42 from mininet
. node
import Node
, OVSSwitch
, Host
43 from mininet
. log
import setLogLevel
, info
44 from mininet
. cli
import CLI
45 from mininet
. link
import Intf
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 for line
in error
. splitlines ():
56 self
. errors
. append ( line
)
59 "Returns True if there were errors, otherwise False."
60 return len ( self
. errors
) > 0
62 def json_diff ( d1
, d2
):
64 Returns a string with the difference between JSON data.
70 dstr1
= json
. dumps ( d1
, ** json_format_opts
)
71 dstr2
= json
. dumps ( d2
, ** json_format_opts
)
72 return difflines ( dstr2
, dstr1
, title1
= 'Expected value' , title2
= 'Current value' , n
= 0 )
76 JSON compare function. Receives two parameters:
78 * `d2`: json subset which we expect
80 Returns `None` when all keys that `d1` has matches `d2`,
81 otherwise a string containing what failed.
83 Note: key absence can be tested by adding a key with value `None`.
85 squeue
= [( d1
, d2
, 'json' )]
86 result
= json_cmp_result ()
89 s1
, s2
= set ( nd1
), set ( nd2
)
91 # Expect all required fields to exist.
92 s2_req
= set ([ key
for key
in nd2
if nd2
[ key
] is not None ])
95 result
. add_error ( 'expected key(s) {} in {} (have {}): \n {}' . format (
96 str ( list ( diff
)), parent
, str ( list ( s1
)), json_diff ( nd1
, nd2
)))
98 for key
in s2
. intersection ( s1
):
99 # Test for non existence of key in d2
101 result
. add_error ( '"{}" should not exist in {} (have {}): \n {}' . format (
102 key
, parent
, str ( s1
), json_diff ( nd1
[ key
], nd2
[ key
])))
104 # If nd1 key is a dict, we have to recurse in it later.
105 if isinstance ( nd2
[ key
], type ({})):
106 if not isinstance ( nd1
[ key
], type ({})):
108 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
109 '(have {}, expected {}): \n {}' . format (
110 type ( nd1
[ key
]), type ( nd2
[ key
]), json_diff ( nd1
[ key
], nd2
[ key
])))
112 nparent
= '{}["{}"]' . format ( parent
, key
)
113 squeue
. append (( nd1
[ key
], nd2
[ key
], nparent
))
116 if isinstance ( nd2
[ key
], type ([])):
117 if not isinstance ( nd1
[ key
], type ([])):
119 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
120 '(have {}, expected {}): \n {}' . format (
121 type ( nd1
[ key
]), type ( nd2
[ key
]), json_diff ( nd1
[ key
], nd2
[ key
])))
124 if len ( nd2
[ key
]) > len ( nd1
[ key
]):
126 '{}["{}"] too few items ' . format ( parent
, key
) +
127 '(have {}, expected {}: \n {})' . format (
128 len ( nd1
[ key
]), len ( nd2
[ key
]),
129 json_diff ( nd1
[ key
], nd2
[ key
])))
132 # List all unmatched items errors
134 for expected
in nd2
[ key
]:
136 for value
in nd1
[ key
]:
137 if json_cmp ({ 'json' : value
}, { 'json' : expected
}) is None :
144 unmatched
. append ( expected
)
146 # If there are unmatched items, error out.
149 '{}["{}"] value is different ( \n {})' . format (
150 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
153 # Compare JSON values
154 if nd1
[ key
] != nd2
[ key
]:
156 '{}["{}"] value is different ( \n {})' . format (
157 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
160 if result
. has_errors ():
165 def run_and_expect ( func
, what
, count
= 20 , wait
= 3 ):
167 Run `func` and compare the result with `what`. Do it for `count` times
168 waiting `wait` seconds between tries. By default it tries 20 times with
169 3 seconds delay between tries.
171 Returns (True, func-return) on success or
172 (False, func-return) on failure.
180 return ( True , result
)
181 return ( False , result
)
185 "Converting Integer to DPID"
189 dpid
= '0' *( 16 - len ( dpid
))+ dpid
192 raise Exception ( 'Unable to derive default datapath ID - '
193 'please either specify a dpid or use a '
194 'canonical switch name such as s23.' )
197 "Check whether pid exists in the current process table."
203 except OSError as err
:
204 if err
. errno
== errno
. ESRCH
:
205 # ESRCH == No such process
207 elif err
. errno
== errno
. EPERM
:
208 # EPERM clearly means there's a process to deny access to
211 # According to "man 2 kill" possible error values are
212 # (EINVAL, EPERM, ESRCH)
217 def get_textdiff ( text1
, text2
, title1
= "" , title2
= "" , ** opts
):
218 "Returns empty string if same or formatted diff"
220 diff
= ' \n ' . join ( difflib
. unified_diff ( text1
, text2
,
221 fromfile
= title1
, tofile
= title2
, ** opts
))
222 # Clean up line endings
223 diff
= os
. linesep
. join ([ s
for s
in diff
. splitlines () if s
])
226 def difflines ( text1
, text2
, title1
= '' , title2
= '' , ** opts
):
227 "Wrapper for get_textdiff to avoid string transformations."
228 text1
= ( ' \n ' . join ( text1
. rstrip (). splitlines ()) + ' \n ' ). splitlines ( 1 )
229 text2
= ( ' \n ' . join ( text2
. rstrip (). splitlines ()) + ' \n ' ). splitlines ( 1 )
230 return get_textdiff ( text1
, text2
, title1
, title2
, ** opts
)
232 def get_file ( content
):
234 Generates a temporary file in '/tmp' with `content` and returns the file name.
236 fde
= tempfile
. NamedTemporaryFile ( mode
= 'w' , delete
= False )
242 def normalize_text ( text
):
244 Strips formating spaces/tabs and carriage returns.
246 text
= re
. sub ( r
'[ \t]+' , ' ' , text
)
247 text
= re
. sub ( r
'\r' , '' , text
)
250 def version_cmp ( v1
, v2
):
252 Compare two version strings and returns:
254 * `-1`: if `v1` is less than `v2`
255 * `0`: if `v1` is equal to `v2`
256 * `1`: if `v1` is greater than `v2`
258 Raises `ValueError` if versions are not well formated.
260 vregex
= r
'(?P<whole>\d+(\.(\d+))*)'
261 v1m
= re
. match ( vregex
, v1
)
262 v2m
= re
. match ( vregex
, v2
)
263 if v1m
is None or v2m
is None :
264 raise ValueError ( "got a invalid version string" )
267 v1g
= v1m
. group ( 'whole' ). split ( '.' )
268 v2g
= v2m
. group ( 'whole' ). split ( '.' )
270 # Get the longest version string
275 # Reverse list because we are going to pop the tail
278 for _
in range ( vnum
):
307 Gets a structured return of the command 'ip route'. It can be used in
308 conjuction with json_cmp() to provide accurate assert explanations.
323 output
= normalize_text ( node
. run ( 'ip route' )). splitlines ()
326 columns
= line
. split ( ' ' )
327 route
= result
[ columns
[ 0 ]] = {}
329 for column
in columns
:
331 route
[ 'dev' ] = column
333 route
[ 'via' ] = column
335 route
[ 'proto' ] = column
337 route
[ 'metric' ] = column
339 route
[ 'scope' ] = column
346 Gets a structured return of the command 'ip -6 route'. It can be used in
347 conjuction with json_cmp() to provide accurate assert explanations.
361 output
= normalize_text ( node
. run ( 'ip -6 route' )). splitlines ()
364 columns
= line
. split ( ' ' )
365 route
= result
[ columns
[ 0 ]] = {}
367 for column
in columns
:
369 route
[ 'dev' ] = column
371 route
[ 'via' ] = column
373 route
[ 'proto' ] = column
375 route
[ 'metric' ] = column
377 route
[ 'pref' ] = column
382 def sleep ( amount
, reason
= None ):
384 Sleep wrapper that registers in the log the amount of sleep
387 logger
. info ( 'Sleeping for {} seconds' . format ( amount
))
389 logger
. info ( reason
+ ' ({} seconds)' . format ( amount
))
393 def checkAddressSanitizerError ( output
, router
, component
):
394 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
396 addressSantizerError
= re
. search ( '(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ' , output
)
397 if addressSantizerError
:
398 sys
. stderr
. write ( " %s : %s triggered an exception by AddressSanitizer \n " % ( router
, component
))
399 # Sanitizer Error found in log
400 pidMark
= addressSantizerError
. group ( 1 )
401 addressSantizerLog
= re
. search ( ' %s (.*) %s ' % ( pidMark
, pidMark
), output
, re
. DOTALL
)
402 if addressSantizerLog
:
403 callingTest
= os
. path
. basename ( sys
._ current
_ frames
(). values ()[ 0 ]. f_back
. f_back
. f_globals
[ '__file__' ])
404 callingProc
= sys
._ getframe
( 2 ). f_code
. co_name
405 with
open ( "/tmp/AddressSanitzer.txt" , "a" ) as addrSanFile
:
406 sys
. stderr
. write ( ' \n ' . join ( addressSantizerLog
. group ( 1 ). splitlines ()) + ' \n ' )
407 addrSanFile
. write ( "## Error: %s \n\n " % addressSantizerError
. group ( 2 ))
408 addrSanFile
. write ( "### AddressSanitizer error in topotest ` %s `, test ` %s `, router ` %s ` \n\n " % ( callingTest
, callingProc
, router
))
409 addrSanFile
. write ( ' ' + ' \n ' . join ( addressSantizerLog
. group ( 1 ). splitlines ()) + ' \n ' )
410 addrSanFile
. write ( " \n --------------- \n " )
414 def addRouter ( topo
, name
):
415 "Adding a FRRouter (or Quagga) to Topology"
417 MyPrivateDirs
= [ '/etc/frr' ,
422 return topo
. addNode ( name
, cls
= Router
, privateDirs
= MyPrivateDirs
)
424 def set_sysctl ( node
, sysctl
, value
):
425 "Set a sysctl value and return None on success or an error string"
426 valuestr
= '{}' . format ( value
)
427 command
= "sysctl {0} = {1} " . format ( sysctl
, valuestr
)
428 cmdret
= node
. cmd ( command
)
430 matches
= re
. search ( r
'([^ ]+) = ([^\s]+)' , cmdret
)
433 if matches
. group ( 1 ) != sysctl
:
435 if matches
. group ( 2 ) != valuestr
:
440 def assert_sysctl ( node
, sysctl
, value
):
441 "Set and assert that the sysctl is set with the specified value."
442 assert set_sysctl ( node
, sysctl
, value
) is None
444 class LinuxRouter ( Node
):
445 "A Node with IPv4/IPv6 forwarding enabled."
447 def config ( self
, ** params
):
448 super ( LinuxRouter
, self
). config (** params
)
449 # Enable forwarding on the router
450 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
451 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
454 Terminate generic LinuxRouter Mininet instance
456 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
457 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
458 super ( LinuxRouter
, self
). terminate ()
461 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
463 def __init__ ( self
, name
, ** params
):
464 super ( Router
, self
) .__ init
__ ( name
, ** params
)
465 self
. logdir
= params
. get ( 'logdir' , '/tmp' )
466 self
. daemondir
= None
467 self
. routertype
= 'frr'
468 self
. daemons
= { 'zebra' : 0 , 'ripd' : 0 , 'ripngd' : 0 , 'ospfd' : 0 ,
469 'ospf6d' : 0 , 'isisd' : 0 , 'bgpd' : 0 , 'pimd' : 0 ,
470 'ldpd' : 0 , 'eigrpd' : 0 , 'nhrpd' : 0 }
472 def _config_frr ( self
, ** params
):
473 "Configure FRR binaries"
474 self
. daemondir
= params
. get ( 'frrdir' )
475 if self
. daemondir
is None :
476 self
. daemondir
= '/usr/lib/frr'
478 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
479 if not os
. path
. isfile ( zebra_path
):
480 raise Exception ( "FRR zebra binary doesn't exist at {}" . format ( zebra_path
))
482 def _config_quagga ( self
, ** params
):
483 "Configure Quagga binaries"
484 self
. daemondir
= params
. get ( 'quaggadir' )
485 if self
. daemondir
is None :
486 self
. daemondir
= '/usr/lib/quagga'
488 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
489 if not os
. path
. isfile ( zebra_path
):
490 raise Exception ( "Quagga zebra binary doesn't exist at {}" . format ( zebra_path
))
492 # pylint: disable=W0221
493 # Some params are only meaningful for the parent class.
494 def config ( self
, ** params
):
495 super ( Router
, self
). config (** params
)
497 # User did not specify the daemons directory, try to autodetect it.
498 self
. daemondir
= params
. get ( 'daemondir' )
499 if self
. daemondir
is None :
500 self
. routertype
= params
. get ( 'routertype' , 'frr' )
501 if self
. routertype
== 'quagga' :
502 self
._ config
_ quagga
(** params
)
504 self
._ config
_ frr
(** params
)
506 # Test the provided path
507 zpath
= os
. path
. join ( self
. daemondir
, 'zebra' )
508 if not os
. path
. isfile ( zpath
):
509 raise Exception ( 'No zebra binary found in {}' . format ( zpath
))
510 # Allow user to specify routertype when the path was specified.
511 if params
. get ( 'routertype' ) is not None :
512 self
. routertype
= self
. params
. get ( 'routertype' )
514 # Enable forwarding on the router
515 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
516 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
518 assert_sysctl ( self
, 'kernel.core_uses_pid' , 1 )
519 assert_sysctl ( self
, 'fs.suid_dumpable' , 2 )
520 corefile
= '{}/{}_ %e _core-sig_ %s- pid_%p.dmp' . format ( self
. logdir
, self
. name
)
521 assert_sysctl ( self
, 'kernel.core_pattern' , corefile
)
522 self
. cmd ( 'ulimit -c unlimited' )
523 # Set ownership of config files
524 self
. cmd ( 'chown {0} : {0} vty /etc/ {0} ' . format ( self
. routertype
))
527 # Delete Running Quagga or FRR Daemons
529 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
530 # for d in StringIO.StringIO(rundaemons):
531 # self.cmd('kill -7 `cat %s`' % d.rstrip())
534 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
535 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
536 super ( Router
, self
). terminate ()
537 def stopRouter ( self
, wait
= True ):
538 # Stop Running Quagga or FRR Daemons
539 rundaemons
= self
. cmd ( 'ls -1 /var/run/ %s /*.pid' % self
. routertype
)
540 if rundaemons
is not None :
542 for d
in StringIO
. StringIO ( rundaemons
):
543 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
544 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
545 logger
. info ( '{}: stopping {}' . format (
547 os
. path
. basename ( d
. rstrip (). rsplit ( "." , 1 )[ 0 ])
549 self
. cmd ( 'kill -TERM %s ' % daemonpid
)
551 if pid_exists ( int ( daemonpid
)):
553 if wait
and numRunning
> 0 :
554 sleep ( 2 , '{}: waiting for daemons stopping' . format ( self
. name
))
555 # 2nd round of kill if daemons didn't exit
556 for d
in StringIO
. StringIO ( rundaemons
):
557 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
558 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
559 logger
. info ( '{}: killing {}' . format (
561 os
. path
. basename ( d
. rstrip (). rsplit ( "." , 1 )[ 0 ])
563 self
. cmd ( 'kill -7 %s ' % daemonpid
)
566 for interface
in self
. intfNames ():
567 self
. cmd ( 'ip address flush' , interface
)
568 def loadConf ( self
, daemon
, source
= None ):
569 # print "Daemons before:", self.daemons
570 if daemon
in self
. daemons
. keys ():
571 self
. daemons
[ daemon
] = 1
573 self
. cmd ( 'touch /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
576 self
. cmd ( 'cp %s /etc/ %s / %s .conf' % ( source
, self
. routertype
, daemon
))
578 self
. cmd ( 'chmod 640 /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
580 self
. cmd ( 'chown %s : %s /etc/ %s / %s .conf' % ( self
. routertype
, self
. routertype
, self
. routertype
, daemon
))
583 logger
. info ( 'No daemon {} known' . format ( daemon
))
584 # print "Daemons after:", self.daemons
585 def startRouter ( self
):
586 # Disable integrated-vtysh-config
587 self
. cmd ( 'echo "no service integrated-vtysh-config" >> /etc/ %s /vtysh.conf' % self
. routertype
)
588 self
. cmd ( 'chown %s : %s vty /etc/ %s /vtysh.conf' % ( self
. routertype
, self
. routertype
, self
. routertype
))
589 # TODO remove the following lines after all tests are migrated to Topogen.
590 # Try to find relevant old logfiles in /tmp and delete them
591 map ( os
. remove
, glob
. glob ( "/tmp/* %s *.log" % self
. name
))
592 # Remove old core files
593 map ( os
. remove
, glob
. glob ( "/tmp/ %s *.dmp" % self
. name
))
594 # Remove IP addresses from OS first - we have them in zebra.conf
596 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
597 # No error - but return message and skip all the tests
598 if self
. daemons
[ 'ldpd' ] == 1 :
599 ldpd_path
= os
. path
. join ( self
. daemondir
, 'ldpd' )
600 if not os
. path
. isfile ( ldpd_path
):
601 logger
. info ( "LDP Test, but no ldpd compiled or installed" )
602 return "LDP Test, but no ldpd compiled or installed"
604 if version_cmp ( platform
. release (), '4.5' ) < 0 :
605 logger
. info ( "LDP Test need Linux Kernel 4.5 minimum" )
606 return "LDP Test need Linux Kernel 4.5 minimum"
608 # Check if required kernel modules are available with a dryrun modprobe
609 # Silent accept of modprobe command assumes ok status
610 if self
. cmd ( '/sbin/modprobe -n mpls-router' ) != "" :
611 logger
. info ( "LDP Test needs mpls-router kernel module" )
612 return "LDP Test needs mpls-router kernel module"
613 if self
. cmd ( '/sbin/modprobe -n mpls-iptunnel' ) != "" :
614 logger
. info ( "LDP Test needs mpls-iptunnel kernel module" )
615 return "LDP Test needs mpls-router kernel module"
617 self
. cmd ( '/sbin/modprobe mpls-router' )
618 self
. cmd ( '/sbin/modprobe mpls-iptunnel' )
619 self
. cmd ( 'echo 100000 > /proc/sys/net/mpls/platform_labels' )
621 if self
. daemons
[ 'eigrpd' ] == 1 :
622 eigrpd_path
= os
. path
. join ( self
. daemondir
, 'eigrpd' )
623 if not os
. path
. isfile ( eigrpd_path
):
624 logger
. info ( "EIGRP Test, but no eigrpd compiled or installed" )
625 return "EIGRP Test, but no eigrpd compiled or installed"
627 # Init done - now restarting daemons
630 def restartRouter ( self
):
631 # Starts actuall daemons without init (ie restart)
633 if self
. daemons
[ 'zebra' ] == 1 :
634 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
635 self
. cmd ( ' {0} > {1} / {2} -zebra.out 2> {1} / {2} -zebra.err &' . format (
636 zebra_path
, self
. logdir
, self
. name
639 logger
. debug ( '{}: {} zebra started' . format ( self
, self
. routertype
))
640 sleep ( 1 , '{}: waiting for zebra to start' . format ( self
. name
))
641 # Fix Link-Local Addresses
642 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
643 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' )
644 # Now start all the other daemons
645 for daemon
in self
. daemons
:
646 # Skip disabled daemons and zebra
647 if self
. daemons
[ daemon
] == 0 or daemon
== 'zebra' :
650 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
651 self
. cmd ( ' {0} > {1} / {2} - {3} .out 2> {1} / {2} - {3} .err &' . format (
652 daemon_path
, self
. logdir
, self
. name
, daemon
655 logger
. debug ( '{}: {} {} started' . format ( self
, self
. routertype
, daemon
))
656 def getStdErr ( self
, daemon
):
657 return self
. getLog ( 'err' , daemon
)
658 def getStdOut ( self
, daemon
):
659 return self
. getLog ( 'out' , daemon
)
660 def getLog ( self
, log
, daemon
):
661 return self
. cmd ( 'cat {}/{}-{}.{}' . format ( self
. logdir
, self
. name
, daemon
, log
))
662 def checkRouterRunning ( self
):
663 "Check if router daemons are running and collect crashinfo they don't run"
667 daemonsRunning
= self
. cmd ( 'vtysh -c "show log" | grep "Logging configuration for"' )
668 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
669 if checkAddressSanitizerError ( daemonsRunning
, self
. name
, "vtysh" ):
670 return " %s : vtysh killed by AddressSanitizer" % ( self
. name
)
672 for daemon
in self
. daemons
:
673 if ( self
. daemons
[ daemon
] == 1 ) and not ( daemon
in daemonsRunning
):
674 sys
. stderr
. write ( " %s : Daemon %s not running \n " % ( self
. name
, daemon
))
676 corefiles
= glob
. glob ( '{}/{}_{}_core*.dmp' . format (
677 self
. logdir
, self
. name
, daemon
))
678 if ( len ( corefiles
) > 0 ):
679 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
680 backtrace
= subprocess
. check_output ([
681 "gdb {} {} --batch -ex bt 2> /dev/null" . format ( daemon_path
, corefiles
[ 0 ])
683 sys
. stderr
. write ( " \n %s : %s crashed. Core file found - Backtrace follows: \n " % ( self
. name
, daemon
))
684 sys
. stderr
. write ( " %s \n " % backtrace
)
686 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
687 if os
. path
. isfile ( '{}/{}-{}.log' . format ( self
. logdir
, self
. name
, daemon
)):
688 log_tail
= subprocess
. check_output ([
689 "tail -n20 {}/{}-{}.log 2> /dev/null" . format (
690 self
. logdir
, self
. name
, daemon
)
692 sys
. stderr
. write ( " \n From %s %s %s log file: \n " % ( self
. routertype
, self
. name
, daemon
))
693 sys
. stderr
. write ( " %s \n " % log_tail
)
695 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
696 if checkAddressSanitizerError ( self
. getStdErr ( daemon
), self
. name
, daemon
):
697 return " %s : Daemon %s not running - killed by AddressSanitizer" % ( self
. name
, daemon
)
699 return " %s : Daemon %s not running" % ( self
. name
, daemon
)
701 def get_ipv6_linklocal ( self
):
702 "Get LinkLocal Addresses from interfaces"
706 ifaces
= self
. cmd ( 'ip -6 address' )
707 # Fix newlines (make them all the same)
708 ifaces
= ( ' \n ' . join ( ifaces
. splitlines ()) + ' \n ' ). splitlines ()
712 m
= re
. search ( '[0-9]+: ([^:@]+)[@if0-9:]+ <' , line
)
714 interface
= m
. group ( 1 )
716 m
= re
. search ( 'inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link' , line
)
720 if ( ll_per_if_count
> 1 ):
721 linklocal
+= [[ " %s-%s " % ( interface
, ll_per_if_count
), local
]]
723 linklocal
+= [[ interface
, local
]]
725 def daemon_available ( self
, daemon
):
726 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
728 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
729 if not os
. path
. isfile ( daemon_path
):
731 if ( daemon
== 'ldpd' ):
732 if version_cmp ( platform
. release (), '4.5' ) < 0 :
734 if self
. cmd ( '/sbin/modprobe -n mpls-router' ) != "" :
736 if self
. cmd ( '/sbin/modprobe -n mpls-iptunnel' ) != "" :
740 def get_routertype ( self
):
741 "Return the type of Router (frr or quagga)"
743 return self
. routertype
744 def report_memory_leaks ( self
, filename_prefix
, testscript
):
745 "Report Memory Leaks to file prefixed with given string"
748 filename
= filename_prefix
+ re
. sub ( r
"\.py" , "" , testscript
) + ".txt"
749 for daemon
in self
. daemons
:
750 if ( self
. daemons
[ daemon
] == 1 ):
751 log
= self
. getStdErr ( daemon
)
752 if "memstats" in log
:
754 logger
. info ( ' \n Router {} {} StdErr Log: \n {}' . format (
755 self
. name
, daemon
, log
))
758 # Check if file already exists
759 fileexists
= os
. path
. isfile ( filename
)
760 leakfile
= open ( filename
, "a" )
762 # New file - add header
763 leakfile
. write ( "# Memory Leak Detection for topotest %s \n\n " % testscript
)
764 leakfile
. write ( "## Router %s \n " % self
. name
)
765 leakfile
. write ( "### Process %s \n " % daemon
)
766 log
= re
. sub ( "core_handler: " , "" , log
)
767 log
= re
. sub ( r
"(showing active allocations in memory group [a-zA-Z0-9]+)" , r
"\n#### \1\n" , log
)
768 log
= re
. sub ( "memstats: " , " " , log
)
775 class LegacySwitch ( OVSSwitch
):
776 "A Legacy Switch without OpenFlow"
778 def __init__ ( self
, name
, ** params
):
779 OVSSwitch
.__ init
__ ( self
, name
, failMode
= 'standalone' , ** params
)