]>
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 {})' . format (
96 str ( list ( diff
)), parent
, str ( list ( s1
))))
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 {})' . format (
102 key
, parent
, str ( s1
)))
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 {})' . format ( type ( nd1
[ key
]), type ( nd2
[ key
])))
111 nparent
= '{}["{}"]' . format ( parent
, key
)
112 squeue
. append (( nd1
[ key
], nd2
[ key
], nparent
))
115 if isinstance ( nd2
[ key
], type ([])):
116 if not isinstance ( nd1
[ key
], type ([])):
118 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
119 '(have {}, expected {})' . format ( type ( nd1
[ key
]), type ( nd2
[ key
])))
122 if len ( nd2
[ key
]) > len ( nd1
[ key
]):
124 '{}["{}"] too few items ' . format ( parent
, key
) +
125 '(have {}, expected {}: \n {})' . format (
126 len ( nd1
[ key
]), len ( nd2
[ key
]),
127 json_diff ( nd1
[ key
], nd2
[ key
])))
130 # List all unmatched items errors
132 for expected
in nd2
[ key
]:
134 for value
in nd1
[ key
]:
135 if json_cmp ({ 'json' : value
}, { 'json' : expected
}) is None :
142 unmatched
. append ( expected
)
144 # If there are unmatched items, error out.
147 '{}["{}"] value is different ( \n {})' . format (
148 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
151 # Compare JSON values
152 if nd1
[ key
] != nd2
[ key
]:
154 '{}["{}"] value is different ( \n {})' . format (
155 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
158 if result
. has_errors ():
163 def run_and_expect ( func
, what
, count
= 20 , wait
= 3 ):
165 Run `func` and compare the result with `what`. Do it for `count` times
166 waiting `wait` seconds between tries. By default it tries 20 times with
167 3 seconds delay between tries.
169 Returns (True, func-return) on success or
170 (False, func-return) on failure.
178 return ( True , result
)
179 return ( False , result
)
183 "Converting Integer to DPID"
187 dpid
= '0' *( 16 - len ( dpid
))+ dpid
190 raise Exception ( 'Unable to derive default datapath ID - '
191 'please either specify a dpid or use a '
192 'canonical switch name such as s23.' )
195 "Check whether pid exists in the current process table."
201 except OSError as err
:
202 if err
. errno
== errno
. ESRCH
:
203 # ESRCH == No such process
205 elif err
. errno
== errno
. EPERM
:
206 # EPERM clearly means there's a process to deny access to
209 # According to "man 2 kill" possible error values are
210 # (EINVAL, EPERM, ESRCH)
215 def get_textdiff ( text1
, text2
, title1
= "" , title2
= "" , ** opts
):
216 "Returns empty string if same or formatted diff"
218 diff
= ' \n ' . join ( difflib
. unified_diff ( text1
, text2
,
219 fromfile
= title1
, tofile
= title2
, ** opts
))
220 # Clean up line endings
221 diff
= os
. linesep
. join ([ s
for s
in diff
. splitlines () if s
])
224 def difflines ( text1
, text2
, title1
= '' , title2
= '' , ** opts
):
225 "Wrapper for get_textdiff to avoid string transformations."
226 text1
= ( ' \n ' . join ( text1
. rstrip (). splitlines ()) + ' \n ' ). splitlines ( 1 )
227 text2
= ( ' \n ' . join ( text2
. rstrip (). splitlines ()) + ' \n ' ). splitlines ( 1 )
228 return get_textdiff ( text1
, text2
, title1
, title2
, ** opts
)
230 def get_file ( content
):
232 Generates a temporary file in '/tmp' with `content` and returns the file name.
234 fde
= tempfile
. NamedTemporaryFile ( mode
= 'w' , delete
= False )
240 def normalize_text ( text
):
242 Strips formating spaces/tabs and carriage returns.
244 text
= re
. sub ( r
'[ \t]+' , ' ' , text
)
245 text
= re
. sub ( r
'\r' , '' , text
)
248 def version_cmp ( v1
, v2
):
250 Compare two version strings and returns:
252 * `-1`: if `v1` is less than `v2`
253 * `0`: if `v1` is equal to `v2`
254 * `1`: if `v1` is greater than `v2`
256 Raises `ValueError` if versions are not well formated.
258 vregex
= r
'(?P<whole>\d+(\.(\d+))*)'
259 v1m
= re
. match ( vregex
, v1
)
260 v2m
= re
. match ( vregex
, v2
)
261 if v1m
is None or v2m
is None :
262 raise ValueError ( "got a invalid version string" )
265 v1g
= v1m
. group ( 'whole' ). split ( '.' )
266 v2g
= v2m
. group ( 'whole' ). split ( '.' )
268 # Get the longest version string
273 # Reverse list because we are going to pop the tail
276 for _
in range ( vnum
):
305 Gets a structured return of the command 'ip route'. It can be used in
306 conjuction with json_cmp() to provide accurate assert explanations.
321 output
= normalize_text ( node
. run ( 'ip route' )). splitlines ()
324 columns
= line
. split ( ' ' )
325 route
= result
[ columns
[ 0 ]] = {}
327 for column
in columns
:
329 route
[ 'dev' ] = column
331 route
[ 'via' ] = column
333 route
[ 'proto' ] = column
335 route
[ 'metric' ] = column
337 route
[ 'scope' ] = column
344 Gets a structured return of the command 'ip -6 route'. It can be used in
345 conjuction with json_cmp() to provide accurate assert explanations.
359 output
= normalize_text ( node
. run ( 'ip -6 route' )). splitlines ()
362 columns
= line
. split ( ' ' )
363 route
= result
[ columns
[ 0 ]] = {}
365 for column
in columns
:
367 route
[ 'dev' ] = column
369 route
[ 'via' ] = column
371 route
[ 'proto' ] = column
373 route
[ 'metric' ] = column
375 route
[ 'pref' ] = column
380 def sleep ( amount
, reason
= None ):
382 Sleep wrapper that registers in the log the amount of sleep
385 logger
. info ( 'Sleeping for {} seconds' . format ( amount
))
387 logger
. info ( reason
+ ' ({} seconds)' . format ( amount
))
391 def checkAddressSanitizerError ( output
, router
, component
):
392 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
394 addressSantizerError
= re
. search ( '(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ' , output
)
395 if addressSantizerError
:
396 sys
. stderr
. write ( " %s : %s triggered an exception by AddressSanitizer \n " % ( router
, component
))
397 # Sanitizer Error found in log
398 pidMark
= addressSantizerError
. group ( 1 )
399 addressSantizerLog
= re
. search ( ' %s (.*) %s ' % ( pidMark
, pidMark
), output
, re
. DOTALL
)
400 if addressSantizerLog
:
401 callingTest
= os
. path
. basename ( sys
._ current
_ frames
(). values ()[ 0 ]. f_back
. f_back
. f_globals
[ '__file__' ])
402 callingProc
= sys
._ getframe
( 2 ). f_code
. co_name
403 with
open ( "/tmp/AddressSanitzer.txt" , "a" ) as addrSanFile
:
404 sys
. stderr
. write ( ' \n ' . join ( addressSantizerLog
. group ( 1 ). splitlines ()) + ' \n ' )
405 addrSanFile
. write ( "## Error: %s \n\n " % addressSantizerError
. group ( 2 ))
406 addrSanFile
. write ( "### AddressSanitizer error in topotest ` %s `, test ` %s `, router ` %s ` \n\n " % ( callingTest
, callingProc
, router
))
407 addrSanFile
. write ( ' ' + ' \n ' . join ( addressSantizerLog
. group ( 1 ). splitlines ()) + ' \n ' )
408 addrSanFile
. write ( " \n --------------- \n " )
412 def addRouter ( topo
, name
):
413 "Adding a FRRouter (or Quagga) to Topology"
415 MyPrivateDirs
= [ '/etc/frr' ,
420 return topo
. addNode ( name
, cls
= Router
, privateDirs
= MyPrivateDirs
)
422 def set_sysctl ( node
, sysctl
, value
):
423 "Set a sysctl value and return None on success or an error string"
424 valuestr
= '{}' . format ( value
)
425 command
= "sysctl {0} = {1} " . format ( sysctl
, valuestr
)
426 cmdret
= node
. cmd ( command
)
428 matches
= re
. search ( r
'([^ ]+) = ([^\s]+)' , cmdret
)
431 if matches
. group ( 1 ) != sysctl
:
433 if matches
. group ( 2 ) != valuestr
:
438 def assert_sysctl ( node
, sysctl
, value
):
439 "Set and assert that the sysctl is set with the specified value."
440 assert set_sysctl ( node
, sysctl
, value
) is None
442 class LinuxRouter ( Node
):
443 "A Node with IPv4/IPv6 forwarding enabled."
445 def config ( self
, ** params
):
446 super ( LinuxRouter
, self
). config (** params
)
447 # Enable forwarding on the router
448 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
449 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
452 Terminate generic LinuxRouter Mininet instance
454 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
455 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
456 super ( LinuxRouter
, self
). terminate ()
459 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
461 def __init__ ( self
, name
, ** params
):
462 super ( Router
, self
) .__ init
__ ( name
, ** params
)
463 self
. logdir
= params
. get ( 'logdir' , '/tmp' )
464 self
. daemondir
= None
465 self
. routertype
= 'frr'
466 self
. daemons
= { 'zebra' : 0 , 'ripd' : 0 , 'ripngd' : 0 , 'ospfd' : 0 ,
467 'ospf6d' : 0 , 'isisd' : 0 , 'bgpd' : 0 , 'pimd' : 0 ,
468 'ldpd' : 0 , 'eigrpd' : 0 , 'nhrpd' : 0 }
470 def _config_frr ( self
, ** params
):
471 "Configure FRR binaries"
472 self
. daemondir
= params
. get ( 'frrdir' )
473 if self
. daemondir
is None :
474 self
. daemondir
= '/usr/lib/frr'
476 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
477 if not os
. path
. isfile ( zebra_path
):
478 raise Exception ( "FRR zebra binary doesn't exist at {}" . format ( zebra_path
))
480 def _config_quagga ( self
, ** params
):
481 "Configure Quagga binaries"
482 self
. daemondir
= params
. get ( 'quaggadir' )
483 if self
. daemondir
is None :
484 self
. daemondir
= '/usr/lib/quagga'
486 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
487 if not os
. path
. isfile ( zebra_path
):
488 raise Exception ( "Quagga zebra binary doesn't exist at {}" . format ( zebra_path
))
490 # pylint: disable=W0221
491 # Some params are only meaningful for the parent class.
492 def config ( self
, ** params
):
493 super ( Router
, self
). config (** params
)
495 # User did not specify the daemons directory, try to autodetect it.
496 self
. daemondir
= params
. get ( 'daemondir' )
497 if self
. daemondir
is None :
498 self
. routertype
= params
. get ( 'routertype' , 'frr' )
499 if self
. routertype
== 'quagga' :
500 self
._ config
_ quagga
(** params
)
502 self
._ config
_ frr
(** params
)
504 # Test the provided path
505 zpath
= os
. path
. join ( self
. daemondir
, 'zebra' )
506 if not os
. path
. isfile ( zpath
):
507 raise Exception ( 'No zebra binary found in {}' . format ( zpath
))
508 # Allow user to specify routertype when the path was specified.
509 if params
. get ( 'routertype' ) is not None :
510 self
. routertype
= self
. params
. get ( 'routertype' )
512 # Enable forwarding on the router
513 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
514 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
516 assert_sysctl ( self
, 'kernel.core_uses_pid' , 1 )
517 assert_sysctl ( self
, 'fs.suid_dumpable' , 2 )
518 corefile
= '{}/{}_ %e _core-sig_ %s- pid_%p.dmp' . format ( self
. logdir
, self
. name
)
519 assert_sysctl ( self
, 'kernel.core_pattern' , corefile
)
520 self
. cmd ( 'ulimit -c unlimited' )
521 # Set ownership of config files
522 self
. cmd ( 'chown {0} : {0} vty /etc/ {0} ' . format ( self
. routertype
))
525 # Delete Running Quagga or FRR Daemons
527 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
528 # for d in StringIO.StringIO(rundaemons):
529 # self.cmd('kill -7 `cat %s`' % d.rstrip())
532 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
533 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
534 super ( Router
, self
). terminate ()
535 def stopRouter ( self
):
536 # Stop Running Quagga or FRR Daemons
537 rundaemons
= self
. cmd ( 'ls -1 /var/run/ %s /*.pid' % self
. routertype
)
538 if rundaemons
is not None :
539 for d
in StringIO
. StringIO ( rundaemons
):
540 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
541 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
542 self
. cmd ( 'kill -TERM %s ' % daemonpid
)
544 sleep ( 2 , 'waiting for router "{}" daemons to finish' . format (
546 # 2nd round of kill if daemons didn't exist
547 for d
in StringIO
. StringIO ( rundaemons
):
548 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
549 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
550 self
. cmd ( 'kill -7 %s ' % daemonpid
)
553 for interface
in self
. intfNames ():
554 self
. cmd ( 'ip address flush' , interface
)
555 def loadConf ( self
, daemon
, source
= None ):
556 # print "Daemons before:", self.daemons
557 if daemon
in self
. daemons
. keys ():
558 self
. daemons
[ daemon
] = 1
560 self
. cmd ( 'touch /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
563 self
. cmd ( 'cp %s /etc/ %s / %s .conf' % ( source
, self
. routertype
, daemon
))
565 self
. cmd ( 'chmod 640 /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
567 self
. cmd ( 'chown %s : %s /etc/ %s / %s .conf' % ( self
. routertype
, self
. routertype
, self
. routertype
, daemon
))
570 logger
. info ( 'No daemon {} known' . format ( daemon
))
571 # print "Daemons after:", self.daemons
572 def startRouter ( self
):
573 # Disable integrated-vtysh-config
574 self
. cmd ( 'echo "no service integrated-vtysh-config" >> /etc/ %s /vtysh.conf' % self
. routertype
)
575 self
. cmd ( 'chown %s : %s vty /etc/ %s /vtysh.conf' % ( self
. routertype
, self
. routertype
, self
. routertype
))
576 # TODO remove the following lines after all tests are migrated to Topogen.
577 # Try to find relevant old logfiles in /tmp and delete them
578 map ( os
. remove
, glob
. glob ( "/tmp/* %s *.log" % self
. name
))
579 # Remove old core files
580 map ( os
. remove
, glob
. glob ( "/tmp/ %s *.dmp" % self
. name
))
581 # Remove IP addresses from OS first - we have them in zebra.conf
583 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
584 # No error - but return message and skip all the tests
585 if self
. daemons
[ 'ldpd' ] == 1 :
586 ldpd_path
= os
. path
. join ( self
. daemondir
, 'ldpd' )
587 if not os
. path
. isfile ( ldpd_path
):
588 logger
. info ( "LDP Test, but no ldpd compiled or installed" )
589 return "LDP Test, but no ldpd compiled or installed"
591 if version_cmp ( platform
. release (), '4.5' ) < 0 :
592 logger
. info ( "LDP Test need Linux Kernel 4.5 minimum" )
593 return "LDP Test need Linux Kernel 4.5 minimum"
595 # Check if required kernel modules are available with a dryrun modprobe
596 # Silent accept of modprobe command assumes ok status
597 if self
. cmd ( '/sbin/modprobe -n mpls-router' ) != "" :
598 logger
. info ( "LDP Test needs mpls-router kernel module" )
599 return "LDP Test needs mpls-router kernel module"
600 if self
. cmd ( '/sbin/modprobe -n mpls-iptunnel' ) != "" :
601 logger
. info ( "LDP Test needs mpls-iptunnel kernel module" )
602 return "LDP Test needs mpls-router kernel module"
604 self
. cmd ( '/sbin/modprobe mpls-router' )
605 self
. cmd ( '/sbin/modprobe mpls-iptunnel' )
606 self
. cmd ( 'echo 100000 > /proc/sys/net/mpls/platform_labels' )
608 if self
. daemons
[ 'eigrpd' ] == 1 :
609 eigrpd_path
= os
. path
. join ( self
. daemondir
, 'eigrpd' )
610 if not os
. path
. isfile ( eigrpd_path
):
611 logger
. info ( "EIGRP Test, but no eigrpd compiled or installed" )
612 return "EIGRP Test, but no eigrpd compiled or installed"
614 # Init done - now restarting daemons
617 def restartRouter ( self
):
618 # Starts actuall daemons without init (ie restart)
620 if self
. daemons
[ 'zebra' ] == 1 :
621 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
622 self
. cmd ( ' {0} > {1} / {2} -zebra.out 2> {1} / {2} -zebra.err &' . format (
623 zebra_path
, self
. logdir
, self
. name
626 logger
. debug ( '{}: {} zebra started' . format ( self
, self
. routertype
))
628 # Fix Link-Local Addresses
629 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
630 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' )
631 # Now start all the other daemons
632 for daemon
in self
. daemons
:
633 # Skip disabled daemons and zebra
634 if self
. daemons
[ daemon
] == 0 or daemon
== 'zebra' :
637 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
638 self
. cmd ( ' {0} > {1} / {2} - {3} .out 2> {1} / {2} - {3} .err &' . format (
639 daemon_path
, self
. logdir
, self
. name
, daemon
642 logger
. debug ( '{}: {} {} started' . format ( self
, self
. routertype
, daemon
))
643 def getStdErr ( self
, daemon
):
644 return self
. getLog ( 'err' , daemon
)
645 def getStdOut ( self
, daemon
):
646 return self
. getLog ( 'out' , daemon
)
647 def getLog ( self
, log
, daemon
):
648 return self
. cmd ( 'cat {}/{}-{}.{}' . format ( self
. logdir
, self
. name
, daemon
, log
))
649 def checkRouterRunning ( self
):
650 "Check if router daemons are running and collect crashinfo they don't run"
654 daemonsRunning
= self
. cmd ( 'vtysh -c "show log" | grep "Logging configuration for"' )
655 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
656 if checkAddressSanitizerError ( daemonsRunning
, self
. name
, "vtysh" ):
657 return " %s : vtysh killed by AddressSanitizer" % ( self
. name
)
659 for daemon
in self
. daemons
:
660 if ( self
. daemons
[ daemon
] == 1 ) and not ( daemon
in daemonsRunning
):
661 sys
. stderr
. write ( " %s : Daemon %s not running \n " % ( self
. name
, daemon
))
663 corefiles
= glob
. glob ( '{}/{}_{}_core*.dmp' . format (
664 self
. logdir
, self
. name
, daemon
))
665 if ( len ( corefiles
) > 0 ):
666 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
667 backtrace
= subprocess
. check_output ([
668 "gdb {} {} --batch -ex bt 2> /dev/null" . format ( daemon_path
, corefiles
[ 0 ])
670 sys
. stderr
. write ( " \n %s : %s crashed. Core file found - Backtrace follows: \n " % ( self
. name
, daemon
))
671 sys
. stderr
. write ( " %s \n " % backtrace
)
673 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
674 if os
. path
. isfile ( '{}/{}-{}.log' . format ( self
. logdir
, self
. name
, daemon
)):
675 log_tail
= subprocess
. check_output ([
676 "tail -n20 {}/{}-{}.log 2> /dev/null" . format (
677 self
. logdir
, self
. name
, daemon
)
679 sys
. stderr
. write ( " \n From %s %s %s log file: \n " % ( self
. routertype
, self
. name
, daemon
))
680 sys
. stderr
. write ( " %s \n " % log_tail
)
682 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
683 if checkAddressSanitizerError ( self
. getStdErr ( daemon
), self
. name
, daemon
):
684 return " %s : Daemon %s not running - killed by AddressSanitizer" % ( self
. name
, daemon
)
686 return " %s : Daemon %s not running" % ( self
. name
, daemon
)
688 def get_ipv6_linklocal ( self
):
689 "Get LinkLocal Addresses from interfaces"
693 ifaces
= self
. cmd ( 'ip -6 address' )
694 # Fix newlines (make them all the same)
695 ifaces
= ( ' \n ' . join ( ifaces
. splitlines ()) + ' \n ' ). splitlines ()
699 m
= re
. search ( '[0-9]+: ([^:@]+)[@if0-9:]+ <' , line
)
701 interface
= m
. group ( 1 )
703 m
= re
. search ( 'inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link' , line
)
707 if ( ll_per_if_count
> 1 ):
708 linklocal
+= [[ " %s-%s " % ( interface
, ll_per_if_count
), local
]]
710 linklocal
+= [[ interface
, local
]]
712 def daemon_available ( self
, daemon
):
713 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
715 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
716 if not os
. path
. isfile ( daemon_path
):
718 if ( daemon
== 'ldpd' ):
719 if version_cmp ( platform
. release (), '4.5' ) < 0 :
721 if self
. cmd ( '/sbin/modprobe -n mpls-router' ) != "" :
723 if self
. cmd ( '/sbin/modprobe -n mpls-iptunnel' ) != "" :
727 def get_routertype ( self
):
728 "Return the type of Router (frr or quagga)"
730 return self
. routertype
731 def report_memory_leaks ( self
, filename_prefix
, testscript
):
732 "Report Memory Leaks to file prefixed with given string"
735 filename
= filename_prefix
+ re
. sub ( r
"\.py" , "" , testscript
) + ".txt"
736 for daemon
in self
. daemons
:
737 if ( self
. daemons
[ daemon
] == 1 ):
738 log
= self
. getStdErr ( daemon
)
739 if "memstats" in log
:
741 logger
. info ( ' \n Router {} {} StdErr Log: \n {}' . format (
742 self
. name
, daemon
, log
))
745 # Check if file already exists
746 fileexists
= os
. path
. isfile ( filename
)
747 leakfile
= open ( filename
, "a" )
749 # New file - add header
750 leakfile
. write ( "# Memory Leak Detection for topotest %s \n\n " % testscript
)
751 leakfile
. write ( "## Router %s \n " % self
. name
)
752 leakfile
. write ( "### Process %s \n " % daemon
)
753 log
= re
. sub ( "core_handler: " , "" , log
)
754 log
= re
. sub ( r
"(showing active allocations in memory group [a-zA-Z0-9]+)" , r
"\n#### \1\n" , log
)
755 log
= re
. sub ( "memstats: " , " " , log
)
762 class LegacySwitch ( OVSSwitch
):
763 "A Legacy Switch without OpenFlow"
765 def __init__ ( self
, name
, ** params
):
766 OVSSwitch
.__ init
__ ( self
, name
, failMode
= 'standalone' , ** params
)