Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 086ef80e

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

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

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

    
47

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
190
    # Interface management, configuration, and routing
191

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

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

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

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

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

    
224
    def deleteIntfs( self ):
225
        "Delete all of our interfaces."
226
        # In theory the interfaces should go away after we shut down.
227
        # However, this takes time, so we're better off removing them
228
        # explicitly so that we won't get errors if we run before they
229
        # have been removed by the kernel. Unfortunately this is very slow.
230
        self.cmd( 'kill %ofprotocol' )
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 intfIsUp( self, port ):
279
        """Check if interface for a given port number is up.
280
           port: port number"""
281
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ port ] )
282

    
283
    # Other methods
284
    def __str__( self ):
285
        result = self.name + ':'
286
        result += ' IP=' + str( self.IP() )
287
        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
288
        result += ' waiting=' + str( self.waiting )
289
        return result
290

    
291

    
292
class Host( Node ):
293
    "A host is simply a Node."
294
    pass
295

    
296

    
297
class Switch( Node ):
298
    """A Switch is a Node that is running (or has execed?)
299
       an OpenFlow switch."""
300

    
301
    def sendCmd( self, cmd ):
302
        """Send command to Node.
303
           cmd: string"""
304
        if not self.execed:
305
            return Node.sendCmd( self, cmd )
306
        else:
307
            error( '*** Error: %s has execed and cannot accept commands' %
308
                     self.name )
309

    
310
    def monitor( self ):
311
        "Monitor node."
312
        if not self.execed:
313
            return Node.monitor( self )
314
        else:
315
            return True, ''
316

    
317

    
318
class UserSwitch( Switch ):
319
    """User-space switch.
320
       Currently only works in the root namespace."""
321

    
322
    def __init__( self, name, *args, **kwargs ):
323
        """Init.
324
           name: name for the switch"""
325
        Switch.__init__( self, name, inNamespace=False, **kwargs )
326

    
327
    def start( self, controllers ):
328
        """Start OpenFlow reference user datapath.
329
           Log to /tmp/sN-{ofd,ofp}.log.
330
           controllers: list of controller objects"""
331
        controller = controllers[ 0 ]
332
        ofdlog = '/tmp/' + self.name + '-ofd.log'
333
        ofplog = '/tmp/' + self.name + '-ofp.log'
334
        self.cmd( 'ifconfig lo up' )
335
        intfs = sorted( self.intfs.values() )
336

    
337
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
338
            ' punix:/tmp/' + self.name +
339
            ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
340
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
341
            ' tcp:' + controller.IP() + ' --fail=closed' +
342
            ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
343

    
344
    def stop( self ):
345
        "Stop OpenFlow reference user datapath."
346
        self.cmd( 'kill %ofdatapath' )
347
        self.cmd( 'kill %ofprotocol' )
348
        self.deleteIntfs()
349

    
350
class KernelSwitch( Switch ):
351
    """Kernel-space switch.
352
       Currently only works in the root namespace."""
353

    
354
    def __init__( self, name, dp=None, **kwargs ):
355
        """Init.
356
           name:
357
           dp: netlink id (0, 1, 2, ...)
358
           defaultMAC: default MAC as string; random value if None"""
359
        Switch.__init__( self, name, inNamespace=False, **kwargs )
360
        self.dp = dp
361

    
362
    def start( self, controllers ):
363
        "Start up reference kernel datapath."
364
        ofplog = '/tmp/' + self.name + '-ofp.log'
365
        quietRun( 'ifconfig lo up' )
366
        # Delete local datapath if it exists;
367
        # then create a new one monitoring the given interfaces
368
        quietRun( 'dpctl deldp nl:%i' % self.dp )
369
        self.cmd( 'dpctl adddp nl:%i' % self.dp )
370
        if self.defaultMAC:
371
            intf = 'of%i' % self.dp
372
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', self.defaultMAC ] )
373

    
374
        if len( self.intfs ) != max( self.intfs ) + 1:
375
            raise Exception( 'only contiguous, zero-indexed port ranges'
376
                            'supported: %s' % self.intfs )
377
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
378
        self.cmd( 'dpctl addif nl:' + str( self.dp ) + ' ' +
379
            ' '.join( intfs ) )
380
        # Run protocol daemon
381
        controller = controllers[ 0 ]
382
        self.cmd( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
383
                      controller.IP() + ':' +
384
                      str( controller.port ) +
385
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
386
        self.execed = False
387

    
388
    def stop( self ):
389
        "Terminate kernel datapath."
390
        quietRun( 'dpctl deldp nl:%i' % self.dp )
391
        self.deleteIntfs()
392

    
393
class OVSKernelSwitch( Switch ):
394
    """Open VSwitch kernel-space switch.
395
       Currently only works in the root namespace."""
396

    
397
    def __init__( self, name, dp=None, **kwargs ):
398
        """Init.
399
           name:
400
           dp: netlink id (0, 1, 2, ...)
401
           dpid: datapath ID as unsigned int; random value if None"""
402
        Switch.__init__( self, name, inNamespace=False, **kwargs )
403
        self.dp = dp
404

    
405
    def start( self, controllers ):
406
        "Start up kernel datapath."
407
        ofplog = '/tmp/' + self.name + '-ofp.log'
408
        quietRun( 'ifconfig lo up' )
409
        # Delete local datapath if it exists;
410
        # then create a new one monitoring the given interfaces
411
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
412
        self.cmd( 'ovs-dpctl add-dp dp%i' % self.dp )
413
        if self.defaultMAC:
414
            intf = 'dp' % self.dp
415
            mac = self.defaultMAC
416
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
417

    
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( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
423
                      ' '.join( intfs ) )
424
        # Run protocol daemon
425
        controller = controllers[ 0 ]
426
        self.cmd( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
427
                      controller.IP() + ':' +
428
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
429
        self.execed = False
430

    
431
    def stop( self ):
432
        "Terminate kernel datapath."
433
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
434
        self.deleteIntfs()
435

    
436

    
437
class Controller( Node ):
438
    """A Controller is a Node that is running (or has execed?) an
439
       OpenFlow controller."""
440

    
441
    def __init__( self, name, inNamespace=False, controller='controller',
442
                 cargs='-v ptcp:', cdir=None, defaultIP="127.0.0.1",
443
                 port=6633 ):
444
        self.controller = controller
445
        self.cargs = cargs
446
        self.cdir = cdir
447
        self.port = port
448
        Node.__init__( self, name, inNamespace=inNamespace,
449
            defaultIP=defaultIP )
450

    
451
    def start( self ):
452
        """Start <controller> <args> on controller.
453
           Log to /tmp/cN.log"""
454
        cout = '/tmp/' + self.name + '.log'
455
        if self.cdir is not None:
456
            self.cmd( 'cd ' + self.cdir )
457
        self.cmd( self.controller + ' ' + self.cargs +
458
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
459
        self.execed = False
460

    
461
    def stop( self ):
462
        "Stop controller."
463
        self.cmd( 'kill %' + self.controller )
464
        self.terminate()
465

    
466
    def IP( self ):
467
        "Return IP address of the Controller"
468
        return self.defaultIP
469

    
470

    
471
class ControllerParams( object ):
472
    "Container for controller IP parameters."
473

    
474
    def __init__( self, ip, prefixLen ):
475
        """Init.
476
           ip: string, controller IP address
477
           prefixLen: prefix length, e.g. 8 for /8, covering 16M"""
478
        self.ip = ip
479
        self.prefixLen = prefixLen
480

    
481

    
482
class NOX( Controller ):
483
    "Controller to run a NOX application."
484

    
485
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
486
        """Init.
487
           name: name to give controller
488
           noxArgs: list of args, or single arg, to pass to NOX"""
489
        if type( noxArgs ) != list:
490
            noxArgs = [ noxArgs ]
491
        if not noxArgs:
492
            noxArgs = [ 'packetdump' ]
493

    
494
        if 'NOX_CORE_DIR' not in os.environ:
495
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
496
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
497

    
498
        Controller.__init__( self, name,
499
            controller=noxCoreDir + '/nox_core',
500
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' +
501
                    ' '.join( noxArgs ),
502
            cdir = noxCoreDir, **kwargs )
503

    
504

    
505
class RemoteController( Controller ):
506
    "Controller running outside of Mininet's control."
507

    
508
    def __init__( self, name, inNamespace=False, defaultIP='127.0.0.1',
509
                 port=6633 ):
510
        """Init.
511
           name: name to give controller
512
           ipAddress: the IP address where the remote controller is
513
           listening
514
           port: the port where the remote controller is listening"""
515
        Controller.__init__( self, name, defaultIP=defaultIP, port=port )
516

    
517
    def start( self ):
518
        "Overridden to do nothing."
519
        return
520

    
521
    def stop( self ):
522
        "Overridden to do nothing."
523
        return