Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 80be5642

History | View | Annotate | Download (18.6 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, moveIntf
53

    
54

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

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

    
62
    def __init__( self, name, inNamespace=True,
63
        defaultMAC=None, defaultIP=None ):
64
        """name: name of node
65
           inNamespace: in network namespace?
66
           defaultMAC: default MAC address for intf 0
67
           defaultIP: default IP address for intf 0"""
68
        self.name = name
69
        closeFds = False # speed vs. memory use
70
        # xpg_echo is needed so we can echo our sentinel in sendCmd
71
        cmd = [ '/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.intfCount = 0
88
        self.intfs = {} # dict of port numbers to interface names
89
        self.ports = {} # dict of interface names to port numbers
90
                        # replace with Port objects, eventually ?
91
        self.ips = {} # dict of interfaces to ip addresses as strings
92
        self.connection = {} # remote node connected to each interface
93
        self.waiting = False
94
        self.execed = False
95
        self.defaultIP = defaultIP
96
        self.defaultMAC = defaultMAC
97

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

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

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

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

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

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

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

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

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

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

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

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

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

    
197
    # Interface management, configuration, and routing
198

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

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

    
209
    def newIntf( self ):
210
        "Reserve and return a new interface name."
211
        intfName = self.intfName( self.intfCount )
212
        self.intfCount += 1
213
        return intfName
214

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

    
227
    def connect( self, intf, dstNode, dstIntf ):
228
        "Register connection of intf to dstIntf on dstNode."
229
        self.connection[ intf ] = ( dstNode, dstIntf )
230

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

    
243
    def setMAC( self, intf, mac ):
244
        """Set the MAC address for an interface.
245
           mac: MAC address as string"""
246
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
247
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
248
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
249
        return result
250

    
251
    def setARP( self, ip, mac ):
252
        """Add an ARP entry.
253
           ip: IP address as string
254
           mac: MAC address as string"""
255
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
256
        return result
257

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

    
268
    def setHostRoute( self, ip, intf ):
269
        """Add route to host.
270
           ip: IP address as dotted decimal
271
           intf: string, interface name"""
272
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
273

    
274
    def setDefaultRoute( self, intf ):
275
        """Set the default route to go through intf.
276
           intf: string, interface name"""
277
        self.cmd( 'ip route flush' )
278
        return self.cmd( 'route add default ' + intf )
279

    
280
    def IP( self ):
281
        "Return IP address of interface 0"
282
        return self.ips.get( self.intfs.get( 0 , None ), None )
283

    
284
    def MAC( self ):
285
        "Return MAC address of interface 0"
286
        ifconfig = self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
287
        macs = re.findall( '..:..:..:..:..:..', ifconfig )
288
        if len( macs ) > 0:
289
            return macs[ 0 ]
290
        else:
291
            return None
292

    
293
    def intfIsUp( self, port ):
294
        """Check if interface for a given port number is up.
295
           port: port number"""
296
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ port ] )
297

    
298
    # Other methods
299
    def __str__( self ):
300
        result = self.name + ':'
301
        result += ' IP=' + str( self.IP() )
302
        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
303
        result += ' waiting=' + str( self.waiting )
304
        return result
305

    
306

    
307
class Host( Node ):
308
    "A host is simply a Node."
309
    pass
310

    
311

    
312
class Switch( Node ):
313
    """A Switch is a Node that is running (or has execed?)
314
       an OpenFlow switch."""
315

    
316
    def sendCmd( self, cmd ):
317
        """Send command to Node.
318
           cmd: string"""
319
        if not self.execed:
320
            return Node.sendCmd( self, cmd )
321
        else:
322
            error( '*** Error: %s has execed and cannot accept commands' %
323
                     self.name )
324

    
325
    def monitor( self ):
326
        "Monitor node."
327
        if not self.execed:
328
            return Node.monitor( self )
329
        else:
330
            return True, ''
331

    
332

    
333
class UserSwitch( Switch ):
334
    """User-space switch.
335
       Currently only works in the root namespace."""
336

    
337
    def __init__( self, name, *args, **kwargs ):
338
        """Init.
339
           name: name for the switch"""
340
        Switch.__init__( self, name, inNamespace=False, **kwargs )
341

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

    
352
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
353
            ' punix:/tmp/' + self.name +
354
            ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
355
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
356
            ' tcp:' + controller.IP() + ' --fail=closed' +
357
            ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
358

    
359
    def stop( self ):
360
        "Stop OpenFlow reference user datapath."
361
        self.cmd( 'kill %ofdatapath' )
362
        self.cmd( 'kill %ofprotocol' )
363
        self.deleteIntfs()
364

    
365
class KernelSwitch( Switch ):
366
    """Kernel-space switch.
367
       Currently only works in the root namespace."""
368

    
369
    def __init__( self, name, dp=None, **kwargs ):
370
        """Init.
371
           name:
372
           dp: netlink id (0, 1, 2, ...)
373
           defaultMAC: default MAC as string; random value if None"""
374
        Switch.__init__( self, name, inNamespace=False, **kwargs )
375
        self.dp = dp
376

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

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

    
403
    def stop( self ):
404
        "Terminate kernel datapath."
405
        quietRun( 'dpctl deldp nl:%i' % self.dp )
406
        self.cmd( 'kill %ofprotocol' )
407
        self.deleteIntfs()
408

    
409
class OVSKernelSwitch( Switch ):
410
    """Open VSwitch kernel-space switch.
411
       Currently only works in the root namespace."""
412

    
413
    def __init__( self, name, dp=None, **kwargs ):
414
        """Init.
415
           name:
416
           dp: netlink id (0, 1, 2, ...)
417
           dpid: datapath ID as unsigned int; random value if None"""
418
        Switch.__init__( self, name, inNamespace=False, **kwargs )
419
        self.dp = dp
420

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

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

    
447
    def stop( self ):
448
        "Terminate kernel datapath."
449
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
450
        self.cmd( 'kill %ovs-openflowd' )
451
        self.deleteIntfs()
452

    
453

    
454
class Controller( Node ):
455
    """A Controller is a Node that is running (or has execed?) an
456
       OpenFlow controller."""
457

    
458
    def __init__( self, name, inNamespace=False, controller='controller',
459
                 cargs='-v ptcp:', cdir=None, defaultIP="127.0.0.1",
460
                 port=6633 ):
461
        self.controller = controller
462
        self.cargs = cargs
463
        self.cdir = cdir
464
        self.port = port
465
        Node.__init__( self, name, inNamespace=inNamespace,
466
            defaultIP=defaultIP )
467

    
468
    def start( self ):
469
        """Start <controller> <args> on controller.
470
           Log to /tmp/cN.log"""
471
        cout = '/tmp/' + self.name + '.log'
472
        if self.cdir is not None:
473
            self.cmd( 'cd ' + self.cdir )
474
        self.cmd( self.controller + ' ' + self.cargs +
475
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
476
        self.execed = False
477

    
478
    def stop( self ):
479
        "Stop controller."
480
        self.cmd( 'kill %' + self.controller )
481
        self.terminate()
482

    
483
    def IP( self ):
484
        "Return IP address of the Controller"
485
        return self.defaultIP
486

    
487

    
488
class ControllerParams( object ):
489
    "Container for controller IP parameters."
490

    
491
    def __init__( self, ip, prefixLen ):
492
        """Init.
493
           ip: string, controller IP address
494
           prefixLen: prefix length, e.g. 8 for /8, covering 16M"""
495
        self.ip = ip
496
        self.prefixLen = prefixLen
497

    
498

    
499
class NOX( Controller ):
500
    "Controller to run a NOX application."
501

    
502
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
503
        """Init.
504
           name: name to give controller
505
           noxArgs: list of args, or single arg, to pass to NOX"""
506
        if type( noxArgs ) != list:
507
            noxArgs = [ noxArgs ]
508
        if not noxArgs:
509
            noxArgs = [ 'packetdump' ]
510

    
511
        if 'NOX_CORE_DIR' not in os.environ:
512
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
513
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
514

    
515
        Controller.__init__( self, name,
516
            controller=noxCoreDir + '/nox_core',
517
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' +
518
                    ' '.join( noxArgs ),
519
            cdir = noxCoreDir, **kwargs )
520

    
521

    
522
class RemoteController( Controller ):
523
    "Controller running outside of Mininet's control."
524

    
525
    def __init__( self, name, inNamespace=False, defaultIP='127.0.0.1',
526
                 port=6633 ):
527
        """Init.
528
           name: name to give controller
529
           ipAddress: the IP address where the remote controller is
530
           listening
531
           port: the port where the remote controller is listening"""
532
        Controller.__init__( self, name, defaultIP=defaultIP, port=port )
533

    
534
    def start( self ):
535
        "Overridden to do nothing."
536
        return
537

    
538
    def stop( self ):
539
        "Overridden to do nothing."
540
        return