Statistics
| Branch: | Tag: | Revision:

mininet / mininet / util.py @ edf60032

History | View | Annotate | Download (14.8 KB)

1
"Utility functions for Mininet."
2

    
3
from mininet.log import output, info, error, warn
4

    
5
from time import sleep
6
from resource import setrlimit, RLIMIT_NPROC, RLIMIT_NOFILE
7
from select import poll, POLLIN
8
from subprocess import call, check_call, Popen, PIPE, STDOUT
9
import re
10
from fcntl import fcntl, F_GETFL, F_SETFL
11
from os import O_NONBLOCK
12

    
13
# Command execution support
14

    
15
def run( cmd ):
16
    """Simple interface to subprocess.call()
17
       cmd: list of command params"""
18
    return call( cmd.split( ' ' ) )
19

    
20
def checkRun( cmd ):
21
    """Simple interface to subprocess.check_call()
22
       cmd: list of command params"""
23
    return check_call( cmd.split( ' ' ) )
24

    
25
# pylint doesn't understand explicit type checking
26
# pylint: disable-msg=E1103
27

    
28
def oldQuietRun( *cmd ):
29
    """Run a command, routing stderr to stdout, and return the output.
30
       cmd: list of command params"""
31
    if len( cmd ) == 1:
32
        cmd = cmd[ 0 ]
33
        if isinstance( cmd, str ):
34
            cmd = cmd.split( ' ' )
35
    popen = Popen( cmd, stdout=PIPE, stderr=STDOUT )
36
    # We can't use Popen.communicate() because it uses
37
    # select(), which can't handle
38
    # high file descriptor numbers! poll() can, however.
39
    out = ''
40
    readable = poll()
41
    readable.register( popen.stdout )
42
    while True:
43
        while readable.poll():
44
            data = popen.stdout.read( 1024 )
45
            if len( data ) == 0:
46
                break
47
            out += data
48
        popen.poll()
49
        if popen.returncode is not None:
50
            break
51
    return out
52

    
53

    
54
# This is a bit complicated, but it enables us to
55
# monitor command output as it is happening
56

    
57
def errRun( *cmd, **kwargs ):
58
    """Run a command and return stdout, stderr and return code
59
       cmd: string or list of command and args
60
       stderr: STDOUT to merge stderr with stdout
61
       shell: run command using shell
62
       echo: monitor output to console"""
63
    # Allow passing in a list or a string
64
    if len( cmd ) == 1:
65
        cmd = cmd[ 0 ]
66
        if isinstance( cmd, str ):
67
            cmd = cmd.split( ' ' )
68
    cmd = [ str( arg ) for arg in cmd ]
69
    # By default we separate stderr, don't run in a shell, and don't echo
70
    stderr = kwargs.get( 'stderr', PIPE )
71
    shell = kwargs.get( 'shell', False )
72
    echo = kwargs.get( 'echo', False )
73
    if echo:
74
        # cmd goes to stderr, output goes to stdout
75
        info( cmd, '\n' )
76
    popen = Popen( cmd, stdout=PIPE, stderr=stderr, shell=shell )
77
    # We use poll() because select() doesn't work with large fd numbers,
78
    # and thus communicate() doesn't work either
79
    out, err = '', ''
80
    poller = poll()
81
    poller.register( popen.stdout, POLLIN )
82
    fdtofile = { popen.stdout.fileno(): popen.stdout }
83
    outDone, errDone = False, True
84
    if popen.stderr:
85
        fdtofile[ popen.stderr.fileno() ] = popen.stderr
86
        poller.register( popen.stderr, POLLIN )
87
        errDone = False
88
    while not outDone or not errDone:
89
        readable = poller.poll()
90
        for fd, _event in readable:
91
            f = fdtofile[ fd ]
92
            data = f.read( 1024 )
93
            if echo:
94
                output( data )
95
            if f == popen.stdout:
96
                out += data
97
                if data == '':
98
                    outDone = True
99
            elif f == popen.stderr:
100
                err += data
101
                if data == '':
102
                    errDone = True
103
    returncode = popen.wait()
104
    return out, err, returncode
105

    
106
def errFail( *cmd, **kwargs ):
107
    "Run a command using errRun and raise exception on nonzero exit"
108
    out, err, ret = errRun( *cmd, **kwargs )
109
    if ret:
110
        raise Exception( "errFail: %s failed with return code %s: %s"
111
                         % ( cmd, ret, err ) )
112
    return out, err, ret
113

    
114
def quietRun( cmd, **kwargs ):
115
    "Run a command and return merged stdout and stderr"
116
    return errRun( cmd, stderr=STDOUT, **kwargs )[ 0 ]
117

    
118
# pylint: enable-msg=E1103
119
# pylint: disable-msg=E1101
120

    
121
def isShellBuiltin( cmd ):
122
    "Return True if cmd is a bash builtin."
123
    if isShellBuiltin.builtIns is None:
124
        isShellBuiltin.builtIns = quietRun( 'bash -c enable' )
125
    space = cmd.find( ' ' )
126
    if space > 0:
127
        cmd = cmd[ :space]
128
    return cmd in isShellBuiltin.builtIns
129

    
130
isShellBuiltin.builtIns = None
131

    
132
# pylint: enable-msg=E1101
133

    
134
# Interface management
135
#
136
# Interfaces are managed as strings which are simply the
137
# interface names, of the form 'nodeN-ethM'.
138
#
139
# To connect nodes, we create a pair of veth interfaces, and then place them
140
# in the pair of nodes that we want to communicate. We then update the node's
141
# list of interfaces and connectivity map.
142
#
143
# For the kernel datapath, switch interfaces
144
# live in the root namespace and thus do not have to be
145
# explicitly moved.
146

    
147
def makeIntfPair( intf1, intf2 ):
148
    """Make a veth pair connecting intf1 and intf2.
149
       intf1: string, interface
150
       intf2: string, interface
151
       returns: success boolean"""
152
    # Delete any old interfaces with the same names
153
    quietRun( 'ip link del ' + intf1 )
154
    quietRun( 'ip link del ' + intf2 )
155
    # Create new pair
156
    cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2
157
    return quietRun( cmd )
158

    
159
def retry( retries, delaySecs, fn, *args, **keywords ):
160
    """Try something several times before giving up.
161
       n: number of times to retry
162
       delaySecs: wait this long between tries
163
       fn: function to call
164
       args: args to apply to function call"""
165
    tries = 0
166
    while not fn( *args, **keywords ) and tries < retries:
167
        sleep( delaySecs )
168
        tries += 1
169
    if tries >= retries:
170
        error( "*** gave up after %i retries\n" % tries )
171
        exit( 1 )
172

    
173
def moveIntfNoRetry( intf, node, printError=False ):
174
    """Move interface to node, without retrying.
175
       intf: string, interface
176
       node: Node object
177
       printError: if true, print error"""
178
    cmd = 'ip link set ' + intf + ' netns ' + repr( node.pid )
179
    quietRun( cmd )
180
    links = node.cmd( 'ip link show' )
181
    if not ( ' %s:' % intf ) in links:
182
        if printError:
183
            error( '*** Error: moveIntf: ' + intf +
184
                   ' not successfully moved to ' + node.name + '\n' )
185
        return False
186
    return True
187

    
188
def moveIntf( intf, node, printError=False, retries=3, delaySecs=0.001 ):
189
    """Move interface to node, retrying on failure.
190
       intf: string, interface
191
       node: Node object
192
       printError: if true, print error"""
193
    retry( retries, delaySecs, moveIntfNoRetry, intf, node, printError )
194

    
195
# Support for dumping network
196

    
197
def dumpNodeConnections( nodes ):
198
    "Dump connections to/from nodes."
199

    
200
    def dumpConnections( node ):
201
        "Helper function: dump connections to node"
202
        for intf in node.intfList():
203
            output( ' %s:' % intf )
204
            if intf.link:
205
                intfs = [ intf.link.intf1, intf.link.intf2 ]
206
                intfs.remove( intf )
207
                output( intfs[ 0 ] )
208
            else:
209
                output( ' ' )
210

    
211
    for node in nodes:
212
        output( node.name )
213
        dumpConnections( node )
214
        output( '\n' )
215

    
216
def dumpNetConnections( net ):
217
    "Dump connections in network"
218
    nodes = net.controllers + net.switches + net.hosts
219
    dumpNodeConnections( nodes )
220

    
221
# IP and Mac address formatting and parsing
222

    
223
def _colonHex( val, bytecount ):
224
    """Generate colon-hex string.
225
       val: input as unsigned int
226
       bytescount: number of bytes to convert
227
       returns: chStr colon-hex string"""
228
    pieces = []
229
    for i in range( bytecount - 1, -1, -1 ):
230
        piece = ( ( 0xff << ( i * 8 ) ) & val ) >> ( i * 8 )
231
        pieces.append( '%02x' % piece )
232
    chStr = ':'.join( pieces )
233
    return chStr
234

    
235
def macColonHex( mac ):
236
    """Generate MAC colon-hex string from unsigned int.
237
       mac: MAC address as unsigned int
238
       returns: macStr MAC colon-hex string"""
239
    return _colonHex( mac, 6 )
240

    
241
def ipStr( ip ):
242
    """Generate IP address string from an unsigned int.
243
       ip: unsigned int of form w << 24 | x << 16 | y << 8 | z
244
       returns: ip address string w.x.y.z, or 10.x.y.z if w==0"""
245
    w = ( ip >> 24 ) & 0xff
246
    w = 10 if w == 0 else w
247
    x = ( ip >> 16 ) & 0xff
248
    y = ( ip >> 8 ) & 0xff
249
    z = ip & 0xff
250
    return "%i.%i.%i.%i" % ( w, x, y, z )
251

    
252
def ipNum( w, x, y, z ):
253
    """Generate unsigned int from components of IP address
254
       returns: w << 24 | x << 16 | y << 8 | z"""
255
    return ( w << 24 ) | ( x << 16 ) | ( y << 8 ) | z
256

    
257
def ipAdd( i, prefixLen=8, ipBaseNum=0x0a000000 ):
258
    """Return IP address string from ints
259
       i: int to be added to ipbase
260
       prefixLen: optional IP prefix length
261
       ipBaseNum: option base IP address as int
262
       returns IP address as string"""
263
    # Ugly but functional
264
    assert i < ( 1 << ( 32 - prefixLen ) )
265
    mask = 0xffffffff ^ ( ( 1 << prefixLen ) - 1 )
266
    ipnum = i + ( ipBaseNum & mask )
267
    return ipStr( ipnum )
268

    
269
def ipParse( ip ):
270
    "Parse an IP address and return an unsigned int."
271
    args = [ int( arg ) for arg in ip.split( '.' ) ]
272
    return ipNum( *args )
273

    
274
def netParse( ipstr ):
275
    """Parse an IP network specification, returning
276
       address and prefix len as unsigned ints"""
277
    prefixLen = 0
278
    if '/' in ipstr:
279
        ip, pf = ipstr.split( '/' )
280
        prefixLen = int( pf )
281
    return ipParse( ip ), prefixLen
282

    
283
def checkInt( s ):
284
    "Check if input string is an int"
285
    try:
286
        int( s )
287
        return True
288
    except ValueError:
289
        return False
290

    
291
def checkFloat( s ):
292
    "Check if input string is a float"
293
    try:
294
        float( s )
295
        return True
296
    except ValueError:
297
        return False
298

    
299
def makeNumeric( s ):
300
    "Convert string to int or float if numeric."
301
    if checkInt( s ):
302
        return int( s )
303
    elif checkFloat( s ):
304
        return float( s )
305
    else:
306
        return s
307

    
308
# Popen support
309

    
310
def pmonitor(popens, timeoutms=500, readline=True,
311
             readmax=1024 ):
312
    """Monitor dict of hosts to popen objects
313
       a line at a time
314
       timeoutms: timeout for poll()
315
       readline: return single line of output
316
       yields: host, line/output (if any)
317
       terminates: when all EOFs received"""
318
    poller = poll()
319
    fdToHost = {}
320
    for host, popen in popens.iteritems():
321
        fd = popen.stdout.fileno()
322
        fdToHost[ fd ] = host
323
        poller.register( fd, POLLIN )
324
        if not readline:
325
            # Use non-blocking reads
326
            flags = fcntl( fd, F_GETFL )
327
            fcntl( fd, F_SETFL, flags | O_NONBLOCK )
328
    while True:
329
        fds = poller.poll( timeoutms )
330
        if fds:
331
            for fd, _event in fds:
332
                host = fdToHost[ fd ]
333
                popen = popens[ host ]
334
                if readline:
335
                    # Attempt to read a line of output
336
                    # This blocks until we receive a newline!
337
                    line = popen.stdout.readline()
338
                else:
339
                    line = popen.stdout.read( readmax )
340
                yield host, line
341
                # Check for EOF
342
                if not line:
343
                    popen.poll()
344
                    if popen.returncode is not None:
345
                        poller.unregister( fd )
346
                        del popens[ host ]
347
                        if not popens:
348
                            return
349
        else:
350
            yield None, ''
351

    
352
# Other stuff we use
353

    
354
def fixLimits():
355
    "Fix ridiculously small resource limits."
356
    setrlimit( RLIMIT_NPROC, ( 8192, 8192 ) )
357
    setrlimit( RLIMIT_NOFILE, ( 16384, 16384 ) )
358

    
359
def mountCgroups():
360
    "Make sure cgroups file system is mounted"
361
    mounts = quietRun( 'mount' )
362
    cgdir = '/sys/fs/cgroup'
363
    csdir = cgdir + '/cpuset'
364
    if 'cgroups on %s' % cgdir not in mounts:
365
        raise Exception( "cgroups not mounted on " + cgdir )
366
    if 'cpuset on %s' % csdir not in mounts:
367
        errRun( 'mkdir -p ' + csdir )
368
        errRun( 'mount -t cgroup -ocpuset cpuset ' + csdir )
369

    
370
def natural( text ):
371
    "To sort sanely/alphabetically: sorted( l, key=natural )"
372
    def num( s ):
373
        "Convert text segment to int if necessary"
374
        return int( s ) if s.isdigit() else s
375
    return [  num( s ) for s in re.split( r'(\d+)', text ) ]
376

    
377
def naturalSeq( t ):
378
    "Natural sort key function for sequences"
379
    return [ natural( x ) for x in t ]
380

    
381
def numCores():
382
    "Returns number of CPU cores based on /proc/cpuinfo"
383
    if hasattr( numCores, 'ncores' ):
384
        return numCores.ncores
385
    try:
386
        numCores.ncores = int( quietRun('grep -c processor /proc/cpuinfo') )
387
    except ValueError:
388
        return 0
389
    return numCores.ncores
390

    
391
def irange(start, end):
392
    """Inclusive range from start to end (vs. Python insanity.)
393
       irange(1,5) -> 1, 2, 3, 4, 5"""
394
    return range( start, end + 1 )
395

    
396
def custom( cls, **params ):
397
    "Returns customized constructor for class cls."
398
    # Note: we may wish to see if we can use functools.partial() here
399
    # and in customConstructor
400
    def customized( *args, **kwargs):
401
        "Customized constructor"
402
        kwargs = kwargs.copy()
403
        kwargs.update( params )
404
        return cls( *args, **kwargs )
405
    customized.__name__ = 'custom(%s,%s)' % ( cls, params )
406
    return customized
407

    
408
def splitArgs( argstr ):
409
    """Split argument string into usable python arguments
410
       argstr: argument string with format fn,arg2,kw1=arg3...
411
       returns: fn, args, kwargs"""
412
    split = argstr.split( ',' )
413
    fn = split[ 0 ]
414
    params = split[ 1: ]
415
    # Convert int and float args; removes the need for function
416
    # to be flexible with input arg formats.
417
    args = [ makeNumeric( s ) for s in params if '=' not in s ]
418
    kwargs = {}
419
    for s in [ p for p in params if '=' in p ]:
420
        key, val = s.split( '=' )
421
        kwargs[ key ] = makeNumeric( val )
422
    return fn, args, kwargs
423

    
424
def customConstructor( constructors, argStr ):
425
    """Return custom constructor based on argStr
426
    The args and key/val pairs in argsStr will be automatically applied
427
    when the generated constructor is later used.
428
    """
429
    cname, newargs, kwargs = splitArgs( argStr )
430
    constructor = constructors.get( cname, None )
431

    
432
    if not constructor:
433
        raise Exception( "error: %s is unknown - please specify one of %s" %
434
                         ( cname, constructors.keys() ) )
435

    
436
    def customized( name, *args, **params ):
437
        "Customized constructor, useful for Node, Link, and other classes"
438
        params = params.copy()
439
        params.update( kwargs )
440
        if not newargs:
441
            return constructor( name, *args, **params )
442
        if args:
443
            warn( 'warning: %s replacing %s with %s\n' % (
444
                  constructor, args, newargs ) )
445
        return constructor( name, *newargs, **params )
446

    
447
    customized.__name__ = 'customConstructor(%s)' % argStr
448
    return customized
449

    
450
def buildTopo( topos, topoStr ):
451
    """Create topology from string with format (object, arg1, arg2,...).
452
    input topos is a dict of topo names to constructors, possibly w/args.
453
    """
454
    topo, args, kwargs = splitArgs( topoStr )
455
    if topo not in topos:
456
        raise Exception( 'Invalid topo name %s' % topo )
457
    return topos[ topo ]( *args, **kwargs )