Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 75d72d96

History | View | Annotate | Download (18.5 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
"""
37

    
38
import os
39
import re
40
import signal
41
import select
42
from subprocess import Popen, PIPE, STDOUT
43
from time import sleep
44

    
45
from mininet.log import info, error, debug
46
from mininet.util import quietRun, moveIntf
47

    
48

    
49
class Node( object ):
50
    """A virtual network node is simply a shell in a network namespace.
51
       We communicate with it using pipes."""
52

    
53
    inToNode = {} # mapping of input fds to nodes
54
    outToNode = {} # mapping of output fds to nodes
55

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

    
92
    @classmethod
93
    def fdToNode( cls, fd ):
94
        """Return node corresponding to given file descriptor.
95
           fd: file descriptor
96
           returns: node"""
97
        node = Node.outToNode.get( fd )
98
        return node or Node.inToNode.get( fd )
99

    
100
    def cleanup( self ):
101
        "Help python collect its garbage."
102
        self.shell = None
103

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

    
110
    def write( self, data ):
111
        """Write data to node.
112
           data: string"""
113
        os.write( self.stdin.fileno(), data )
114

    
115
    def terminate( self ):
116
        "Send kill signal to Node and clean up after it."
117
        os.kill( self.pid, signal.SIGKILL )
118
        self.cleanup()
119

    
120
    def stop( self ):
121
        "Stop node."
122
        self.terminate()
123

    
124
    def waitReadable( self ):
125
        "Wait until node's output is readable."
126
        self.pollOut.poll()
127

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

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

    
153
    def sendInt( self ):
154
        "Send ^C, hopefully interrupting an interactive subprocess."
155
        self.write( chr( 3 ) )
156

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

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

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

    
191
    # Interface management, configuration, and routing
192

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

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

    
203
    def newIntf( self ):
204
        "Reserve and return a new interface name."
205
        intfName = self.intfName( self.intfCount )
206
        self.intfCount += 1
207
        return intfName
208

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

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

    
225
    def deleteIntfs( self ):
226
        "Delete all of our interfaces."
227
        # In theory the interfaces should go away after we shut down.
228
        # However, this takes time, so we're better off removing them
229
        # explicitly so that we won't get errors if we run before they
230
        # have been removed by the kernel. Unfortunately this is very slow.
231
        for intf in self.intfs.values():
232
            quietRun( 'ip link del ' + intf )
233
            info( '.' )
234
            # Does it help to sleep to let things run?
235
            sleep( 0.001 )
236

    
237
    def setMAC( self, intf, mac ):
238
        """Set the MAC address for an interface.
239
           mac: MAC address as string"""
240
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
241
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
242
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
243
        return result
244

    
245
    def setARP( self, ip, mac ):
246
        """Add an ARP entry.
247
           ip: IP address as string
248
           mac: MAC address as string"""
249
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
250
        return result
251

    
252
    def setIP( self, intf, ip, prefixLen ):
253
        """Set the IP address for an interface.
254
           intf: interface name
255
           ip: IP address as a string
256
           prefixLen: prefix length, e.g. 8 for /8 or 16M addrs"""
257
        ipSub = '%s/%d' % ( ip, prefixLen )
258
        result = self.cmd( [ 'ifconfig', intf, ipSub, 'up' ] )
259
        self.ips[ intf ] = ip
260
        return result
261

    
262
    def setHostRoute( self, ip, intf ):
263
        """Add route to host.
264
           ip: IP address as dotted decimal
265
           intf: string, interface name"""
266
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
267

    
268
    def setDefaultRoute( self, intf ):
269
        """Set the default route to go through intf.
270
           intf: string, interface name"""
271
        self.cmd( 'ip route flush' )
272
        return self.cmd( 'route add default ' + intf )
273

    
274
    def IP( self ):
275
        "Return IP address of interface 0"
276
        return self.ips.get( self.intfs.get( 0 , None ), None )
277

    
278
    def MAC( self ):
279
        "Return MAC address of interface 0"
280
        ifconfig = self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
281
        macs = re.findall( '..:..:..:..:..:..', ifconfig )
282
        if len( macs ) > 0:
283
            return macs[ 0 ]
284
        else:
285
            return None
286
        
287
    def intfIsUp( self, port ):
288
        """Check if interface for a given port number is up.
289
           port: port number"""
290
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ port ] )
291

    
292
    # Other methods
293
    def __str__( self ):
294
        result = self.name + ':'
295
        result += ' IP=' + str( self.IP() )
296
        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
297
        result += ' waiting=' + str( self.waiting )
298
        return result
299

    
300

    
301
class Host( Node ):
302
    "A host is simply a Node."
303
    pass
304

    
305

    
306
class Switch( Node ):
307
    """A Switch is a Node that is running (or has execed?)
308
       an OpenFlow switch."""
309

    
310
    def sendCmd( self, cmd ):
311
        """Send command to Node.
312
           cmd: string"""
313
        if not self.execed:
314
            return Node.sendCmd( self, cmd )
315
        else:
316
            error( '*** Error: %s has execed and cannot accept commands' %
317
                     self.name )
318

    
319
    def monitor( self ):
320
        "Monitor node."
321
        if not self.execed:
322
            return Node.monitor( self )
323
        else:
324
            return True, ''
325

    
326

    
327
class UserSwitch( Switch ):
328
    """User-space switch.
329
       Currently only works in the root namespace."""
330

    
331
    def __init__( self, name, *args, **kwargs ):
332
        """Init.
333
           name: name for the switch"""
334
        Switch.__init__( self, name, inNamespace=False, **kwargs )
335

    
336
    def start( self, controllers ):
337
        """Start OpenFlow reference user datapath.
338
           Log to /tmp/sN-{ofd,ofp}.log.
339
           controllers: list of controller objects"""
340
        controller = controllers[ 0 ]
341
        ofdlog = '/tmp/' + self.name + '-ofd.log'
342
        ofplog = '/tmp/' + self.name + '-ofp.log'
343
        self.cmd( 'ifconfig lo up' )
344
        intfs = sorted( self.intfs.values() )
345

    
346
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
347
            ' punix:/tmp/' + self.name +
348
            ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
349
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
350
            ' tcp:' + controller.IP() + ' --fail=closed' +
351
            ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
352

    
353
    def stop( self ):
354
        "Stop OpenFlow reference user datapath."
355
        self.cmd( 'kill %ofdatapath' )
356
        self.cmd( 'kill %ofprotocol' )
357
        self.deleteIntfs()
358

    
359
class KernelSwitch( Switch ):
360
    """Kernel-space switch.
361
       Currently only works in the root namespace."""
362

    
363
    def __init__( self, name, dp=None, **kwargs ):
364
        """Init.
365
           name:
366
           dp: netlink id (0, 1, 2, ...)
367
           defaultMAC: default MAC as string; random value if None"""
368
        Switch.__init__( self, name, inNamespace=False, **kwargs )
369
        self.dp = dp
370

    
371
    def start( self, controllers ):
372
        "Start up reference kernel datapath."
373
        ofplog = '/tmp/' + self.name + '-ofp.log'
374
        quietRun( 'ifconfig lo up' )
375
        # Delete local datapath if it exists;
376
        # then create a new one monitoring the given interfaces
377
        quietRun( 'dpctl deldp nl:%i' % self.dp )
378
        self.cmd( 'dpctl adddp nl:%i' % self.dp )
379
        if self.defaultMAC:
380
            intf = 'of%i' % self.dp
381
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', self.defaultMAC ] )
382

    
383
        if len( self.intfs ) != max( self.intfs ) + 1:
384
            raise Exception( 'only contiguous, zero-indexed port ranges'
385
                            'supported: %s' % self.intfs )
386
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
387
        self.cmd( 'dpctl addif nl:' + str( self.dp ) + ' ' +
388
            ' '.join( intfs ) )
389
        # Run protocol daemon
390
        controller = controllers[ 0 ]
391
        self.cmd( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
392
                      controller.IP() + ':' +
393
                      str( controller.port ) +
394
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
395
        self.execed = False
396

    
397
    def stop( self ):
398
        "Terminate kernel datapath."
399
        quietRun( 'dpctl deldp nl:%i' % self.dp )
400
        self.cmd( 'kill %ofprotocol' )
401
        self.deleteIntfs()
402

    
403
class OVSKernelSwitch( Switch ):
404
    """Open VSwitch kernel-space switch.
405
       Currently only works in the root namespace."""
406

    
407
    def __init__( self, name, dp=None, **kwargs ):
408
        """Init.
409
           name:
410
           dp: netlink id (0, 1, 2, ...)
411
           dpid: datapath ID as unsigned int; random value if None"""
412
        Switch.__init__( self, name, inNamespace=False, **kwargs )
413
        self.dp = dp
414

    
415
    def start( self, controllers ):
416
        "Start up kernel datapath."
417
        ofplog = '/tmp/' + self.name + '-ofp.log'
418
        quietRun( 'ifconfig lo up' )
419
        # Delete local datapath if it exists;
420
        # then create a new one monitoring the given interfaces
421
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
422
        self.cmd( 'ovs-dpctl add-dp dp%i' % self.dp )
423
        if self.defaultMAC:
424
            intf = 'dp' % self.dp
425
            mac = self.defaultMAC
426
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
427

    
428
        if len( self.intfs ) != max( self.intfs ) + 1:
429
            raise Exception( 'only contiguous, zero-indexed port ranges'
430
                            'supported: %s' % self.intfs )
431
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
432
        self.cmd( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
433
                      ' '.join( intfs ) )
434
        # Run protocol daemon
435
        controller = controllers[ 0 ]
436
        self.cmd( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
437
                      controller.IP() + ':' +
438
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
439
        self.execed = False
440

    
441
    def stop( self ):
442
        "Terminate kernel datapath."
443
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
444
        self.cmd( 'kill %ovs-openflowd' )
445
        self.deleteIntfs()
446

    
447

    
448
class Controller( Node ):
449
    """A Controller is a Node that is running (or has execed?) an
450
       OpenFlow controller."""
451

    
452
    def __init__( self, name, inNamespace=False, controller='controller',
453
                 cargs='-v ptcp:', cdir=None, defaultIP="127.0.0.1",
454
                 port=6633 ):
455
        self.controller = controller
456
        self.cargs = cargs
457
        self.cdir = cdir
458
        self.port = port
459
        Node.__init__( self, name, inNamespace=inNamespace,
460
            defaultIP=defaultIP )
461

    
462
    def start( self ):
463
        """Start <controller> <args> on controller.
464
           Log to /tmp/cN.log"""
465
        cout = '/tmp/' + self.name + '.log'
466
        if self.cdir is not None:
467
            self.cmd( 'cd ' + self.cdir )
468
        self.cmd( self.controller + ' ' + self.cargs +
469
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
470
        self.execed = False
471

    
472
    def stop( self ):
473
        "Stop controller."
474
        self.cmd( 'kill %' + self.controller )
475
        self.terminate()
476

    
477
    def IP( self ):
478
        "Return IP address of the Controller"
479
        return self.defaultIP
480

    
481

    
482
class ControllerParams( object ):
483
    "Container for controller IP parameters."
484

    
485
    def __init__( self, ip, prefixLen ):
486
        """Init.
487
           ip: string, controller IP address
488
           prefixLen: prefix length, e.g. 8 for /8, covering 16M"""
489
        self.ip = ip
490
        self.prefixLen = prefixLen
491

    
492

    
493
class NOX( Controller ):
494
    "Controller to run a NOX application."
495

    
496
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
497
        """Init.
498
           name: name to give controller
499
           noxArgs: list of args, or single arg, to pass to NOX"""
500
        if type( noxArgs ) != list:
501
            noxArgs = [ noxArgs ]
502
        if not noxArgs:
503
            noxArgs = [ 'packetdump' ]
504

    
505
        if 'NOX_CORE_DIR' not in os.environ:
506
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
507
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
508

    
509
        Controller.__init__( self, name,
510
            controller=noxCoreDir + '/nox_core',
511
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' +
512
                    ' '.join( noxArgs ),
513
            cdir = noxCoreDir, **kwargs )
514

    
515

    
516
class RemoteController( Controller ):
517
    "Controller running outside of Mininet's control."
518

    
519
    def __init__( self, name, inNamespace=False, defaultIP='127.0.0.1',
520
                 port=6633 ):
521
        """Init.
522
           name: name to give controller
523
           ipAddress: the IP address where the remote controller is
524
           listening
525
           port: the port where the remote controller is listening"""
526
        Controller.__init__( self, name, defaultIP=defaultIP, port=port )
527

    
528
    def start( self ):
529
        "Overridden to do nothing."
530
        return
531

    
532
    def stop( self ):
533
        "Overridden to do nothing."
534
        return