]>
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 get_test_logdir ( node
= None , init
= False ):
64 Return the current test log directory based on PYTEST_CURRENT_TEST
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
70 cur_test
= os
. environ
[ 'PYTEST_CURRENT_TEST' ]
72 ret
= '/tmp/topotests/' + cur_test
[ 0 : cur_test
. find ( ".py" )]. replace ( '/' , '.' )
74 dir = ret
+ "/" + node
76 os
. system ( 'mkdir -p ' + dir )
77 os
. system ( 'chmod -R go+rw /tmp/topotests' )
80 def json_diff ( d1
, d2
):
82 Returns a string with the difference between JSON data.
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 )
94 JSON compare function. Receives two parameters:
96 * `d2`: json subset which we expect
98 Returns `None` when all keys that `d1` has matches `d2`,
99 otherwise a string containing what failed.
101 Note: key absence can be tested by adding a key with value `None`.
103 squeue
= [( d1
, d2
, 'json' )]
104 result
= json_cmp_result ()
107 s1
, s2
= set ( nd1
), set ( nd2
)
109 # Expect all required fields to exist.
110 s2_req
= set ([ key
for key
in nd2
if nd2
[ key
] is not None ])
113 result
. add_error ( 'expected key(s) {} in {} (have {}): \n {}' . format (
114 str ( list ( diff
)), parent
, str ( list ( s1
)), json_diff ( nd1
, nd2
)))
116 for key
in s2
. intersection ( s1
):
117 # Test for non existence of key in d2
119 result
. add_error ( '"{}" should not exist in {} (have {}): \n {}' . format (
120 key
, parent
, str ( s1
), json_diff ( nd1
[ key
], nd2
[ key
])))
122 # If nd1 key is a dict, we have to recurse in it later.
123 if isinstance ( nd2
[ key
], type ({})):
124 if not isinstance ( nd1
[ key
], type ({})):
126 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
127 '(have {}, expected {}): \n {}' . format (
128 type ( nd1
[ key
]), type ( nd2
[ key
]), json_diff ( nd1
[ key
], nd2
[ key
])))
130 nparent
= '{}["{}"]' . format ( parent
, key
)
131 squeue
. append (( nd1
[ key
], nd2
[ key
], nparent
))
134 if isinstance ( nd2
[ key
], type ([])):
135 if not isinstance ( nd1
[ key
], type ([])):
137 '{}["{}"] has different type than expected ' . format ( parent
, key
) +
138 '(have {}, expected {}): \n {}' . format (
139 type ( nd1
[ key
]), type ( nd2
[ key
]), json_diff ( nd1
[ key
], nd2
[ key
])))
142 if len ( nd2
[ key
]) > len ( nd1
[ key
]):
144 '{}["{}"] too few items ' . format ( parent
, key
) +
145 '(have {}, expected {}: \n {})' . format (
146 len ( nd1
[ key
]), len ( nd2
[ key
]),
147 json_diff ( nd1
[ key
], nd2
[ key
])))
150 # List all unmatched items errors
152 for expected
in nd2
[ key
]:
154 for value
in nd1
[ key
]:
155 if json_cmp ({ 'json' : value
}, { 'json' : expected
}) is None :
162 unmatched
. append ( expected
)
164 # If there are unmatched items, error out.
167 '{}["{}"] value is different ( \n {})' . format (
168 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
171 # Compare JSON values
172 if nd1
[ key
] != nd2
[ key
]:
174 '{}["{}"] value is different ( \n {})' . format (
175 parent
, key
, json_diff ( nd1
[ key
], nd2
[ key
])))
178 if result
. has_errors ():
183 def run_and_expect ( func
, what
, count
= 20 , wait
= 3 ):
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.
189 Returns (True, func-return) on success or
190 (False, func-return) on failure.
198 return ( True , result
)
199 return ( False , result
)
203 "Converting Integer to DPID"
207 dpid
= '0' *( 16 - len ( dpid
))+ dpid
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.' )
215 "Check whether pid exists in the current process table."
221 except OSError as err
:
222 if err
. errno
== errno
. ESRCH
:
223 # ESRCH == No such process
225 elif err
. errno
== errno
. EPERM
:
226 # EPERM clearly means there's a process to deny access to
229 # According to "man 2 kill" possible error values are
230 # (EINVAL, EPERM, ESRCH)
235 def get_textdiff ( text1
, text2
, title1
= "" , title2
= "" , ** opts
):
236 "Returns empty string if same or formatted diff"
238 diff
= ' \n ' . join ( difflib
. unified_diff ( text1
, text2
,
239 fromfile
= title1
, tofile
= title2
, ** opts
))
240 # Clean up line endings
241 diff
= os
. linesep
. join ([ s
for s
in diff
. splitlines () if s
])
244 def difflines ( text1
, text2
, title1
= '' , title2
= '' , ** opts
):
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 )
248 return get_textdiff ( text1
, text2
, title1
, title2
, ** opts
)
250 def get_file ( content
):
252 Generates a temporary file in '/tmp' with `content` and returns the file name.
254 fde
= tempfile
. NamedTemporaryFile ( mode
= 'w' , delete
= False )
260 def normalize_text ( text
):
262 Strips formating spaces/tabs and carriage returns.
264 text
= re
. sub ( r
'[ \t]+' , ' ' , text
)
265 text
= re
. sub ( r
'\r' , '' , text
)
268 def version_cmp ( v1
, v2
):
270 Compare two version strings and returns:
272 * `-1`: if `v1` is less than `v2`
273 * `0`: if `v1` is equal to `v2`
274 * `1`: if `v1` is greater than `v2`
276 Raises `ValueError` if versions are not well formated.
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" )
285 v1g
= v1m
. group ( 'whole' ). split ( '.' )
286 v2g
= v2m
. group ( 'whole' ). split ( '.' )
288 # Get the longest version string
293 # Reverse list because we are going to pop the tail
296 for _
in range ( vnum
):
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.
341 output
= normalize_text ( node
. run ( 'ip route' )). splitlines ()
344 columns
= line
. split ( ' ' )
345 route
= result
[ columns
[ 0 ]] = {}
347 for column
in columns
:
349 route
[ 'dev' ] = column
351 route
[ 'via' ] = column
353 route
[ 'proto' ] = column
355 route
[ 'metric' ] = column
357 route
[ 'scope' ] = column
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.
379 output
= normalize_text ( node
. run ( 'ip -6 route' )). splitlines ()
382 columns
= line
. split ( ' ' )
383 route
= result
[ columns
[ 0 ]] = {}
385 for column
in columns
:
387 route
[ 'dev' ] = column
389 route
[ 'via' ] = column
391 route
[ 'proto' ] = column
393 route
[ 'metric' ] = column
395 route
[ 'pref' ] = column
400 def sleep ( amount
, reason
= None ):
402 Sleep wrapper that registers in the log the amount of sleep
405 logger
. info ( 'Sleeping for {} seconds' . format ( amount
))
407 logger
. info ( reason
+ ' ({} seconds)' . format ( amount
))
411 def checkAddressSanitizerError ( output
, router
, component
):
412 "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise"
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 " )
432 def addRouter ( topo
, name
):
433 "Adding a FRRouter (or Quagga) to Topology"
435 MyPrivateDirs
= [ '/etc/frr' ,
440 return topo
. addNode ( name
, cls
= Router
, privateDirs
= MyPrivateDirs
)
442 def 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
)
448 matches
= re
. search ( r
'([^ ]+) = ([^\s]+)' , cmdret
)
451 if matches
. group ( 1 ) != sysctl
:
453 if matches
. group ( 2 ) != valuestr
:
458 def 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
462 class LinuxRouter ( Node
):
463 "A Node with IPv4/IPv6 forwarding enabled."
465 def config ( self
, ** params
):
466 super ( LinuxRouter
, self
). config (** params
)
467 # Enable forwarding on the router
468 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
469 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
472 Terminate generic LinuxRouter Mininet instance
474 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
475 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
476 super ( LinuxRouter
, self
). terminate ()
479 "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine"
481 def __init__ ( self
, name
, ** params
):
482 super ( Router
, self
) .__ init
__ ( name
, ** params
)
483 self
. logdir
= params
. get ( 'logdir' , get_test_logdir ( name
, True ))
484 self
. daemondir
= None
486 self
. routertype
= 'frr'
487 self
. daemons
= { 'zebra' : 0 , 'ripd' : 0 , 'ripngd' : 0 , 'ospfd' : 0 ,
488 'ospf6d' : 0 , 'isisd' : 0 , 'bgpd' : 0 , 'pimd' : 0 ,
489 'ldpd' : 0 , 'eigrpd' : 0 , 'nhrpd' : 0 }
490 self
. daemons_options
= { 'zebra' : '' }
491 self
. reportCores
= True
493 def _config_frr ( self
, ** params
):
494 "Configure FRR binaries"
495 self
. daemondir
= params
. get ( 'frrdir' )
496 if self
. daemondir
is None :
497 self
. daemondir
= '/usr/lib/frr'
499 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
500 if not os
. path
. isfile ( zebra_path
):
501 raise Exception ( "FRR zebra binary doesn't exist at {}" . format ( zebra_path
))
503 def _config_quagga ( self
, ** params
):
504 "Configure Quagga binaries"
505 self
. daemondir
= params
. get ( 'quaggadir' )
506 if self
. daemondir
is None :
507 self
. daemondir
= '/usr/lib/quagga'
509 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
510 if not os
. path
. isfile ( zebra_path
):
511 raise Exception ( "Quagga zebra binary doesn't exist at {}" . format ( zebra_path
))
513 # pylint: disable=W0221
514 # Some params are only meaningful for the parent class.
515 def config ( self
, ** params
):
516 super ( Router
, self
). config (** params
)
518 # User did not specify the daemons directory, try to autodetect it.
519 self
. daemondir
= params
. get ( 'daemondir' )
520 if self
. daemondir
is None :
521 self
. routertype
= params
. get ( 'routertype' , 'frr' )
522 if self
. routertype
== 'quagga' :
523 self
._ config
_ quagga
(** params
)
525 self
._ config
_ frr
(** params
)
527 # Test the provided path
528 zpath
= os
. path
. join ( self
. daemondir
, 'zebra' )
529 if not os
. path
. isfile ( zpath
):
530 raise Exception ( 'No zebra binary found in {}' . format ( zpath
))
531 # Allow user to specify routertype when the path was specified.
532 if params
. get ( 'routertype' ) is not None :
533 self
. routertype
= self
. params
. get ( 'routertype' )
535 # Enable forwarding on the router
536 assert_sysctl ( self
, 'net.ipv4.ip_forward' , 1 )
537 assert_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 1 )
539 assert_sysctl ( self
, 'kernel.core_uses_pid' , 1 )
540 assert_sysctl ( self
, 'fs.suid_dumpable' , 1 )
541 #this applies to the kernel not the namespace...
542 #original on ubuntu 17.x, but apport won't save as in namespace
543 # |/usr/share/apport/apport %p %s %c %d %P
544 corefile
= ' %e _core-sig_ %s- pid_%p.dmp'
545 assert_sysctl ( self
, 'kernel.core_pattern' , corefile
)
546 self
. cmd ( 'ulimit -c unlimited' )
547 # Set ownership of config files
548 self
. cmd ( 'chown {0} : {0} vty /etc/ {0} ' . format ( self
. routertype
))
551 # Delete Running Quagga or FRR Daemons
553 # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype)
554 # for d in StringIO.StringIO(rundaemons):
555 # self.cmd('kill -7 `cat %s`' % d.rstrip())
558 set_sysctl ( self
, 'net.ipv4.ip_forward' , 0 )
559 set_sysctl ( self
, 'net.ipv6.conf.all.forwarding' , 0 )
560 super ( Router
, self
). terminate ()
561 os
. system ( 'chmod -R go+rw /tmp/topotests' )
563 def stopRouter ( self
, wait
= True ):
564 # Stop Running Quagga or FRR Daemons
565 rundaemons
= self
. cmd ( 'ls -1 /var/run/ %s /*.pid' % self
. routertype
)
566 if re
. search ( r
"No such file or directory" , rundaemons
):
568 if rundaemons
is not None :
570 for d
in StringIO
. StringIO ( rundaemons
):
571 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
572 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
573 logger
. info ( '{}: stopping {}' . format (
575 os
. path
. basename ( d
. rstrip (). rsplit ( "." , 1 )[ 0 ])
577 self
. cmd ( 'kill -TERM %s ' % daemonpid
)
579 if pid_exists ( int ( daemonpid
)):
581 if wait
and numRunning
> 0 :
582 sleep ( 2 , '{}: waiting for daemons stopping' . format ( self
. name
))
583 # 2nd round of kill if daemons didn't exit
584 for d
in StringIO
. StringIO ( rundaemons
):
585 daemonpid
= self
. cmd ( 'cat %s ' % d
. rstrip ()). rstrip ()
586 if ( daemonpid
. isdigit () and pid_exists ( int ( daemonpid
))):
587 logger
. info ( '{}: killing {}' . format (
589 os
. path
. basename ( d
. rstrip (). rsplit ( "." , 1 )[ 0 ])
591 self
. cmd ( 'kill -7 %s ' % daemonpid
)
593 self
. cmd ( 'rm -- {}' . format ( d
. rstrip ()))
595 self
. checkRouterCores ( reportOnce
= True )
598 for interface
in self
. intfNames ():
599 self
. cmd ( 'ip address flush' , interface
)
601 def checkCapability ( self
, daemon
, param
):
602 if param
is not None :
603 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
604 daemon_search_option
= param
. replace ( '-' , '' )
605 output
= self
. cmd ( ' {0} -h | grep {1} ' . format (
606 daemon_path
, daemon_search_option
))
607 if daemon_search_option
not in output
:
611 def loadConf ( self
, daemon
, source
= None , param
= None ):
612 # print "Daemons before:", self.daemons
613 if daemon
in self
. daemons
. keys ():
614 self
. daemons
[ daemon
] = 1
615 if param
is not None :
616 self
. daemons_options
[ daemon
] = param
618 self
. cmd ( 'touch /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
621 self
. cmd ( 'cp %s /etc/ %s / %s .conf' % ( source
, self
. routertype
, daemon
))
623 self
. cmd ( 'chmod 640 /etc/ %s / %s .conf' % ( self
. routertype
, daemon
))
625 self
. cmd ( 'chown %s : %s /etc/ %s / %s .conf' % ( self
. routertype
, self
. routertype
, self
. routertype
, daemon
))
628 logger
. info ( 'No daemon {} known' . format ( daemon
))
629 # print "Daemons after:", self.daemons
631 def startRouter ( self
, tgen
= None ):
632 # Disable integrated-vtysh-config
633 self
. cmd ( 'echo "no service integrated-vtysh-config" >> /etc/ %s /vtysh.conf' % self
. routertype
)
634 self
. cmd ( 'chown %s : %s vty /etc/ %s /vtysh.conf' % ( self
. routertype
, self
. routertype
, self
. routertype
))
635 # TODO remove the following lines after all tests are migrated to Topogen.
636 # Try to find relevant old logfiles in /tmp and delete them
637 map ( os
. remove
, glob
. glob ( '{}/{}/*.log' . format ( self
. logdir
, self
. name
)))
638 # Remove old core files
639 map ( os
. remove
, glob
. glob ( '{}/{}/*.dmp' . format ( self
. logdir
, self
. name
)))
640 # Remove IP addresses from OS first - we have them in zebra.conf
642 # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher
643 # No error - but return message and skip all the tests
644 if self
. daemons
[ 'ldpd' ] == 1 :
645 ldpd_path
= os
. path
. join ( self
. daemondir
, 'ldpd' )
646 if not os
. path
. isfile ( ldpd_path
):
647 logger
. info ( "LDP Test, but no ldpd compiled or installed" )
648 return "LDP Test, but no ldpd compiled or installed"
650 if version_cmp ( platform
. release (), '4.5' ) < 0 :
651 logger
. info ( "LDP Test need Linux Kernel 4.5 minimum" )
652 return "LDP Test need Linux Kernel 4.5 minimum"
655 self
. hasmpls
= tgen
. hasmpls
656 if self
. hasmpls
!= True :
657 logger
. info ( "LDP/MPLS Tests will be skipped, platform missing module(s)" )
659 # Test for MPLS Kernel modules available
661 if os
. system ( '/sbin/modprobe mpls-router' ) != 0 :
662 logger
. info ( 'MPLS tests will not run (missing mpls-router kernel module)' )
663 elif os
. system ( '/sbin/modprobe mpls-iptunnel' ) != 0 :
664 logger
. info ( 'MPLS tests will not run (missing mpls-iptunnel kernel module)' )
667 if self
. hasmpls
!= True :
668 return "LDP/MPLS Tests need mpls kernel modules"
669 self
. cmd ( 'echo 100000 > /proc/sys/net/mpls/platform_labels' )
671 if self
. daemons
[ 'eigrpd' ] == 1 :
672 eigrpd_path
= os
. path
. join ( self
. daemondir
, 'eigrpd' )
673 if not os
. path
. isfile ( eigrpd_path
):
674 logger
. info ( "EIGRP Test, but no eigrpd compiled or installed" )
675 return "EIGRP Test, but no eigrpd compiled or installed"
680 def restartRouter ( self
):
681 # Starts actual daemons without init (ie restart)
682 # cd to per node directory
683 self
. cmd ( 'cd {}/{}' . format ( self
. logdir
, self
. name
))
684 self
. cmd ( 'umask 000' )
685 #Re-enable to allow for report per run
686 self
. reportCores
= True
688 if self
. daemons
[ 'zebra' ] == 1 :
689 zebra_path
= os
. path
. join ( self
. daemondir
, 'zebra' )
690 zebra_option
= self
. daemons_options
[ 'zebra' ]
691 self
. cmd ( ' {0} {1} > zebra.out 2> zebra.err &' . format (
692 zebra_path
, zebra_option
, self
. logdir
, self
. name
695 logger
. debug ( '{}: {} zebra started' . format ( self
, self
. routertype
))
696 sleep ( 1 , '{}: waiting for zebra to start' . format ( self
. name
))
697 # Fix Link-Local Addresses
698 # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this
699 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' )
700 # Now start all the other daemons
701 for daemon
in self
. daemons
:
702 # Skip disabled daemons and zebra
703 if self
. daemons
[ daemon
] == 0 or daemon
== 'zebra' :
706 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
707 self
. cmd ( ' {0} > {3} .out 2> {3} .err &' . format (
708 daemon_path
, self
. logdir
, self
. name
, daemon
711 logger
. debug ( '{}: {} {} started' . format ( self
, self
. routertype
, daemon
))
712 def getStdErr ( self
, daemon
):
713 return self
. getLog ( 'err' , daemon
)
714 def getStdOut ( self
, daemon
):
715 return self
. getLog ( 'out' , daemon
)
716 def getLog ( self
, log
, daemon
):
717 return self
. cmd ( 'cat {}/{}/{}.{}' . format ( self
. logdir
, self
. name
, daemon
, log
))
719 def checkRouterCores ( self
, reportLeaks
= True , reportOnce
= False ):
720 if reportOnce
and not self
. reportCores
:
723 for daemon
in self
. daemons
:
724 if ( self
. daemons
[ daemon
] == 1 ):
726 corefiles
= glob
. glob ( '{}/{}/{}_core*.dmp' . format (
727 self
. logdir
, self
. name
, daemon
))
728 if ( len ( corefiles
) > 0 ):
729 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
730 backtrace
= subprocess
. check_output ([
731 "gdb {} {} --batch -ex bt 2> /dev/null" . format ( daemon_path
, corefiles
[ 0 ])
733 sys
. stderr
. write ( " \n %s : %s crashed. Core file found - Backtrace follows: \n " % ( self
. name
, daemon
))
734 sys
. stderr
. write ( " %s " % backtrace
)
737 log
= self
. getStdErr ( daemon
)
738 if "memstats" in log
:
739 sys
. stderr
. write ( " %s : %s has memory leaks: \n " % ( self
. name
, daemon
))
740 log
= re
. sub ( "core_handler: " , "" , log
)
741 log
= re
. sub ( r
"(showing active allocations in memory group [a-zA-Z0-9]+)" , r
"\n ## \1" , log
)
742 log
= re
. sub ( "memstats: " , " " , log
)
743 sys
. stderr
. write ( log
)
745 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
746 if checkAddressSanitizerError ( self
. getStdErr ( daemon
), self
. name
, daemon
):
747 sys
. stderr
. write ( " %s : Daemon %s killed by AddressSanitizer" % ( self
. name
, daemon
))
750 self
. reportCores
= False
752 def checkRouterRunning ( self
):
753 "Check if router daemons are running and collect crashinfo they don't run"
757 daemonsRunning
= self
. cmd ( 'vtysh -c "show log" | grep "Logging configuration for"' )
758 # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found
759 if checkAddressSanitizerError ( daemonsRunning
, self
. name
, "vtysh" ):
760 return " %s : vtysh killed by AddressSanitizer" % ( self
. name
)
762 for daemon
in self
. daemons
:
763 if ( self
. daemons
[ daemon
] == 1 ) and not ( daemon
in daemonsRunning
):
764 sys
. stderr
. write ( " %s : Daemon %s not running \n " % ( self
. name
, daemon
))
766 corefiles
= glob
. glob ( '{}/{}/{}_core*.dmp' . format (
767 self
. logdir
, self
. name
, daemon
))
768 if ( len ( corefiles
) > 0 ):
769 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
770 backtrace
= subprocess
. check_output ([
771 "gdb {} {} --batch -ex bt 2> /dev/null" . format ( daemon_path
, corefiles
[ 0 ])
773 sys
. stderr
. write ( " \n %s : %s crashed. Core file found - Backtrace follows: \n " % ( self
. name
, daemon
))
774 sys
. stderr
. write ( " %s \n " % backtrace
)
776 # No core found - If we find matching logfile in /tmp, then print last 20 lines from it.
777 if os
. path
. isfile ( '{}/{}/{}.log' . format ( self
. logdir
, self
. name
, daemon
)):
778 log_tail
= subprocess
. check_output ([
779 "tail -n20 {}/{}/{}.log 2> /dev/null" . format (
780 self
. logdir
, self
. name
, daemon
)
782 sys
. stderr
. write ( " \n From %s %s %s log file: \n " % ( self
. routertype
, self
. name
, daemon
))
783 sys
. stderr
. write ( " %s \n " % log_tail
)
785 # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found
786 if checkAddressSanitizerError ( self
. getStdErr ( daemon
), self
. name
, daemon
):
787 return " %s : Daemon %s not running - killed by AddressSanitizer" % ( self
. name
, daemon
)
789 return " %s : Daemon %s not running" % ( self
. name
, daemon
)
791 def get_ipv6_linklocal ( self
):
792 "Get LinkLocal Addresses from interfaces"
796 ifaces
= self
. cmd ( 'ip -6 address' )
797 # Fix newlines (make them all the same)
798 ifaces
= ( ' \n ' . join ( ifaces
. splitlines ()) + ' \n ' ). splitlines ()
802 m
= re
. search ( '[0-9]+: ([^:@]+)[@if0-9:]+ <' , line
)
804 interface
= m
. group ( 1 )
806 m
= re
. search ( 'inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link' , line
)
810 if ( ll_per_if_count
> 1 ):
811 linklocal
+= [[ " %s-%s " % ( interface
, ll_per_if_count
), local
]]
813 linklocal
+= [[ interface
, local
]]
815 def daemon_available ( self
, daemon
):
816 "Check if specified daemon is installed (and for ldp if kernel supports MPLS)"
818 daemon_path
= os
. path
. join ( self
. daemondir
, daemon
)
819 if not os
. path
. isfile ( daemon_path
):
821 if ( daemon
== 'ldpd' ):
822 if version_cmp ( platform
. release (), '4.5' ) < 0 :
824 if self
. cmd ( '/sbin/modprobe -n mpls-router' ) != "" :
826 if self
. cmd ( '/sbin/modprobe -n mpls-iptunnel' ) != "" :
830 def get_routertype ( self
):
831 "Return the type of Router (frr or quagga)"
833 return self
. routertype
834 def report_memory_leaks ( self
, filename_prefix
, testscript
):
835 "Report Memory Leaks to file prefixed with given string"
838 filename
= filename_prefix
+ re
. sub ( r
"\.py" , "" , testscript
) + ".txt"
839 for daemon
in self
. daemons
:
840 if ( self
. daemons
[ daemon
] == 1 ):
841 log
= self
. getStdErr ( daemon
)
842 if "memstats" in log
:
844 logger
. info ( ' \n Router {} {} StdErr Log: \n {}' . format (
845 self
. name
, daemon
, log
))
848 # Check if file already exists
849 fileexists
= os
. path
. isfile ( filename
)
850 leakfile
= open ( filename
, "a" )
852 # New file - add header
853 leakfile
. write ( "# Memory Leak Detection for topotest %s \n\n " % testscript
)
854 leakfile
. write ( "## Router %s \n " % self
. name
)
855 leakfile
. write ( "### Process %s \n " % daemon
)
856 log
= re
. sub ( "core_handler: " , "" , log
)
857 log
= re
. sub ( r
"(showing active allocations in memory group [a-zA-Z0-9]+)" , r
"\n#### \1\n" , log
)
858 log
= re
. sub ( "memstats: " , " " , log
)
865 class LegacySwitch ( OVSSwitch
):
866 "A Legacy Switch without OpenFlow"
868 def __init__ ( self
, name
, ** params
):
869 OVSSwitch
.__ init
__ ( self
, name
, failMode
= 'standalone' , ** params
)