Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ b453e006

History | View | Annotate | Download (40.2 KB)

1
"""
2
Node objects for Mininet.
3

4
Nodes provide a simple abstraction for interacting with hosts, switches
5
and controllers. Local nodes are simply one or more processes on the local
6
machine.
7

8
Node: superclass for all (primarily local) network nodes.
9

10
Host: a virtual host. By default, a host is simply a shell; commands
11
    may be sent using Cmd (which waits for output), or using sendCmd(),
12
    which returns immediately, allowing subsequent monitoring using
13
    monitor(). Examples of how to run experiments using this
14
    functionality are provided in the examples/ directory.
15

16
CPULimitedHost: a virtual host whose CPU bandwidth is limited by
17
    RT or CFS bandwidth limiting.
18

19
Switch: superclass for switch nodes.
20

21
UserSwitch: a switch using the user-space switch from the OpenFlow
22
    reference implementation.
23

24
KernelSwitch: a switch using the kernel switch from the OpenFlow reference
25
    implementation.
26

27
OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch
28
    implementation (openvswitch.org).
29

30
Controller: superclass for OpenFlow controllers. The default controller
31
    is controller(8) from the reference implementation.
32

33
NOXController: a controller node using NOX (noxrepo.org).
34

35
RemoteController: a remote controller node, which may use any
36
    arbitrary OpenFlow-compatible controller, and which is not
37
    created or managed by mininet.
38

39
Future enhancements:
40

41
- Possibly make Node, Switch and Controller more abstract so that
42
  they can be used for both local and remote nodes
43

44
- Create proxy objects for remote nodes (Mininet: Cluster Edition)
45
"""
46

    
47
import os
48
import re
49
import signal
50
import select
51
from subprocess import Popen, PIPE, STDOUT
52

    
53
from mininet.log import info, error, warn, debug
54
from mininet.util import ( quietRun, errRun, errFail, moveIntf, isShellBuiltin,
55
                           numCores, retry, mountCgroups )
56
from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN
57
from mininet.link import Link, Intf, TCIntf
58

    
59
class Node( object ):
60
    """A virtual network node is simply a shell in a network namespace.
61
       We communicate with it using pipes."""
62

    
63
    portBase = 0  # Nodes always start with eth0/port0, even in OF 1.0
64

    
65
    def __init__( self, name, inNamespace=True, **params ):
66
        """name: name of node
67
           inNamespace: in network namespace?
68
           params: Node parameters (see config() for details)"""
69

    
70
        # Make sure class actually works
71
        self.checkSetup()
72

    
73
        self.name = name
74
        self.inNamespace = inNamespace
75

    
76
        # Stash configuration parameters for future reference
77
        self.params = params
78

    
79
        self.intfs = {}  # dict of port numbers to interfaces
80
        self.ports = {}  # dict of interfaces to port numbers
81
                         # replace with Port objects, eventually ?
82
        self.nameToIntf = {}  # dict of interface names to Intfs
83

    
84
        # Make pylint happy
85
        ( self.shell, self.execed, self.pid, self.stdin, self.stdout,
86
            self.lastPid, self.lastCmd, self.pollOut ) = (
87
                None, None, None, None, None, None, None, None )
88
        self.waiting = False
89
        self.readbuf = ''
90

    
91
        # Start command interpreter shell
92
        self.startShell()
93

    
94
    # File descriptor to node mapping support
95
    # Class variables and methods
96

    
97
    inToNode = {}  # mapping of input fds to nodes
98
    outToNode = {}  # mapping of output fds to nodes
99

    
100
    @classmethod
101
    def fdToNode( cls, fd ):
102
        """Return node corresponding to given file descriptor.
103
           fd: file descriptor
104
           returns: node"""
105
        node = cls.outToNode.get( fd )
106
        return node or cls.inToNode.get( fd )
107

    
108
    # Command support via shell process in namespace
109

    
110
    def startShell( self ):
111
        "Start a shell process for running commands"
112
        if self.shell:
113
            error( "%s: shell is already running" )
114
            return
115
        # mnexec: (c)lose descriptors, (d)etach from tty,
116
        # (p)rint pid, and run in (n)amespace
117
        opts = '-cdp'
118
        if self.inNamespace:
119
            opts += 'n'
120
        # bash -m: enable job control
121
        cmd = [ 'mnexec', opts, 'bash', '-m' ]
122
        self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
123
                            close_fds=True )
124
        self.stdin = self.shell.stdin
125
        self.stdout = self.shell.stdout
126
        self.pid = self.shell.pid
127
        self.pollOut = select.poll()
128
        self.pollOut.register( self.stdout )
129
        # Maintain mapping between file descriptors and nodes
130
        # This is useful for monitoring multiple nodes
131
        # using select.poll()
132
        self.outToNode[ self.stdout.fileno() ] = self
133
        self.inToNode[ self.stdin.fileno() ] = self
134
        self.execed = False
135
        self.lastCmd = None
136
        self.lastPid = None
137
        self.readbuf = ''
138
        self.waiting = False
139

    
140
    def cleanup( self ):
141
        "Help python collect its garbage."
142
        if not self.inNamespace:
143
            for intfName in self.intfNames():
144
                if self.name in intfName:
145
                    quietRun( 'ip link del ' + intfName )
146
        self.shell = None
147

    
148
    # Subshell I/O, commands and control
149

    
150
    def read( self, maxbytes=1024 ):
151
        """Buffered read from node, non-blocking.
152
           maxbytes: maximum number of bytes to return"""
153
        count = len( self.readbuf )
154
        if count < maxbytes:
155
            data = os.read( self.stdout.fileno(), maxbytes - count )
156
            self.readbuf += data
157
        if maxbytes >= len( self.readbuf ):
158
            result = self.readbuf
159
            self.readbuf = ''
160
        else:
161
            result = self.readbuf[ :maxbytes ]
162
            self.readbuf = self.readbuf[ maxbytes: ]
163
        return result
164

    
165
    def readline( self ):
166
        """Buffered readline from node, non-blocking.
167
           returns: line (minus newline) or None"""
168
        self.readbuf += self.read( 1024 )
169
        if '\n' not in self.readbuf:
170
            return None
171
        pos = self.readbuf.find( '\n' )
172
        line = self.readbuf[ 0: pos ]
173
        self.readbuf = self.readbuf[ pos + 1: ]
174
        return line
175

    
176
    def write( self, data ):
177
        """Write data to node.
178
           data: string"""
179
        os.write( self.stdin.fileno(), data )
180

    
181
    def terminate( self ):
182
        "Send kill signal to Node and clean up after it."
183
        os.kill( self.pid, signal.SIGKILL )
184
        self.cleanup()
185

    
186
    def stop( self ):
187
        "Stop node."
188
        self.terminate()
189

    
190
    def waitReadable( self, timeoutms=None ):
191
        """Wait until node's output is readable.
192
           timeoutms: timeout in ms or None to wait indefinitely."""
193
        if len( self.readbuf ) == 0:
194
            self.pollOut.poll( timeoutms )
195

    
196
    def sendCmd( self, *args, **kwargs ):
197
        """Send a command, followed by a command to echo a sentinel,
198
           and return without waiting for the command to complete.
199
           args: command and arguments, or string
200
           printPid: print command's PID?"""
201
        assert not self.waiting
202
        printPid = kwargs.get( 'printPid', True )
203
        # Allow sendCmd( [ list ] )
204
        if len( args ) == 1 and type( args[ 0 ] ) is list:
205
            cmd = args[ 0 ]
206
        # Allow sendCmd( cmd, arg1, arg2... )
207
        elif len( args ) > 0:
208
            cmd = args
209
        # Convert to string
210
        if not isinstance( cmd, str ):
211
            cmd = ' '.join( [ str( c ) for c in cmd ] )
212
        if not re.search( r'\w', cmd ):
213
            # Replace empty commands with something harmless
214
            cmd = 'echo -n'
215
        self.lastCmd = cmd
216
        printPid = printPid and not isShellBuiltin( cmd )
217
        if len( cmd ) > 0 and cmd[ -1 ] == '&':
218
            # print ^A{pid}\n{sentinel}
219
            cmd += ' printf "\\001%d\n\\177" $! \n'
220
        else:
221
            # print sentinel
222
            cmd += '; printf "\\177"'
223
            if printPid and not isShellBuiltin( cmd ):
224
                cmd = 'mnexec -p ' + cmd
225
        self.write( cmd + '\n' )
226
        self.lastPid = None
227
        self.waiting = True
228

    
229
    def sendInt( self, sig=signal.SIGINT ):
230
        "Interrupt running command."
231
        if self.lastPid:
232
            try:
233
                os.kill( self.lastPid, sig )
234
            except OSError:
235
                pass
236

    
237
    def monitor( self, timeoutms=None ):
238
        """Monitor and return the output of a command.
239
           Set self.waiting to False if command has completed.
240
           timeoutms: timeout in ms or None to wait indefinitely."""
241
        self.waitReadable( timeoutms )
242
        data = self.read( 1024 )
243
        # Look for PID
244
        marker = chr( 1 ) + r'\d+\n'
245
        if chr( 1 ) in data:
246
            markers = re.findall( marker, data )
247
            if markers:
248
                self.lastPid = int( markers[ 0 ][ 1: ] )
249
                data = re.sub( marker, '', data )
250
        # Look for sentinel/EOF
251
        if len( data ) > 0 and data[ -1 ] == chr( 127 ):
252
            self.waiting = False
253
            data = data[ :-1 ]
254
        elif chr( 127 ) in data:
255
            self.waiting = False
256
            data = data.replace( chr( 127 ), '' )
257
        return data
258

    
259
    def waitOutput( self, verbose=False ):
260
        """Wait for a command to complete.
261
           Completion is signaled by a sentinel character, ASCII(127)
262
           appearing in the output stream.  Wait for the sentinel and return
263
           the output, including trailing newline.
264
           verbose: print output interactively"""
265
        log = info if verbose else debug
266
        output = ''
267
        while self.waiting:
268
            data = self.monitor()
269
            output += data
270
            log( data )
271
        return output
272

    
273
    def cmd( self, *args, **kwargs ):
274
        """Send a command, wait for output, and return it.
275
           cmd: string"""
276
        verbose = kwargs.get( 'verbose', False )
277
        log = info if verbose else debug
278
        log( '*** %s : %s\n' % ( self.name, args ) )
279
        self.sendCmd( *args, **kwargs )
280
        return self.waitOutput( verbose )
281

    
282
    def cmdPrint( self, *args):
283
        """Call cmd and printing its output
284
           cmd: string"""
285
        return self.cmd( *args, **{ 'verbose': True } )
286

    
287
    def popen( self, *args, **kwargs ):
288
        """Return a Popen() object in our namespace
289
           args: Popen() args, single list, or string
290
           kwargs: Popen() keyword args"""
291
        defaults = { 'stdout': PIPE, 'stderr': PIPE,
292
                     'mncmd':
293
                     [ 'mnexec', '-a', str( self.pid ) ] }
294
        defaults.update( kwargs )
295
        if len( args ) == 1:
296
            if type( args[ 0 ] ) is list:
297
                # popen([cmd, arg1, arg2...])
298
                cmd = args[ 0 ]
299
            elif type( args[ 0 ] ) is str:
300
                # popen("cmd arg1 arg2...")
301
                cmd = args[ 0 ].split()
302
            else:
303
                raise Exception( 'popen() requires a string or list' )
304
        elif len( args ) > 0:
305
            # popen( cmd, arg1, arg2... )
306
            cmd = list( args )
307
        # Attach to our namespace  using mnexec -a
308
        mncmd = defaults[ 'mncmd' ]
309
        del defaults[ 'mncmd' ]
310
        cmd = mncmd + cmd
311
        # Shell requires a string, not a list!
312
        if defaults.get( 'shell', False ):
313
            cmd = ' '.join( cmd )
314
        return Popen( cmd, **defaults )
315

    
316
    def pexec( self, *args, **kwargs ):
317
        """Execute a command using popen
318
           returns: out, err, exitcode"""
319
        popen = self.popen( *args, **kwargs)
320
        out, err = popen.communicate()
321
        exitcode = popen.wait()
322
        return out, err, exitcode
323

    
324
    # Interface management, configuration, and routing
325

    
326
    # BL notes: This might be a bit redundant or over-complicated.
327
    # However, it does allow a bit of specialization, including
328
    # changing the canonical interface names. It's also tricky since
329
    # the real interfaces are created as veth pairs, so we can't
330
    # make a single interface at a time.
331

    
332
    def newPort( self ):
333
        "Return the next port number to allocate."
334
        if len( self.ports ) > 0:
335
            return max( self.ports.values() ) + 1
336
        return self.portBase
337

    
338
    def addIntf( self, intf, port=None ):
339
        """Add an interface.
340
           intf: interface
341
           port: port number (optional, typically OpenFlow port number)"""
342
        if port is None:
343
            port = self.newPort()
344
        self.intfs[ port ] = intf
345
        self.ports[ intf ] = port
346
        self.nameToIntf[ intf.name ] = intf
347
        debug( '\n' )
348
        debug( 'added intf %s:%d to node %s\n' % ( intf, port, self.name ) )
349
        if self.inNamespace:
350
            debug( 'moving', intf, 'into namespace for', self.name, '\n' )
351
            moveIntf( intf.name, self )
352

    
353
    def defaultIntf( self ):
354
        "Return interface for lowest port"
355
        ports = self.intfs.keys()
356
        if ports:
357
            return self.intfs[ min( ports ) ]
358
        else:
359
            warn( '*** defaultIntf: warning:', self.name,
360
                  'has no interfaces\n' )
361

    
362
    def intf( self, intf='' ):
363
        """Return our interface object with given string name,
364
           default intf if name is falsy (None, empty string, etc).
365
           or the input intf arg.
366

367
        Having this fcn return its arg for Intf objects makes it
368
        easier to construct functions with flexible input args for
369
        interfaces (those that accept both string names and Intf objects).
370
        """
371
        if not intf:
372
            return self.defaultIntf()
373
        elif type( intf) is str:
374
            return self.nameToIntf[ intf ]
375
        else:
376
            return intf
377

    
378
    def connectionsTo( self, node):
379
        "Return [ intf1, intf2... ] for all intfs that connect self to node."
380
        # We could optimize this if it is important
381
        connections = []
382
        for intf in self.intfList():
383
            link = intf.link
384
            if link:
385
                node1, node2 = link.intf1.node, link.intf2.node
386
                if node1 == self and node2 == node:
387
                    connections += [ ( intf, link.intf2 ) ]
388
                elif node1 == node and node2 == self:
389
                    connections += [ ( intf, link.intf1 ) ]
390
        return connections
391

    
392
    def deleteIntfs( self ):
393
        "Delete all of our interfaces."
394
        # In theory the interfaces should go away after we shut down.
395
        # However, this takes time, so we're better off removing them
396
        # explicitly so that we won't get errors if we run before they
397
        # have been removed by the kernel. Unfortunately this is very slow,
398
        # at least with Linux kernels before 2.6.33
399
        for intf in self.intfs.values():
400
            intf.delete()
401
            info( '.' )
402

    
403
    # Routing support
404

    
405
    def setARP( self, ip, mac ):
406
        """Add an ARP entry.
407
           ip: IP address as string
408
           mac: MAC address as string"""
409
        result = self.cmd( 'arp', '-s', ip, mac )
410
        return result
411

    
412
    def setHostRoute( self, ip, intf ):
413
        """Add route to host.
414
           ip: IP address as dotted decimal
415
           intf: string, interface name"""
416
        return self.cmd( 'route add -host', ip, 'dev', intf )
417

    
418
    def setDefaultRoute( self, intf=None ):
419
        """Set the default route to go through intf.
420
           intf: string, interface name"""
421
        if not intf:
422
            intf = self.defaultIntf()
423
        self.cmd( 'ip route flush root 0/0' )
424
        return self.cmd( 'route add default %s' % intf )
425

    
426
    # Convenience and configuration methods
427

    
428
    def setMAC( self, mac, intf=None ):
429
        """Set the MAC address for an interface.
430
           intf: intf or intf name
431
           mac: MAC address as string"""
432
        return self.intf( intf ).setMAC( mac )
433

    
434
    def setIP( self, ip, prefixLen=8, intf=None ):
435
        """Set the IP address for an interface.
436
           intf: intf or intf name
437
           ip: IP address as a string
438
           prefixLen: prefix length, e.g. 8 for /8 or 16M addrs"""
439
        # This should probably be rethought
440
        if '/' not in ip:
441
            ip = '%s/%s' % ( ip, prefixLen )
442
        return self.intf( intf ).setIP( ip )
443

    
444
    def IP( self, intf=None ):
445
        "Return IP address of a node or specific interface."
446
        return self.intf( intf ).IP()
447

    
448
    def MAC( self, intf=None ):
449
        "Return MAC address of a node or specific interface."
450
        return self.intf( intf ).IP()
451

    
452
    def intfIsUp( self, intf=None ):
453
        "Check if an interface is up."
454
        return self.intf( intf ).isUp()
455

    
456
    # The reason why we configure things in this way is so
457
    # That the parameters can be listed and documented in
458
    # the config method.
459
    # Dealing with subclasses and superclasses is slightly
460
    # annoying, but at least the information is there!
461

    
462
    def setParam( self, results, method, **param ):
463
        """Internal method: configure a *single* parameter
464
           results: dict of results to update
465
           method: config method name
466
           param: arg=value (ignore if value=None)
467
           value may also be list or dict"""
468
        name, value = param.items()[ 0 ]
469
        f = getattr( self, method, None )
470
        if not f or value is None:
471
            return
472
        if type( value ) is list:
473
            result = f( *value )
474
        elif type( value ) is dict:
475
            result = f( **value )
476
        else:
477
            result = f( value )
478
        results[ name ] = result
479
        return result
480

    
481
    def config( self, mac=None, ip=None,
482
                defaultRoute=None, lo='up', **_params ):
483
        """Configure Node according to (optional) parameters:
484
           mac: MAC address for default interface
485
           ip: IP address for default interface
486
           ifconfig: arbitrary interface configuration
487
           Subclasses should override this method and call
488
           the parent class's config(**params)"""
489
        # If we were overriding this method, we would call
490
        # the superclass config method here as follows:
491
        # r = Parent.config( **_params )
492
        r = {}
493
        self.setParam( r, 'setMAC', mac=mac )
494
        self.setParam( r, 'setIP', ip=ip )
495
        self.setParam( r, 'defaultRoute', defaultRoute=defaultRoute )
496
        # This should be examined
497
        self.cmd( 'ifconfig lo ' + lo )
498
        return r
499

    
500
    def configDefault( self, **moreParams ):
501
        "Configure with default parameters"
502
        self.params.update( moreParams )
503
        self.config( **self.params )
504

    
505
    # This is here for backward compatibility
506
    def linkTo( self, node, link=Link ):
507
        """(Deprecated) Link to another node
508
           replace with Link( node1, node2)"""
509
        return link( self, node )
510

    
511
    # Other methods
512

    
513
    def intfList( self ):
514
        "List of our interfaces sorted by port number"
515
        return [ self.intfs[ p ] for p in sorted( self.intfs.iterkeys() ) ]
516

    
517
    def intfNames( self ):
518
        "The names of our interfaces sorted by port number"
519
        return [ str( i ) for i in self.intfList() ]
520

    
521
    def __repr__( self ):
522
        "More informative string representation"
523
        intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() )
524
                              for i in self.intfList() ] ) )
525
        return '<%s %s: %s pid=%s> ' % (
526
            self.__class__.__name__, self.name, intfs, self.pid )
527

    
528
    def __str__( self ):
529
        "Abbreviated string representation"
530
        return self.name
531

    
532
    # Automatic class setup support
533

    
534
    isSetup = False
535

    
536
    @classmethod
537
    def checkSetup( cls ):
538
        "Make sure our class and superclasses are set up"
539
        while cls and not getattr( cls, 'isSetup', True ):
540
            cls.setup()
541
            cls.isSetup = True
542
            # Make pylint happy
543
            cls = getattr( type( cls ), '__base__', None )
544

    
545
    @classmethod
546
    def setup( cls ):
547
        "Make sure our class dependencies are available"
548
        pathCheck( 'mnexec', 'ifconfig', moduleName='Mininet')
549

    
550

    
551
class Host( Node ):
552
    "A host is simply a Node"
553
    pass
554

    
555

    
556
class CPULimitedHost( Host ):
557

    
558
    "CPU limited host"
559

    
560
    def __init__( self, name, sched='cfs', **kwargs ):
561
        Host.__init__( self, name, **kwargs )
562
        # Initialize class if necessary
563
        if not CPULimitedHost.inited:
564
            CPULimitedHost.init()
565
        # Create a cgroup and move shell into it
566
        self.cgroup = 'cpu,cpuacct,cpuset:/' + self.name
567
        errFail( 'cgcreate -g ' + self.cgroup )
568
        # We don't add ourselves to a cpuset because you must
569
        # specify the cpu and memory placement first
570
        errFail( 'cgclassify -g cpu,cpuacct:/%s %s' % ( self.name, self.pid ) )
571
        # BL: Setting the correct period/quota is tricky, particularly
572
        # for RT. RT allows very small quotas, but the overhead
573
        # seems to be high. CFS has a mininimum quota of 1 ms, but
574
        # still does better with larger period values.
575
        self.period_us = kwargs.get( 'period_us', 100000 )
576
        self.sched = sched
577
        self.rtprio = 20
578

    
579
    def cgroupSet( self, param, value, resource='cpu' ):
580
        "Set a cgroup parameter and return its value"
581
        cmd = 'cgset -r %s.%s=%s /%s' % (
582
            resource, param, value, self.name )
583
        quietRun( cmd )
584
        nvalue = int( self.cgroupGet( param, resource ) )
585
        if nvalue != value:
586
            error( '*** error: cgroupSet: %s set to %s instead of %s\n'
587
                   % ( param, nvalue, value ) )
588
        return nvalue
589

    
590
    def cgroupGet( self, param, resource='cpu' ):
591
        "Return value of cgroup parameter"
592
        cmd = 'cgget -r %s.%s /%s' % (
593
            resource, param, self.name )
594
        return int( quietRun( cmd ).split()[ -1 ] )
595

    
596
    def cgroupDel( self ):
597
        "Clean up our cgroup"
598
        # info( '*** deleting cgroup', self.cgroup, '\n' )
599
        _out, _err, exitcode = errRun( 'cgdelete -r ' + self.cgroup )
600
        return exitcode != 0
601

    
602
    def popen( self, *args, **kwargs ):
603
        """Return a Popen() object in node's namespace
604
           args: Popen() args, single list, or string
605
           kwargs: Popen() keyword args"""
606
        # Tell mnexec to execute command in our cgroup
607
        mncmd = [ 'mnexec', '-a', str( self.pid ),
608
                  '-g', self.name ]
609
        if self.sched == 'rt':
610
            mncmd += [ '-r', str( self.rtprio ) ]
611
        return Host.popen( self, *args, mncmd=mncmd, **kwargs )
612

    
613
    def cleanup( self ):
614
        "Clean up our cgroup"
615
        retry( retries=3, delaySecs=1, fn=self.cgroupDel )
616

    
617
    def chrt( self ):
618
        "Set RT scheduling priority"
619
        quietRun( 'chrt -p %s %s' % ( self.rtprio, self.pid ) )
620
        result = quietRun( 'chrt -p %s' % self.pid )
621
        firstline = result.split( '\n' )[ 0 ]
622
        lastword = firstline.split( ' ' )[ -1 ]
623
        if lastword != 'SCHED_RR':
624
            error( '*** error: could not assign SCHED_RR to %s\n' % self.name )
625
        return lastword
626

    
627
    def rtInfo( self, f ):
628
        "Internal method: return parameters for RT bandwidth"
629
        pstr, qstr = 'rt_period_us', 'rt_runtime_us'
630
        # RT uses wall clock time for period and quota
631
        quota = int( self.period_us * f * numCores() )
632
        return pstr, qstr, self.period_us, quota
633

    
634
    def cfsInfo( self, f):
635
        "Internal method: return parameters for CFS bandwidth"
636
        pstr, qstr = 'cfs_period_us', 'cfs_quota_us'
637
        # CFS uses wall clock time for period and CPU time for quota.
638
        quota = int( self.period_us * f * numCores() )
639
        period = self.period_us
640
        if f > 0 and quota < 1000:
641
            debug( '(cfsInfo: increasing default period) ' )
642
            quota = 1000
643
            period = int( quota / f / numCores() )
644
        return pstr, qstr, period, quota
645

    
646
    # BL comment:
647
    # This may not be the right API,
648
    # since it doesn't specify CPU bandwidth in "absolute"
649
    # units the way link bandwidth is specified.
650
    # We should use MIPS or SPECINT or something instead.
651
    # Alternatively, we should change from system fraction
652
    # to CPU seconds per second, essentially assuming that
653
    # all CPUs are the same.
654

    
655
    def setCPUFrac( self, f=-1, sched=None):
656
        """Set overall CPU fraction for this host
657
           f: CPU bandwidth limit (fraction)
658
           sched: 'rt' or 'cfs'
659
           Note 'cfs' requires CONFIG_CFS_BANDWIDTH"""
660
        if not f:
661
            return
662
        if not sched:
663
            sched = self.sched
664
        if sched == 'rt':
665
            pstr, qstr, period, quota = self.rtInfo( f )
666
        elif sched == 'cfs':
667
            pstr, qstr, period, quota = self.cfsInfo( f )
668
        else:
669
            return
670
        if quota < 0:
671
            # Reset to unlimited
672
            quota = -1
673
        # Set cgroup's period and quota
674
        self.cgroupSet( pstr, period )
675
        self.cgroupSet( qstr, quota )
676
        if sched == 'rt':
677
            # Set RT priority if necessary
678
            self.chrt()
679
        info( '(%s %d/%dus) ' % ( sched, quota, period ) )
680

    
681
    def setCPUs( self, cores, mems=0 ):
682
        "Specify (real) cores that our cgroup can run on"
683
        if type( cores ) is list:
684
            cores = ','.join( [ str( c ) for c in cores ] )
685
        self.cgroupSet( resource='cpuset', param='cpus',
686
                        value=cores )
687
        # Memory placement is probably not relevant, but we
688
        # must specify it anyway
689
        self.cgroupSet( resource='cpuset', param='mems',
690
                        value=mems)
691
        # We have to do this here after we've specified
692
        # cpus and mems
693
        errFail( 'cgclassify -g cpuset:/%s %s' % (
694
                 self.name, self.pid ) )
695

    
696
    def config( self, cpu=None, cores=None, **params ):
697
        """cpu: desired overall system CPU fraction
698
           cores: (real) core(s) this host can run on
699
           params: parameters for Node.config()"""
700
        r = Node.config( self, **params )
701
        # Was considering cpu={'cpu': cpu , 'sched': sched}, but
702
        # that seems redundant
703
        self.setParam( r, 'setCPUFrac', cpu=cpu )
704
        self.setParam( r, 'setCPUs', cores=cores )
705
        return r
706

    
707
    inited = False
708

    
709
    @classmethod
710
    def init( cls ):
711
        "Initialization for CPULimitedHost class"
712
        mountCgroups()
713
        cls.inited = True
714

    
715

    
716
# Some important things to note:
717
#
718
# The "IP" address which setIP() assigns to the switch is not
719
# an "IP address for the switch" in the sense of IP routing.
720
# Rather, it is the IP address for the control interface,
721
# on the control network, and it is only relevant to the
722
# controller. If you are running in the root namespace
723
# (which is the only way to run OVS at the moment), the
724
# control interface is the loopback interface, and you
725
# normally never want to change its IP address!
726
#
727
# In general, you NEVER want to attempt to use Linux's
728
# network stack (i.e. ifconfig) to "assign" an IP address or
729
# MAC address to a switch data port. Instead, you "assign"
730
# the IP and MAC addresses in the controller by specifying
731
# packets that you want to receive or send. The "MAC" address
732
# reported by ifconfig for a switch data port is essentially
733
# meaningless. It is important to understand this if you
734
# want to create a functional router using OpenFlow.
735

    
736
class Switch( Node ):
737
    """A Switch is a Node that is running (or has execed?)
738
       an OpenFlow switch."""
739

    
740
    portBase = 1  # Switches start with port 1 in OpenFlow
741
    dpidLen = 16  # digits in dpid passed to switch
742

    
743
    def __init__( self, name, dpid=None, opts='', listenPort=None, **params):
744
        """dpid: dpid for switch (or None to derive from name, e.g. s1 -> 1)
745
           opts: additional switch options
746
           listenPort: port to listen on for dpctl connections"""
747
        Node.__init__( self, name, **params )
748
        self.dpid = dpid if dpid else self.defaultDpid()
749
        self.opts = opts
750
        self.listenPort = listenPort
751
        if not self.inNamespace:
752
            self.controlIntf = Intf( 'lo', self, port=0 )
753

    
754
    def defaultDpid( self ):
755
        "Derive dpid from switch name, s1 -> 1"
756
        try:
757
            dpid = int( re.findall( '\d+', self.name )[ 0 ] )
758
            dpid = hex( dpid )[ 2: ]
759
            dpid = '0' * ( self.dpidLen - len( dpid ) ) + dpid
760
            return dpid
761
        except IndexError:
762
            raise Exception( 'Unable to derive default datapath ID - '
763
                             'please either specify a dpid or use a '
764
                             'canonical switch name such as s23.' )
765

    
766
    def defaultIntf( self ):
767
        "Return control interface"
768
        if self.controlIntf:
769
            return self.controlIntf
770
        else:
771
            return Node.defaultIntf( self )
772

    
773
    def sendCmd( self, *cmd, **kwargs ):
774
        """Send command to Node.
775
           cmd: string"""
776
        kwargs.setdefault( 'printPid', False )
777
        if not self.execed:
778
            return Node.sendCmd( self, *cmd, **kwargs )
779
        else:
780
            error( '*** Error: %s has execed and cannot accept commands' %
781
                   self.name )
782

    
783
    def __repr__( self ):
784
        "More informative string representation"
785
        intfs = ( ','.join( [ '%s:%s' % ( i.name, i.IP() )
786
                              for i in self.intfList() ] ) )
787
        return '<%s %s: %s pid=%s> ' % (
788
            self.__class__.__name__, self.name, intfs, self.pid )
789

    
790
class UserSwitch( Switch ):
791
    "User-space switch."
792

    
793
    dpidLen = 12
794

    
795
    def __init__( self, name, **kwargs ):
796
        """Init.
797
           name: name for the switch"""
798
        Switch.__init__( self, name, **kwargs )
799
        pathCheck( 'ofdatapath', 'ofprotocol',
800
                   moduleName='the OpenFlow reference user switch' +
801
                              '(openflow.org)' )
802
        if self.listenPort:
803
            self.opts += ' --listen=ptcp:%i ' % self.listenPort
804

    
805
    @classmethod
806
    def setup( cls ):
807
        "Ensure any dependencies are loaded; if not, try to load them."
808
        if not os.path.exists( '/dev/net/tun' ):
809
            moduleDeps( add=TUN )
810

    
811
    def dpctl( self, *args ):
812
        "Run dpctl command"
813
        if not self.listenPort:
814
            return "can't run dpctl without passive listening port"
815
        return self.cmd( 'dpctl ' + ' '.join( args ) +
816
                         ' tcp:127.0.0.1:%i' % self.listenPort )
817

    
818
    def start( self, controllers ):
819
        """Start OpenFlow reference user datapath.
820
           Log to /tmp/sN-{ofd,ofp}.log.
821
           controllers: list of controller objects"""
822
        # Add controllers
823
        clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
824
                            for c in controllers ] )
825
        ofdlog = '/tmp/' + self.name + '-ofd.log'
826
        ofplog = '/tmp/' + self.name + '-ofp.log'
827
        self.cmd( 'ifconfig lo up' )
828
        intfs = [ str( i ) for i in self.intfList() if not i.IP() ]
829
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
830
                  ' punix:/tmp/' + self.name + ' -d ' + self.dpid +
831
                  ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
832
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
833
                  ' ' + clist +
834
                  ' --fail=closed ' + self.opts +
835
                  ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
836

    
837
    def stop( self ):
838
        "Stop OpenFlow reference user datapath."
839
        self.cmd( 'kill %ofdatapath' )
840
        self.cmd( 'kill %ofprotocol' )
841
        self.deleteIntfs()
842

    
843

    
844
class OVSLegacyKernelSwitch( Switch ):
845
    """Open VSwitch legacy kernel-space switch using ovs-openflowd.
846
       Currently only works in the root namespace."""
847

    
848
    def __init__( self, name, dp=None, **kwargs ):
849
        """Init.
850
           name: name for switch
851
           dp: netlink id (0, 1, 2, ...)
852
           defaultMAC: default MAC as unsigned int; random value if None"""
853
        Switch.__init__( self, name, **kwargs )
854
        self.dp = dp if dp else self.name
855
        self.intf = self.dp
856
        if self.inNamespace:
857
            error( "OVSKernelSwitch currently only works"
858
                   " in the root namespace.\n" )
859
            exit( 1 )
860

    
861
    @classmethod
862
    def setup( cls ):
863
        "Ensure any dependencies are loaded; if not, try to load them."
864
        pathCheck( 'ovs-dpctl', 'ovs-openflowd',
865
                   moduleName='Open vSwitch (openvswitch.org)')
866
        moduleDeps( subtract=OF_KMOD, add=OVS_KMOD )
867

    
868
    def start( self, controllers ):
869
        "Start up kernel datapath."
870
        ofplog = '/tmp/' + self.name + '-ofp.log'
871
        quietRun( 'ifconfig lo up' )
872
        # Delete local datapath if it exists;
873
        # then create a new one monitoring the given interfaces
874
        self.cmd( 'ovs-dpctl del-dp ' + self.dp )
875
        self.cmd( 'ovs-dpctl add-dp ' + self.dp )
876
        intfs = [ str( i ) for i in self.intfList() if not i.IP() ]
877
        self.cmd( 'ovs-dpctl', 'add-if', self.dp, ' '.join( intfs ) )
878
        # Run protocol daemon
879
        clist = ','.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
880
                            for c in controllers ] )
881
        self.cmd( 'ovs-openflowd ' + self.dp +
882
                  ' ' + clist +
883
                  ' --fail=secure ' + self.opts +
884
                  ' --datapath-id=' + self.dpid +
885
                  ' 1>' + ofplog + ' 2>' + ofplog + '&' )
886
        self.execed = False
887

    
888
    def stop( self ):
889
        "Terminate kernel datapath."
890
        quietRun( 'ovs-dpctl del-dp ' + self.dp )
891
        self.cmd( 'kill %ovs-openflowd' )
892
        self.deleteIntfs()
893

    
894

    
895
class OVSSwitch( Switch ):
896
    "Open vSwitch switch. Depends on ovs-vsctl."
897

    
898
    def __init__( self, name, failMode='secure', **params ):
899
        """Init.
900
           name: name for switch
901
           failMode: controller loss behavior (secure|open)"""
902
        Switch.__init__( self, name, **params )
903
        self.failMode = failMode
904

    
905
    @classmethod
906
    def setup( cls ):
907
        "Make sure Open vSwitch is installed and working"
908
        pathCheck( 'ovs-vsctl',
909
                   moduleName='Open vSwitch (openvswitch.org)')
910
        # This should no longer be needed, and it breaks
911
        # with OVS 1.7 which has renamed the kernel module:
912
        #  moduleDeps( subtract=OF_KMOD, add=OVS_KMOD )
913
        out, err, exitcode = errRun( 'ovs-vsctl -t 1 show' )
914
        if exitcode:
915
            error( out + err +
916
                   'ovs-vsctl exited with code %d\n' % exitcode +
917
                   '*** Error connecting to ovs-db with ovs-vsctl\n'
918
                   'Make sure that Open vSwitch is installed, '
919
                   'that ovsdb-server is running, and that\n'
920
                   '"ovs-vsctl show" works correctly.\n'
921
                   'You may wish to try '
922
                   '"service openvswitch-switch start".\n' )
923
            exit( 1 )
924

    
925
    def dpctl( self, *args ):
926
        "Run ovs-dpctl command"
927
        return self.cmd( 'ovs-dpctl', args[ 0 ], self, *args[ 1: ] )
928

    
929
    @staticmethod
930
    def TCReapply( intf ):
931
        """Unfortunately OVS and Mininet are fighting
932
           over tc queuing disciplines. As a quick hack/
933
           workaround, we clear OVS's and reapply our own."""
934
        if type( intf ) is TCIntf:
935
            intf.config( **intf.params )
936

    
937
    def attach( self, intf ):
938
        "Connect a data port"
939
        self.cmd( 'ovs-vsctl add-port', self, intf )
940
        self.cmd( 'ifconfig', intf, 'up' )
941
        self.TCReapply( intf )
942

    
943
    def detach( self, intf ):
944
        "Disconnect a data port"
945
        self.cmd( 'ovs-vsctl del-port', self, intf )
946

    
947
    def start( self, controllers ):
948
        "Start up a new OVS OpenFlow switch using ovs-vsctl"
949
        if self.inNamespace:
950
            raise Exception(
951
                'OVS kernel switch does not work in a namespace' )
952
        # We should probably call config instead, but this
953
        # requires some rethinking...
954
        self.cmd( 'ifconfig lo up' )
955
        # Annoyingly, --if-exists option seems not to work
956
        self.cmd( 'ovs-vsctl del-br', self )
957
        self.cmd( 'ovs-vsctl add-br', self )
958
        self.cmd( 'ovs-vsctl -- set Bridge', self,
959
                  'other_config:datapath-id=' + self.dpid )
960
        self.cmd( 'ovs-vsctl set-fail-mode', self, self.failMode )
961
        for intf in self.intfList():
962
            if not intf.IP():
963
                self.attach( intf )
964
        # Add controllers
965
        clist = ' '.join( [ 'tcp:%s:%d' % ( c.IP(), c.port )
966
                            for c in controllers ] )
967
        if self.listenPort:
968
            clist += ' ptcp:%s' % self.listenPort
969
        self.cmd( 'ovs-vsctl set-controller', self, clist )
970

    
971
    def stop( self ):
972
        "Terminate OVS switch."
973
        self.cmd( 'ovs-vsctl del-br', self )
974
        self.deleteIntfs()
975

    
976
OVSKernelSwitch = OVSSwitch
977

    
978

    
979
class Controller( Node ):
980
    """A Controller is a Node that is running (or has execed?) an
981
       OpenFlow controller."""
982

    
983
    def __init__( self, name, inNamespace=False, command='controller',
984
                  cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1",
985
                  port=6633, **params ):
986
        self.command = command
987
        self.cargs = cargs
988
        self.cdir = cdir
989
        self.ip = ip
990
        self.port = port
991
        Node.__init__( self, name, inNamespace=inNamespace,
992
                       ip=ip, **params  )
993
        self.cmd( 'ifconfig lo up' )  # Shouldn't be necessary
994
        self.checkListening()
995

    
996
    def checkListening( self ):
997
        "Make sure no controllers are running on our port"
998
        # Verify that Telnet is installed first:
999
        out, err, returnCode = errRun( "which telnet" )
1000
        if 'telnet' not in out or returnCode != 0:
1001
            raise Exception( "Error running telnet to check for listening "
1002
                             "controllers; please check that it is "
1003
                             "installed." )
1004
        listening = self.cmd( "echo A | telnet -e A %s %d" %
1005
                              ( self.ip, self.port ) )
1006
        if 'Unable' not in listening:
1007
            servers = self.cmd( 'netstat -atp' ).split( '\n' )
1008
            pstr = ':%d ' % self.port
1009
            clist = servers[ 0:1 ] + [ s for s in servers if pstr in s ]
1010
            raise Exception( "Please shut down the controller which is"
1011
                             " running on port %d:\n" % self.port +
1012
                             '\n'.join( clist ) )
1013

    
1014
    def start( self ):
1015
        """Start <controller> <args> on controller.
1016
           Log to /tmp/cN.log"""
1017
        pathCheck( self.command )
1018
        cout = '/tmp/' + self.name + '.log'
1019
        if self.cdir is not None:
1020
            self.cmd( 'cd ' + self.cdir )
1021
        self.cmd( self.command + ' ' + self.cargs % self.port +
1022
                  ' 1>' + cout + ' 2>' + cout + '&' )
1023
        self.execed = False
1024

    
1025
    def stop( self ):
1026
        "Stop controller."
1027
        self.cmd( 'kill %' + self.command )
1028
        self.terminate()
1029

    
1030
    def IP( self, intf=None ):
1031
        "Return IP address of the Controller"
1032
        if self.intfs:
1033
            ip = Node.IP( self, intf )
1034
        else:
1035
            ip = self.ip
1036
        return ip
1037

    
1038
    def __repr__( self ):
1039
        "More informative string representation"
1040
        return '<%s %s: %s:%s pid=%s> ' % (
1041
            self.__class__.__name__, self.name,
1042
            self.IP(), self.port, self.pid )
1043

    
1044

    
1045
class OVSController( Controller ):
1046
    "Open vSwitch controller"
1047
    def __init__( self, name, command='ovs-controller', **kwargs ):
1048
        Controller.__init__( self, name, command=command, **kwargs )
1049

    
1050

    
1051
class NOX( Controller ):
1052
    "Controller to run a NOX application."
1053

    
1054
    def __init__( self, name, *noxArgs, **kwargs ):
1055
        """Init.
1056
           name: name to give controller
1057
           noxArgs: arguments (strings) to pass to NOX"""
1058
        if not noxArgs:
1059
            warn( 'warning: no NOX modules specified; '
1060
                  'running packetdump only\n' )
1061
            noxArgs = [ 'packetdump' ]
1062
        elif type( noxArgs ) not in ( list, tuple ):
1063
            noxArgs = [ noxArgs ]
1064

    
1065
        if 'NOX_CORE_DIR' not in os.environ:
1066
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
1067
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
1068

    
1069
        Controller.__init__( self, name,
1070
                             command=noxCoreDir + '/nox_core',
1071
                             cargs='--libdir=/usr/local/lib -v -i ptcp:%s ' +
1072
                             ' '.join( noxArgs ),
1073
                             cdir=noxCoreDir,
1074
                             **kwargs )
1075

    
1076

    
1077
class RemoteController( Controller ):
1078
    "Controller running outside of Mininet's control."
1079

    
1080
    def __init__( self, name, ip='127.0.0.1',
1081
                  port=6633, **kwargs):
1082
        """Init.
1083
           name: name to give controller
1084
           ip: the IP address where the remote controller is
1085
           listening
1086
           port: the port where the remote controller is listening"""
1087
        Controller.__init__( self, name, ip=ip, port=port, **kwargs )
1088

    
1089
    def start( self ):
1090
        "Overridden to do nothing."
1091
        return
1092

    
1093
    def stop( self ):
1094
        "Overridden to do nothing."
1095
        return
1096

    
1097
    def checkListening( self ):
1098
        "Warn if remote controller is not accessible"
1099
        listening = self.cmd( "echo A | telnet -e A %s %d" %
1100
                              ( self.ip, self.port ) )
1101
        if 'Unable' in listening:
1102
            warn( "Unable to contact the remote controller"
1103
                  " at %s:%d\n" % ( self.ip, self.port ) )