Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ e953444f

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
        # setsid is necessary to detach from tty
70
        # xpg_echo is needed so we can echo our sentinel in sendCmd
71
        cmd = [ '/usr/bin/setsid', '/bin/bash', '-O', 'xpg_echo' ]
72
        self.inNamespace = inNamespace
73
        if self.inNamespace:
74
            cmd = [ 'netns' ] + cmd
75
        self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
76
            close_fds=closeFds )
77
        self.stdin = self.shell.stdin
78
        self.stdout = self.shell.stdout
79
        self.pollOut = select.poll()
80
        self.pollOut.register( self.stdout )
81
        # Maintain mapping between file descriptors and nodes
82
        # This could be useful for monitoring multiple nodes
83
        # using select.poll()
84
        self.outToNode[ self.stdout.fileno() ] = self
85
        self.inToNode[ self.stdin.fileno() ] = self
86
        self.pid = self.shell.pid
87
        self.intfs = {} # dict of port numbers to interface names
88
        self.ports = {} # dict of interface names to port numbers
89
                        # replace with Port objects, eventually ?
90
        self.ips = {} # dict of interfaces to ip addresses as strings
91
        self.connection = {} # remote node connected to each interface
92
        self.waiting = False
93
        self.execed = False
94
        self.defaultIP = defaultIP
95
        self.defaultMAC = defaultMAC
96

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

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

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

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

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

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

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

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

    
147
    def sendInt( self ):
148
        """Placeholder for function to interrupt running subprocess.
149
           This is a tricky problem to solve."""
150
        self.write( chr( 3 ) )
151

    
152
    def monitor( self ):
153
        "Monitor the output of a command, returning (done?, data)."
154
        assert self.waiting
155
        self.waitReadable()
156
        data = self.read( 1024 )
157
        if len( data ) > 0 and data[ -1 ] == chr( 127 ):
158
            self.waiting = False
159
            return True, data[ :-1 ]
160
        elif chr( 127 ) in data:
161
            return True, data.replace( chr( 127 ), '' )
162
        return False, data
163

    
164
    def waitOutput( self, verbose=False ):
165
        """Wait for a command to complete.
166
           Completion is signaled by a sentinel character, ASCII(127)
167
           appearing in the output stream.  Wait for the sentinel and return
168
           the output, including trailing newline.
169
           verbose: print output interactively"""
170
        log = info if verbose else debug
171
        output = ''
172
        done = False
173
        while not done:
174
            done, data = self.monitor()
175
            output += data
176
            log( data )
177
        return output
178

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

    
187
    def cmdPrint( self, cmd ):
188
        """Call cmd and printing its output
189
           cmd: string"""
190
        return self.cmd( cmd, verbose=True )
191

    
192
    # Interface management, configuration, and routing
193

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

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

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

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

    
222
    def registerIntf( self, intf, dstNode, dstIntf ):
223
        "Register connection of intf to dstIntf on dstNode."
224
        self.connection[ intf ] = ( dstNode, dstIntf )
225

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

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

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

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

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

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

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

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

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

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

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

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

    
329

    
330
class Host( Node ):
331
    "A host is simply a Node."
332
    pass
333

    
334

    
335
class Switch( Node ):
336
    """A Switch is a Node that is running (or has execed?)
337
       an OpenFlow switch."""
338

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

    
348
    def monitor( self ):
349
        "Monitor node."
350
        if not self.execed:
351
            return Node.monitor( self )
352
        else:
353
            return True, ''
354

    
355

    
356
class UserSwitch( Switch ):
357
    "User-space switch."
358

    
359
    def __init__( self, name, *args, **kwargs ):
360
        """Init.
361
           name: name for the switch"""
362
        Switch.__init__( self, name, **kwargs )
363

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

    
382
    def stop( self ):
383
        "Stop OpenFlow reference user datapath."
384
        self.cmd( 'kill %ofdatapath' )
385
        self.cmd( 'kill %ofprotocol' )
386
        self.deleteIntfs()
387

    
388
class KernelSwitch( Switch ):
389
    """Kernel-space switch.
390
       Currently only works in root namespace."""
391

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

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

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

    
435
class OVSKernelSwitch( Switch ):
436
    """Open VSwitch kernel-space switch.
437
       Currently only works in the root namespace."""
438

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

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

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

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

    
483

    
484
class Controller( Node ):
485
    """A Controller is a Node that is running (or has execed?) an
486
       OpenFlow controller."""
487

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

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

    
508
    def stop( self ):
509
        "Stop controller."
510
        self.cmd( 'kill %' + self.controller )
511
        self.terminate()
512

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

    
520
class ControllerParams( object ):
521
    "Container for controller IP parameters."
522

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

    
530

    
531
class NOX( Controller ):
532
    "Controller to run a NOX application."
533

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

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

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

    
553

    
554
class RemoteController( Controller ):
555
    "Controller running outside of Mininet's control."
556

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

    
566
    def start( self ):
567
        "Overridden to do nothing."
568
        return
569

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