blob: 6a3dc14561ac5a201584bda3bfdc7d432ec82746 [file] [log] [blame]
"Utility functions for Mininet."
from mininet.log import output, info, error, warn
from time import sleep
from resource import setrlimit, RLIMIT_NPROC, RLIMIT_NOFILE
from select import poll, POLLIN
from subprocess import call, check_call, Popen, PIPE, STDOUT
import re
from fcntl import fcntl, F_GETFL, F_SETFL
from os import O_NONBLOCK
import os
# Command execution support
def run( cmd ):
"""Simple interface to subprocess.call()
cmd: list of command params"""
return call( cmd.split( ' ' ) )
def checkRun( cmd ):
"""Simple interface to subprocess.check_call()
cmd: list of command params"""
return check_call( cmd.split( ' ' ) )
# pylint doesn't understand explicit type checking
# pylint: disable-msg=E1103
def oldQuietRun( *cmd ):
"""Run a command, routing stderr to stdout, and return the output.
cmd: list of command params"""
if len( cmd ) == 1:
cmd = cmd[ 0 ]
if isinstance( cmd, str ):
cmd = cmd.split( ' ' )
popen = Popen( cmd, stdout=PIPE, stderr=STDOUT )
# We can't use Popen.communicate() because it uses
# select(), which can't handle
# high file descriptor numbers! poll() can, however.
out = ''
readable = poll()
readable.register( popen.stdout )
while True:
while readable.poll():
data = popen.stdout.read( 1024 )
if len( data ) == 0:
break
out += data
popen.poll()
if popen.returncode is not None:
break
return out
# This is a bit complicated, but it enables us to
# monitor command output as it is happening
def errRun( *cmd, **kwargs ):
"""Run a command and return stdout, stderr and return code
cmd: string or list of command and args
stderr: STDOUT to merge stderr with stdout
shell: run command using shell
echo: monitor output to console"""
# Allow passing in a list or a string
if len( cmd ) == 1:
cmd = cmd[ 0 ]
if isinstance( cmd, str ):
cmd = cmd.split( ' ' )
cmd = [ str( arg ) for arg in cmd ]
# By default we separate stderr, don't run in a shell, and don't echo
stderr = kwargs.get( 'stderr', PIPE )
shell = kwargs.get( 'shell', False )
echo = kwargs.get( 'echo', False )
if echo:
# cmd goes to stderr, output goes to stdout
info( cmd, '\n' )
popen = Popen( cmd, stdout=PIPE, stderr=stderr, shell=shell )
# We use poll() because select() doesn't work with large fd numbers,
# and thus communicate() doesn't work either
out, err = '', ''
poller = poll()
poller.register( popen.stdout, POLLIN )
fdtofile = { popen.stdout.fileno(): popen.stdout }
outDone, errDone = False, True
if popen.stderr:
fdtofile[ popen.stderr.fileno() ] = popen.stderr
poller.register( popen.stderr, POLLIN )
errDone = False
while not outDone or not errDone:
readable = poller.poll()
for fd, _event in readable:
f = fdtofile[ fd ]
data = f.read( 1024 )
if echo:
output( data )
if f == popen.stdout:
out += data
if data == '':
outDone = True
elif f == popen.stderr:
err += data
if data == '':
errDone = True
returncode = popen.wait()
return out, err, returncode
def errFail( *cmd, **kwargs ):
"Run a command using errRun and raise exception on nonzero exit"
out, err, ret = errRun( *cmd, **kwargs )
if ret:
raise Exception( "errFail: %s failed with return code %s: %s"
% ( cmd, ret, err ) )
return out, err, ret
def quietRun( cmd, **kwargs ):
"Run a command and return merged stdout and stderr"
return errRun( cmd, stderr=STDOUT, **kwargs )[ 0 ]
# pylint: enable-msg=E1103
# pylint: disable-msg=E1101
def isShellBuiltin( cmd ):
"Return True if cmd is a bash builtin."
if isShellBuiltin.builtIns is None:
isShellBuiltin.builtIns = quietRun( 'bash -c enable' )
space = cmd.find( ' ' )
if space > 0:
cmd = cmd[ :space]
return cmd in isShellBuiltin.builtIns
isShellBuiltin.builtIns = None
# pylint: enable-msg=E1101
# Interface management
#
# Interfaces are managed as strings which are simply the
# interface names, of the form 'nodeN-ethM'.
#
# To connect nodes, we create a pair of veth interfaces, and then place them
# in the pair of nodes that we want to communicate. We then update the node's
# list of interfaces and connectivity map.
#
# For the kernel datapath, switch interfaces
# live in the root namespace and thus do not have to be
# explicitly moved.
def makeIntfPair( intf1, intf2 ):
"""Make a veth pair connecting intf1 and intf2.
intf1: string, interface
intf2: string, interface
returns: success boolean"""
# Delete any old interfaces with the same names
quietRun( 'ip link del ' + intf1 )
quietRun( 'ip link del ' + intf2 )
# Create new pair
cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2
return quietRun( cmd )
def retry( retries, delaySecs, fn, *args, **keywords ):
"""Try something several times before giving up.
n: number of times to retry
delaySecs: wait this long between tries
fn: function to call
args: args to apply to function call"""
tries = 0
while not fn( *args, **keywords ) and tries < retries:
sleep( delaySecs )
tries += 1
if tries >= retries:
error( "*** gave up after %i retries\n" % tries )
exit( 1 )
def moveIntfNoRetry( intf, node, printError=False ):
"""Move interface to node, without retrying.
intf: string, interface
node: Node object
printError: if true, print error"""
cmd = 'ip link set ' + intf + ' netns ' + repr( node.pid )
quietRun( cmd )
links = node.cmd( 'ip link show' )
if not ( ' %s:' % intf ) in links:
if printError:
error( '*** Error: moveIntf: ' + intf +
' not successfully moved to ' + node.name + '\n' )
return False
return True
def moveIntf( intf, node, printError=False, retries=3, delaySecs=0.001 ):
"""Move interface to node, retrying on failure.
intf: string, interface
node: Node object
printError: if true, print error"""
retry( retries, delaySecs, moveIntfNoRetry, intf, node, printError )
# Support for dumping network
def dumpNodeConnections( nodes ):
"Dump connections to/from nodes."
def dumpConnections( node ):
"Helper function: dump connections to node"
for intf in node.intfList():
output( ' %s:' % intf )
if intf.link:
intfs = [ intf.link.intf1, intf.link.intf2 ]
intfs.remove( intf )
output( intfs[ 0 ] )
else:
output( ' ' )
for node in nodes:
output( node.name )
dumpConnections( node )
output( '\n' )
def dumpNetConnections( net ):
"Dump connections in network"
nodes = net.controllers + net.switches + net.hosts
dumpNodeConnections( nodes )
# IP and Mac address formatting and parsing
def _colonHex( val, bytecount ):
"""Generate colon-hex string.
val: input as unsigned int
bytecount: number of bytes to convert
returns: chStr colon-hex string"""
pieces = []
for i in range( bytecount - 1, -1, -1 ):
piece = ( ( 0xff << ( i * 8 ) ) & val ) >> ( i * 8 )
pieces.append( '%02x' % piece )
chStr = ':'.join( pieces )
return chStr
def macColonHex( mac ):
"""Generate MAC colon-hex string from unsigned int.
mac: MAC address as unsigned int
returns: macStr MAC colon-hex string"""
return _colonHex( mac, 6 )
def ipStr( ip ):
"""Generate IP address string from an unsigned int.
ip: unsigned int of form w << 24 | x << 16 | y << 8 | z
returns: ip address string w.x.y.z, or 10.x.y.z if w==0"""
w = ( ip >> 24 ) & 0xff
w = 10 if w == 0 else w
x = ( ip >> 16 ) & 0xff
y = ( ip >> 8 ) & 0xff
z = ip & 0xff
return "%i.%i.%i.%i" % ( w, x, y, z )
def ipNum( w, x, y, z ):
"""Generate unsigned int from components of IP address
returns: w << 24 | x << 16 | y << 8 | z"""
return ( w << 24 ) | ( x << 16 ) | ( y << 8 ) | z
def nextCCNnet(curCCNnet):
netNum = ipParse(curCCNnet)
return ipStr(netNum+4)
def ipAdd( i, prefixLen=8, ipBaseNum=0x0a000000 ):
"""Return IP address string from ints
i: int to be added to ipbase
prefixLen: optional IP prefix length
ipBaseNum: option base IP address as int
returns IP address as string"""
# Ugly but functional
assert i < ( 1 << ( 32 - prefixLen ) )
mask = 0xffffffff ^ ( ( 1 << prefixLen ) - 1 )
ipnum = i + ( ipBaseNum & mask )
return ipStr( ipnum )
def ipParse( ip ):
"Parse an IP address and return an unsigned int."
args = [ int( arg ) for arg in ip.split( '.' ) ]
return ipNum( *args )
def netParse( ipstr ):
"""Parse an IP network specification, returning
address and prefix len as unsigned ints"""
prefixLen = 0
if '/' in ipstr:
ip, pf = ipstr.split( '/' )
prefixLen = int( pf )
return ipParse( ip ), prefixLen
def checkInt( s ):
"Check if input string is an int"
try:
int( s )
return True
except ValueError:
return False
def checkFloat( s ):
"Check if input string is a float"
try:
float( s )
return True
except ValueError:
return False
def makeNumeric( s ):
"Convert string to int or float if numeric."
if checkInt( s ):
return int( s )
elif checkFloat( s ):
return float( s )
else:
return s
# Popen support
def pmonitor(popens, timeoutms=500, readline=True,
readmax=1024 ):
"""Monitor dict of hosts to popen objects
a line at a time
timeoutms: timeout for poll()
readline: return single line of output
yields: host, line/output (if any)
terminates: when all EOFs received"""
poller = poll()
fdToHost = {}
for host, popen in popens.iteritems():
fd = popen.stdout.fileno()
fdToHost[ fd ] = host
poller.register( fd, POLLIN )
if not readline:
# Use non-blocking reads
flags = fcntl( fd, F_GETFL )
fcntl( fd, F_SETFL, flags | O_NONBLOCK )
while True:
fds = poller.poll( timeoutms )
if fds:
for fd, _event in fds:
host = fdToHost[ fd ]
popen = popens[ host ]
if readline:
# Attempt to read a line of output
# This blocks until we receive a newline!
line = popen.stdout.readline()
else:
line = popen.stdout.read( readmax )
yield host, line
# Check for EOF
if not line:
popen.poll()
if popen.returncode is not None:
poller.unregister( fd )
del popens[ host ]
if not popens:
return
else:
yield None, ''
# Other stuff we use
def fixLimits():
"Fix ridiculously small resource limits."
setrlimit( RLIMIT_NPROC, ( 8192, 8192 ) )
setrlimit( RLIMIT_NOFILE, ( 16384, 16384 ) )
def mountCgroups():
"Make sure cgroups file system is mounted"
mounts = quietRun( 'mount' )
cgdir = '/sys/fs/cgroup'
csdir = cgdir + '/cpuset'
if ('cgroup on %s' % cgdir not in mounts and
'cgroups on %s' % cgdir not in mounts):
raise Exception( "cgroups not mounted on " + cgdir )
if 'cpuset on %s' % csdir not in mounts:
errRun( 'mkdir -p ' + csdir )
errRun( 'mount -t cgroup -ocpuset cpuset ' + csdir )
def natural( text ):
"To sort sanely/alphabetically: sorted( l, key=natural )"
def num( s ):
"Convert text segment to int if necessary"
return int( s ) if s.isdigit() else s
return [ num( s ) for s in re.split( r'(\d+)', text ) ]
def naturalSeq( t ):
"Natural sort key function for sequences"
return [ natural( x ) for x in t ]
def numCores():
"Returns number of CPU cores based on /proc/cpuinfo"
if hasattr( numCores, 'ncores' ):
return numCores.ncores
try:
numCores.ncores = int( quietRun('grep -c processor /proc/cpuinfo') )
except ValueError:
return 0
return numCores.ncores
def irange(start, end):
"""Inclusive range from start to end (vs. Python insanity.)
irange(1,5) -> 1, 2, 3, 4, 5"""
return range( start, end + 1 )
def custom( cls, **params ):
"Returns customized constructor for class cls."
# Note: we may wish to see if we can use functools.partial() here
# and in customConstructor
def customized( *args, **kwargs):
"Customized constructor"
kwargs = kwargs.copy()
kwargs.update( params )
return cls( *args, **kwargs )
customized.__name__ = 'custom(%s,%s)' % ( cls, params )
return customized
def splitArgs( argstr ):
"""Split argument string into usable python arguments
argstr: argument string with format fn,arg2,kw1=arg3...
returns: fn, args, kwargs"""
split = argstr.split( ',' )
fn = split[ 0 ]
params = split[ 1: ]
# Convert int and float args; removes the need for function
# to be flexible with input arg formats.
args = [ makeNumeric( s ) for s in params if '=' not in s ]
kwargs = {}
for s in [ p for p in params if '=' in p ]:
key, val = s.split( '=' )
kwargs[ key ] = makeNumeric( val )
return fn, args, kwargs
def customConstructor( constructors, argStr ):
"""Return custom constructor based on argStr
The args and key/val pairs in argsStr will be automatically applied
when the generated constructor is later used.
"""
cname, newargs, kwargs = splitArgs( argStr )
constructor = constructors.get( cname, None )
if not constructor:
raise Exception( "error: %s is unknown - please specify one of %s" %
( cname, constructors.keys() ) )
def customized( name, *args, **params ):
"Customized constructor, useful for Node, Link, and other classes"
params = params.copy()
params.update( kwargs )
if not newargs:
return constructor( name, *args, **params )
if args:
warn( 'warning: %s replacing %s with %s\n' % (
constructor, args, newargs ) )
return constructor( name, *newargs, **params )
customized.__name__ = 'customConstructor(%s)' % argStr
return customized
def buildTopo( topos, topoStr ):
"""Create topology from string with format (object, arg1, arg2,...).
input topos is a dict of topo names to constructors, possibly w/args.
"""
topo, args, kwargs = splitArgs( topoStr )
if topo not in topos:
raise Exception( 'Invalid topo name %s' % topo )
return topos[ topo ]( *args, **kwargs )
def ensureRoot():
"""Ensure that we are running as root.
Probably we should only sudo when needed as per Big Switch's patch.
"""
if os.getuid() != 0:
print "*** Mininet must run as root."
exit( 1 )
return