Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ b0a7a257

History | View | Annotate | Download (20.1 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
Switch: superclass for switch nodes.
17

18
UserSwitch: a switch using the user-space switch from the OpenFlow
19
    reference implementation.
20

21
KernelSwitch: a switch using the kernel switch from the OpenFlow reference
22
    implementation.
23

24
OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch
25
    implementation (openvswitch.org).
26

27
Controller: superclass for OpenFlow controllers. The default controller
28
    is controller(8) from the reference implementation.
29

30
NOXController: a controller node using NOX (noxrepo.org).
31

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

36
Future enhancements:
37

38
- Possibly make Node, Switch and Controller more abstract so that
39
  they can be used for both local and remote nodes
40

41
- Create proxy objects for remote nodes (Mininet: Cluster Edition)
42
"""
43

    
44
import os
45
import re
46
import signal
47
import select
48
from subprocess import Popen, PIPE, STDOUT
49
from time import sleep
50

    
51
from mininet.log import info, error, debug
52
from mininet.util import quietRun, makeIntfPair, moveIntf
53

    
54
class Node( object ):
55
    """A virtual network node is simply a shell in a network namespace.
56
       We communicate with it using pipes."""
57

    
58
    inToNode = {} # mapping of input fds to nodes
59
    outToNode = {} # mapping of output fds to nodes
60

    
61
    def __init__( self, name, inNamespace=True,
62
        defaultMAC=None, defaultIP=None ):
63
        """name: name of node
64
           inNamespace: in network namespace?
65
           defaultMAC: default MAC address for intf 0
66
           defaultIP: default IP address for intf 0"""
67
        self.name = name
68
        closeFds = False # speed vs. memory use
69
        # xpg_echo is needed so we can echo our sentinel in sendCmd
70
        cmd = [ '/bin/bash', '-O', 'xpg_echo' ]
71
        self.inNamespace = inNamespace
72
        if self.inNamespace:
73
            cmd = [ 'netns' ] + cmd
74
        self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
75
            close_fds=closeFds )
76
        self.stdin = self.shell.stdin
77
        self.stdout = self.shell.stdout
78
        self.pollOut = select.poll()
79
        self.pollOut.register( self.stdout )
80
        # Maintain mapping between file descriptors and nodes
81
        # This could be useful for monitoring multiple nodes
82
        # using select.poll()
83
        self.outToNode[ self.stdout.fileno() ] = self
84
        self.inToNode[ self.stdin.fileno() ] = self
85
        self.pid = self.shell.pid
86
        self.intfs = {} # dict of port numbers to interface names
87
        self.ports = {} # dict of interface names to port numbers
88
                        # replace with Port objects, eventually ?
89
        self.ips = {} # dict of interfaces to ip addresses as strings
90
        self.connection = {} # remote node connected to each interface
91
        self.waiting = False
92
        self.execed = False
93
        self.defaultIP = defaultIP
94
        self.defaultMAC = defaultMAC
95

    
96
    @classmethod
97
    def fdToNode( cls, fd ):
98
        """Return node corresponding to given file descriptor.
99
           fd: file descriptor
100
           returns: node"""
101
        node = Node.outToNode.get( fd )
102
        return node or Node.inToNode.get( fd )
103

    
104
    def cleanup( self ):
105
        "Help python collect its garbage."
106
        self.shell = None
107

    
108
    # Subshell I/O, commands and control
109
    def read( self, bytes ):
110
        """Read from a node.
111
           bytes: maximum number of bytes to read"""
112
        return os.read( self.stdout.fileno(), bytes )
113

    
114
    def write( self, data ):
115
        """Write data to node.
116
           data: string"""
117
        os.write( self.stdin.fileno(), data )
118

    
119
    def terminate( self ):
120
        "Send kill signal to Node and clean up after it."
121
        os.kill( self.pid, signal.SIGKILL )
122
        self.cleanup()
123

    
124
    def stop( self ):
125
        "Stop node."
126
        self.terminate()
127

    
128
    def waitReadable( self ):
129
        "Wait until node's output is readable."
130
        self.pollOut.poll()
131

    
132
    def sendCmd( self, cmd ):
133
        """Send a command, followed by a command to echo a sentinel,
134
           and return without waiting for the command to complete."""
135
        assert not self.waiting
136
        if cmd[ -1 ] == '&':
137
            separator = '&'
138
            cmd = cmd[ :-1 ]
139
        else:
140
            separator = ';'
141
        if isinstance( cmd, list ):
142
            cmd = ' '.join( cmd )
143
        self.write( cmd + separator + ' echo -n "\\0177" \n' )
144
        self.waiting = True
145

    
146
    def monitor( self ):
147
        "Monitor the output of a command, returning (done?, data)."
148
        assert self.waiting
149
        self.waitReadable()
150
        data = self.read( 1024 )
151
        if len( data ) > 0 and data[ -1 ] == chr( 0177 ):
152
            self.waiting = False
153
            return True, data[ :-1 ]
154
        else:
155
            return False, data
156

    
157
    def sendInt( self ):
158
        "Send ^C, hopefully interrupting an interactive subprocess."
159
        self.write( chr( 3 ) )
160

    
161
    def waitOutput( self, verbose=False ):
162
        """Wait for a command to complete.
163
           Completion is signaled by a sentinel character, ASCII(127)
164
           appearing in the output stream.  Wait for the sentinel and return
165
           the output, including trailing newline.
166
           verbose: print output interactively"""
167
        log = info if verbose else debug
168
        assert self.waiting
169
        output = ''
170
        while True:
171
            self.waitReadable()
172
            data = self.read( 1024 )
173
            if len( data ) > 0  and data[ -1 ] == chr( 0177 ):
174
                output += data[ :-1 ]
175
                log( output )
176
                break
177
            else:
178
                output += data
179
        self.waiting = False
180
        return output
181

    
182
    def cmd( self, cmd, verbose=False ):
183
        """Send a command, wait for output, and return it.
184
           cmd: string"""
185
        log = info if verbose else debug
186
        log( '*** %s : %s\n' % ( self.name, cmd ) )
187
        self.sendCmd( cmd )
188
        return self.waitOutput( verbose )
189

    
190
    def cmdPrint( self, cmd ):
191
        """Call cmd and printing its output
192
           cmd: string"""
193
        return self.cmd( cmd, verbose=True )
194

    
195
    # Interface management, configuration, and routing
196

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

    
203
    def intfName( self, n ):
204
        "Construct a canonical interface name node-ethN for interface n."
205
        return self.name + '-eth' + repr( n )
206

    
207
    def newPort( self ):
208
        "Return the next port number to allocate."
209
        if len( self.ports ) > 0:
210
            return max( self.ports.values() ) + 1
211
        return 0
212

    
213
    def addIntf( self, intf, port ):
214
        """Add an interface.
215
           intf: interface name (nodeN-ethM)
216
           port: port number (typically OpenFlow port number)"""
217
        self.intfs[ port ] = intf
218
        self.ports[ intf ] = port
219
        #info( '\n' )
220
        #info( 'added intf %s:%d to node %s\n' % ( intf,port, self.name ) )
221
        if self.inNamespace:
222
            #info( 'moving w/inNamespace set\n' )
223
            moveIntf( intf, self )
224

    
225
    def registerIntf( self, intf, dstNode, dstIntf ):
226
        "Register connection of intf to dstIntf on dstNode."
227
        self.connection[ intf ] = ( dstNode, dstIntf )
228

    
229
    # This is a symmetric operation, but it makes sense to put
230
    # the code here since it is tightly coupled to routines in
231
    # this class. For a more symmetric API, you can use
232
    # mininet.util.createLink()
233

    
234
    def linkTo( self, node2, port1=None, port2=None ):
235
        """Create link to another node, making two new interfaces.
236
           node2: Node to link us to
237
           port1: our port number (optional)
238
           port2: node2 port number (optional)
239
           returns: intf1 name, intf2 name"""
240
        node1 = self
241
        if port1 is None:
242
            port1 = node1.newPort()
243
        if port2 is None:
244
            port2 = node2.newPort()
245
        intf1 = node1.intfName( port1 )
246
        intf2 = node2.intfName( port2 )
247
        makeIntfPair( intf1, intf2 )
248
        node1.addIntf( intf1, port1 )
249
        node2.addIntf( intf2, port2 )
250
        node1.registerIntf( intf1, node2, intf2 )
251
        node2.registerIntf( intf2, node1, intf1 )
252
        return intf1, intf2
253

    
254
    def deleteIntfs( self ):
255
        "Delete all of our interfaces."
256
        # In theory the interfaces should go away after we shut down.
257
        # However, this takes time, so we're better off removing them
258
        # explicitly so that we won't get errors if we run before they
259
        # have been removed by the kernel. Unfortunately this is very slow,
260
        # at least with Linux kernels before 2.6.33
261
        for intf in self.intfs.values():
262
            quietRun( 'ip link del ' + intf )
263
            info( '.' )
264
            # Does it help to sleep to let things run?
265
            sleep( 0.001 )
266

    
267
    def setMAC( self, intf, mac ):
268
        """Set the MAC address for an interface.
269
           mac: MAC address as string"""
270
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
271
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
272
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
273
        return result
274

    
275
    def setARP( self, ip, mac ):
276
        """Add an ARP entry.
277
           ip: IP address as string
278
           mac: MAC address as string"""
279
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
280
        return result
281

    
282
    def setIP( self, intf, ip, prefixLen=8 ):
283
        """Set the IP address for an interface.
284
           intf: interface name
285
           ip: IP address as a string
286
           prefixLen: prefix length, e.g. 8 for /8 or 16M addrs"""
287
        ipSub = '%s/%d' % ( ip, prefixLen )
288
        result = self.cmd( [ 'ifconfig', intf, ipSub, 'up' ] )
289
        self.ips[ intf ] = ip
290
        return result
291

    
292
    def setHostRoute( self, ip, intf ):
293
        """Add route to host.
294
           ip: IP address as dotted decimal
295
           intf: string, interface name"""
296
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
297

    
298
    def setDefaultRoute( self, intf ):
299
        """Set the default route to go through intf.
300
           intf: string, interface name"""
301
        self.cmd( 'ip route flush' )
302
        return self.cmd( 'route add default ' + intf )
303

    
304
    def IP( self, intf=None ):
305
        "Return IP address of a node or specific interface."
306
        if len( self.ips ) == 1:
307
            return self.ips.values()[ 0 ]
308
        if intf:
309
            return self.ips.get( intf, None )
310

    
311
    def MAC( self, intf=None ):
312
        "Return MAC address of a node or specific interface."
313
        if intf is None and len( self.intfs ) == 1:
314
            intf = self.intfs.values()[ 0 ]
315
        ifconfig = self.cmd( 'ifconfig ' + intf )
316
        macs = re.findall( '..:..:..:..:..:..', ifconfig )
317
        if len( macs ) > 0:
318
            return macs[ 0 ]
319

    
320
    def intfIsUp( self, intf ):
321
        "Check if an interface is up."
322
        return 'UP' in self.cmd( 'ifconfig ' + intf )
323

    
324
    # Other methods
325
    def __str__( self ):
326
        result = self.name + ':'
327
        result += ' IP=' + str( self.IP() )
328
        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
329
        result += ' waiting=' + str( self.waiting )
330
        return result
331

    
332

    
333
class Host( Node ):
334
    "A host is simply a Node."
335
    pass
336

    
337

    
338
class Switch( Node ):
339
    """A Switch is a Node that is running (or has execed?)
340
       an OpenFlow switch."""
341

    
342
    def sendCmd( self, cmd ):
343
        """Send command to Node.
344
           cmd: string"""
345
        if not self.execed:
346
            return Node.sendCmd( self, cmd )
347
        else:
348
            error( '*** Error: %s has execed and cannot accept commands' %
349
                     self.name )
350

    
351
    def monitor( self ):
352
        "Monitor node."
353
        if not self.execed:
354
            return Node.monitor( self )
355
        else:
356
            return True, ''
357

    
358

    
359
class UserSwitch( Switch ):
360
    "User-space switch."
361

    
362
    def __init__( self, name, *args, **kwargs ):
363
        """Init.
364
           name: name for the switch"""
365
        Switch.__init__( self, name, **kwargs )
366

    
367
    def start( self, controllers ):
368
        """Start OpenFlow reference user datapath.
369
           Log to /tmp/sN-{ofd,ofp}.log.
370
           controllers: list of controller objects"""
371
        controller = controllers[ 0 ]
372
        ofdlog = '/tmp/' + self.name + '-ofd.log'
373
        ofplog = '/tmp/' + self.name + '-ofp.log'
374
        self.cmd( 'ifconfig lo up' )
375
        intfs = sorted( self.intfs.values() )
376
        if self.inNamespace:
377
            intfs = intfs[ :-1 ]
378
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
379
            ' punix:/tmp/' + self.name +
380
            ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
381
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
382
            ' tcp:' + controller.IP() + ' --fail=closed' +
383
            ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
384

    
385
    def stop( self ):
386
        "Stop OpenFlow reference user datapath."
387
        self.cmd( 'kill %ofdatapath' )
388
        self.cmd( 'kill %ofprotocol' )
389
        self.deleteIntfs()
390

    
391
class KernelSwitch( Switch ):
392
    """Kernel-space switch.
393
       Currently only works in root namespace."""
394

    
395
    def __init__( self, name, dp=None, **kwargs ):
396
        """Init.
397
           name: name for switch
398
           dp: netlink id (0, 1, 2, ...)
399
           defaultMAC: default MAC as string; random value if None"""
400
        Switch.__init__( self, name, **kwargs )
401
        self.dp = dp
402
        if self.inNamespace:
403
            error( "KernelSwitch currently only works"
404
                " in the root namespace." )
405
            exit( 1 )
406

    
407
    def start( self, controllers ):
408
        "Start up reference kernel datapath."
409
        ofplog = '/tmp/' + self.name + '-ofp.log'
410
        quietRun( 'ifconfig lo up' )
411
        # Delete local datapath if it exists;
412
        # then create a new one monitoring the given interfaces
413
        quietRun( 'dpctl deldp nl:%i' % self.dp )
414
        self.cmd( 'dpctl adddp nl:%i' % self.dp )
415
        if self.defaultMAC:
416
            intf = 'of%i' % self.dp
417
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', self.defaultMAC ] )
418
        if len( self.intfs ) != max( self.intfs ) + 1:
419
            raise Exception( 'only contiguous, zero-indexed port ranges'
420
                            'supported: %s' % self.intfs )
421
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
422
        self.cmd( 'dpctl addif nl:' + str( self.dp ) + ' ' +
423
            ' '.join( intfs ) )
424
        # Run protocol daemon
425
        controller = controllers[ 0 ]
426
        self.cmd( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
427
                      controller.IP() + ':' +
428
                      str( controller.port ) +
429
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
430
        self.execed = False
431

    
432
    def stop( self ):
433
        "Terminate kernel datapath."
434
        quietRun( 'dpctl deldp nl:%i' % self.dp )
435
        self.cmd( 'kill %ofprotocol' )
436
        self.deleteIntfs()
437

    
438
class OVSKernelSwitch( Switch ):
439
    """Open VSwitch kernel-space switch.
440
       Currently only works in the root namespace."""
441

    
442
    def __init__( self, name, dp=None, **kwargs ):
443
        """Init.
444
           name: name for switch
445
           dp: netlink id (0, 1, 2, ...)
446
           defaultMAC: default MAC as unsigned int; random value if None"""
447
        Switch.__init__( self, name, **kwargs )
448
        self.dp = dp
449
        if self.inNamespace:
450
            error( "OVSKernelSwitch currently only works"
451
                " in the root namespace." )
452
            exit( 1 )
453

    
454
    def start( self, controllers ):
455
        "Start up kernel datapath."
456
        ofplog = '/tmp/' + self.name + '-ofp.log'
457
        quietRun( 'ifconfig lo up' )
458
        # Delete local datapath if it exists;
459
        # then create a new one monitoring the given interfaces
460
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
461
        self.cmd( 'ovs-dpctl add-dp dp%i' % self.dp )
462
        if self.defaultMAC:
463
            intf = 'dp%i' % self.dp
464
            mac = self.defaultMAC
465
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
466

    
467
        if len( self.intfs ) != max( self.intfs ) + 1:
468
            raise Exception( 'only contiguous, zero-indexed port ranges'
469
                            'supported: %s' % self.intfs )
470
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
471
        self.cmd( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
472
                      ' '.join( intfs ) )
473
        # Run protocol daemon
474
        controller = controllers[ 0 ]
475
        self.cmd( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
476
                      controller.IP() + ':' +
477
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
478
        self.execed = False
479

    
480
    def stop( self ):
481
        "Terminate kernel datapath."
482
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
483
        self.cmd( 'kill %ovs-openflowd' )
484
        self.deleteIntfs()
485

    
486

    
487
class Controller( Node ):
488
    """A Controller is a Node that is running (or has execed?) an
489
       OpenFlow controller."""
490

    
491
    def __init__( self, name, inNamespace=False, controller='controller',
492
                 cargs='-v ptcp:', cdir=None, defaultIP="127.0.0.1",
493
                 port=6633 ):
494
        self.controller = controller
495
        self.cargs = cargs
496
        self.cdir = cdir
497
        self.port = port
498
        Node.__init__( self, name, inNamespace=inNamespace,
499
            defaultIP=defaultIP )
500

    
501
    def start( self ):
502
        """Start <controller> <args> on controller.
503
           Log to /tmp/cN.log"""
504
        cout = '/tmp/' + self.name + '.log'
505
        if self.cdir is not None:
506
            self.cmd( 'cd ' + self.cdir )
507
        self.cmd( self.controller + ' ' + self.cargs +
508
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
509
        self.execed = False
510

    
511
    def stop( self ):
512
        "Stop controller."
513
        self.cmd( 'kill %' + self.controller )
514
        self.terminate()
515

    
516
    def IP( self, intf=None ):
517
        "Return IP address of the Controller"
518
        ip = Node.IP( self, intf=intf )
519
        if ip is None:
520
            ip = self.defaultIP
521
        return ip
522

    
523
class ControllerParams( object ):
524
    "Container for controller IP parameters."
525

    
526
    def __init__( self, ip, prefixLen ):
527
        """Init.
528
           ip: string, controller IP address
529
           prefixLen: prefix length, e.g. 8 for /8, covering 16M"""
530
        self.ip = ip
531
        self.prefixLen = prefixLen
532

    
533

    
534
class NOX( Controller ):
535
    "Controller to run a NOX application."
536

    
537
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
538
        """Init.
539
           name: name to give controller
540
           noxArgs: list of args, or single arg, to pass to NOX"""
541
        if type( noxArgs ) != list:
542
            noxArgs = [ noxArgs ]
543
        if not noxArgs:
544
            noxArgs = [ 'packetdump' ]
545

    
546
        if 'NOX_CORE_DIR' not in os.environ:
547
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
548
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
549

    
550
        Controller.__init__( self, name,
551
            controller=noxCoreDir + '/nox_core',
552
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' +
553
                    ' '.join( noxArgs ),
554
            cdir = noxCoreDir, **kwargs )
555

    
556

    
557
class RemoteController( Controller ):
558
    "Controller running outside of Mininet's control."
559

    
560
    def __init__( self, name, inNamespace=False, defaultIP='127.0.0.1',
561
                 port=6633 ):
562
        """Init.
563
           name: name to give controller
564
           ipAddress: the IP address where the remote controller is
565
           listening
566
           port: the port where the remote controller is listening"""
567
        Controller.__init__( self, name, defaultIP=defaultIP, port=port )
568

    
569
    def start( self ):
570
        "Overridden to do nothing."
571
        return
572

    
573
    def stop( self ):
574
        "Overridden to do nothing."
575
        return