Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ efc9a01c

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

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

    
46

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

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

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

    
83
    @classmethod
84
    def fdToNode( cls, fd ):
85
        """Return node corresponding to given file descriptor.
86
           fd: file descriptor
87
           returns: node"""
88
        node = Node.outToNode.get( fd )
89
        return node or Node.inToNode.get( fd )
90

    
91
    def cleanup( self ):
92
        "Help python collect its garbage."
93
        self.shell = None
94

    
95
    # Subshell I/O, commands and control
96
    def read( self, bytes ):
97
        """Read from a node.
98
           bytes: maximum number of bytes to read"""
99
        return os.read( self.stdout.fileno(), bytes )
100

    
101
    def write( self, data ):
102
        """Write data to node.
103
           data: string"""
104
        os.write( self.stdin.fileno(), data )
105

    
106
    def terminate( self ):
107
        "Send kill signal to Node and clean up after it."
108
        os.kill( self.pid, signal.SIGKILL )
109
        self.cleanup()
110

    
111
    def stop( self ):
112
        "Stop node."
113
        self.terminate()
114

    
115
    def waitReadable( self ):
116
        "Wait until node's output is readable."
117
        self.pollOut.poll()
118

    
119
    def sendCmd( self, cmd ):
120
        """Send a command, followed by a command to echo a sentinel,
121
           and return without waiting for the command to complete."""
122
        assert not self.waiting
123
        if cmd[ -1 ] == '&':
124
            separator = '&'
125
            cmd = cmd[ :-1 ]
126
        else:
127
            separator = ';'
128
        if isinstance( cmd, list ):
129
            cmd = ' '.join( cmd )
130
        self.write( cmd + separator + ' echo -n "\\0177" \n' )
131
        self.waiting = True
132

    
133
    def monitor( self ):
134
        "Monitor the output of a command, returning (done?, data)."
135
        assert self.waiting
136
        self.waitReadable()
137
        data = self.read( 1024 )
138
        if len( data ) > 0 and data[ -1 ] == chr( 0177 ):
139
            self.waiting = False
140
            return True, data[ :-1 ]
141
        else:
142
            return False, data
143

    
144
    def sendInt( self ):
145
        "Send ^C, hopefully interrupting an interactive subprocess."
146
        self.write( chr( 3 ) )
147

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

    
169
    def cmd( self, cmd, verbose=False ):
170
        """Send a command, wait for output, and return it.
171
           cmd: string"""
172
        log = info if verbose else debug
173
        log( '*** %s : %s', self.name, cmd )
174
        self.sendCmd( cmd )
175
        return self.waitOutput( verbose )
176

    
177
    def cmdPrint( self, cmd ):
178
        """Call cmd and printing its output
179
           cmd: string"""
180
        return self.cmd( cmd, verbose=True )
181

    
182
    # Interface management, configuration, and routing
183

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

    
190
    def intfName( self, n ):
191
        "Construct a canonical interface name node-ethN for interface n."
192
        return self.name + '-eth' + repr( n )
193

    
194
    def newIntf( self ):
195
        "Reserve and return a new interface name."
196
        intfName = self.intfName( self.intfCount )
197
        self.intfCount += 1
198
        return intfName
199

    
200
    def addIntf( self, intf, port ):
201
        """Add an interface.
202
           intf: interface name (nodeN-ethM)
203
           port: port number (typically OpenFlow port number)"""
204
        self.intfs[ port ] = intf
205
        self.ports[ intf ] = port
206
        #info( '\n' )
207
        #info( 'added intf %s to node %x\n' % ( srcIntf, src ) )
208
        if self.inNamespace:
209
            #info( 'moving w/inNamespace set\n' )
210
            moveIntf( intf, self )
211

    
212
    def connect( self, intf, dstNode, dstIntf ):
213
        "Register connection of intf to dstIntf on dstNode."
214
        self.connection[ intf ] = ( dstNode, dstIntf )
215

    
216
    def setMAC( self, intf, mac ):
217
        """Set the MAC address for an interface.
218
           mac: MAC address as string"""
219
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
220
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
221
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
222
        return result
223

    
224
    def setARP( self, ip, mac ):
225
        """Add an ARP entry.
226
           ip: IP address as string
227
           mac: MAC address as string"""
228
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
229
        return result
230

    
231
    def setIP( self, intf, ip, bits ):
232
        """Set the IP address for an interface.
233
           intf: interface name
234
           ip: IP address as a string
235
           bits: prefix length of form /24"""
236
        result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
237
        self.ips[ intf ] = ip
238
        return result
239

    
240
    def setHostRoute( self, ip, intf ):
241
        """Add route to host.
242
           ip: IP address as dotted decimal
243
           intf: string, interface name"""
244
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
245

    
246
    def setDefaultRoute( self, intf ):
247
        """Set the default route to go through intf.
248
           intf: string, interface name"""
249
        self.cmd( 'ip route flush' )
250
        return self.cmd( 'route add default ' + intf )
251

    
252
    def IP( self ):
253
        "Return IP address of interface 0"
254
        return self.ips.get( self.intfs.get( 0 , None ), None )
255

    
256
    def intfIsUp( self, port ):
257
        """Check if interface for a given port number is up.
258
           port: port number"""
259
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ port ] )
260

    
261
    # Other methods
262
    def __str__( self ):
263
        result = self.name + ':'
264
        result += ' IP=' + repr( self.IP() )
265
        result += ' intfs=' + ','.join( sorted( self.intfs.values() ) )
266
        result += ' waiting=' + repr( self.waiting )
267
        return result
268

    
269

    
270
class Host( Node ):
271
    "A host is simply a Node."
272
    pass
273

    
274

    
275
class Switch( Node ):
276
    """A Switch is a Node that is running (or has execed?)
277
       an OpenFlow switch."""
278

    
279
    def sendCmd( self, cmd ):
280
        """Send command to Node.
281
           cmd: string"""
282
        if not self.execed:
283
            return Node.sendCmd( self, cmd )
284
        else:
285
            error( '*** Error: %s has execed and cannot accept commands' %
286
                     self.name )
287

    
288
    def monitor( self ):
289
        "Monitor node."
290
        if not self.execed:
291
            return Node.monitor( self )
292
        else:
293
            return True, ''
294

    
295

    
296
class UserSwitch( Switch ):
297
    """User-space switch.
298
       Currently only works in the root namespace."""
299

    
300
    def __init__( self, name ):
301
        """Init.
302
           name: name for the switch"""
303
        Switch.__init__( self, name, inNamespace=False )
304

    
305
    def start( self, controllers ):
306
        """Start OpenFlow reference user datapath.
307
           Log to /tmp/sN-{ofd,ofp}.log.
308
           controllers: list of controller objects"""
309
        controller = controllers[ 0 ]
310
        ofdlog = '/tmp/' + self.name + '-ofd.log'
311
        ofplog = '/tmp/' + self.name + '-ofp.log'
312
        self.cmd( 'ifconfig lo up' )
313
        intfs = sorted( self.intfs.values() )
314

    
315
        self.cmd( 'ofdatapath -i ' + ','.join( intfs ) +
316
            ' punix:/tmp/' + self.name +
317
            ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
318
        self.cmd( 'ofprotocol unix:/tmp/' + self.name +
319
            ' tcp:' + controller.IP() + ' --fail=closed' +
320
            ' 1> ' + ofplog + ' 2>' + ofplog + ' &' )
321

    
322
    def stop( self ):
323
        "Stop OpenFlow reference user datapath."
324
        self.cmd( 'kill %ofdatapath' )
325
        self.cmd( 'kill %ofprotocol' )
326

    
327

    
328
class KernelSwitch( Switch ):
329
    """Kernel-space switch.
330
       Currently only works in the root namespace."""
331

    
332
    def __init__( self, name, dp=None, defaultMac=None ):
333
        """Init.
334
           name:
335
           dp: netlink id (0, 1, 2, ...)
336
           defaultMac: default MAC as string; random value if None"""
337
        Switch.__init__( self, name, inNamespace=False )
338
        self.dp = dp
339
        self.defaultMac = defaultMac
340

    
341
    def start( self, controllers ):
342
        "Start up reference kernel datapath."
343
        ofplog = '/tmp/' + self.name + '-ofp.log'
344
        quietRun( 'ifconfig lo up' )
345
        # Delete local datapath if it exists;
346
        # then create a new one monitoring the given interfaces
347
        quietRun( 'dpctl deldp nl:%i' % self.dp )
348
        self.cmd( 'dpctl adddp nl:%i' % self.dp )
349
        if self.defaultMac:
350
            intf = 'of%i' % self.dp
351
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', self.defaultMac ] )
352

    
353
        if len( self.intfs ) != max( self.intfs ) + 1:
354
            raise Exception( 'only contiguous, zero-indexed port ranges'
355
                            'supported: %s' % self.intfs )
356
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
357
        self.cmd( 'dpctl addif nl:' + str( self.dp ) + ' ' +
358
            ' '.join( intfs ) )
359
        # Run protocol daemon
360
        controller = controllers[ 0 ]
361
        self.cmd( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
362
                      controller.IP() + ':' +
363
                      str( controller.port ) +
364
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
365
        self.execed = False
366

    
367
    def stop( self ):
368
        "Terminate kernel datapath."
369
        quietRun( 'dpctl deldp nl:%i' % self.dp )
370
        # In theory the interfaces should go away after we shut down.
371
        # However, this takes time, so we're better off removing them
372
        # explicitly so that we won't get errors if we run before they
373
        # have been removed by the kernel. Unfortunately this is very slow.
374
        self.cmd( 'kill %ofprotocol' )
375
        for intf in self.intfs.values():
376
            quietRun( 'ip link del ' + intf )
377
            info( '.' )
378

    
379

    
380
class OVSKernelSwitch( Switch ):
381
    """Open VSwitch kernel-space switch.
382
       Currently only works in the root namespace."""
383

    
384
    def __init__( self, name, dp=None, defaultMac=None ):
385
        """Init.
386
           name:
387
           dp: netlink id (0, 1, 2, ...)
388
           dpid: datapath ID as unsigned int; random value if None"""
389
        Switch.__init__( self, name, inNamespace=False )
390
        self.dp = dp
391
        self.defaultMac = defaultMac
392

    
393
    def start( self, controllers ):
394
        "Start up kernel datapath."
395
        ofplog = '/tmp/' + self.name + '-ofp.log'
396
        quietRun( 'ifconfig lo up' )
397
        # Delete local datapath if it exists;
398
        # then create a new one monitoring the given interfaces
399
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
400
        self.cmd( 'ovs-dpctl add-dp dp%i' % self.dp )
401
        if self.defaultMac:
402
            intf = 'dp' % self.dp
403
            mac = self.defaultMac
404
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', mac ] )
405

    
406
        if len( self.intfs ) != max( self.intfs ) + 1:
407
            raise Exception( 'only contiguous, zero-indexed port ranges'
408
                            'supported: %s' % self.intfs )
409
        intfs = [ self.intfs[ port ] for port in sorted( self.intfs.keys() ) ]
410
        self.cmd( 'ovs-dpctl add-if dp' + str( self.dp ) + ' ' +
411
                      ' '.join( intfs ) )
412
        # Run protocol daemon
413
        controller = controllers[ 0 ]
414
        self.cmd( 'ovs-openflowd dp' + str( self.dp ) + ' tcp:' +
415
                      controller.IP() + ':' +
416
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
417
        self.execed = False
418

    
419
    def stop( self ):
420
        "Terminate kernel datapath."
421
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
422
        # In theory the interfaces should go away after we shut down.
423
        # However, this takes time, so we're better off removing them
424
        # explicitly so that we won't get errors if we run before they
425
        # have been removed by the kernel. Unfortunately this is very slow.
426
        self.cmd( 'kill %ovs-openflowd' )
427
        for intf in self.intfs.values():
428
            quietRun( 'ip link del ' + intf )
429
            info( '.' )
430

    
431

    
432
class Controller( Node ):
433
    """A Controller is a Node that is running (or has execed?) an
434
       OpenFlow controller."""
435

    
436
    def __init__( self, name, inNamespace=False, controller='controller',
437
                 cargs='-v ptcp:', cdir=None, ipAddress="127.0.0.1",
438
                 port=6633 ):
439
        self.controller = controller
440
        self.cargs = cargs
441
        self.cdir = cdir
442
        self.ipAddress = ipAddress
443
        self.port = port
444
        Node.__init__( self, name, inNamespace=inNamespace )
445

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

    
456
    def stop( self ):
457
        "Stop controller."
458
        self.cmd( 'kill %' + self.controller )
459
        self.terminate()
460

    
461
    def IP( self ):
462
        "Return IP address of the Controller"
463
        return self.ipAddress
464

    
465

    
466
class ControllerParams( object ):
467
    "Container for controller IP parameters."
468

    
469
    def __init__( self, ip, subnetSize ):
470
        """Init.
471
           ip: integer, controller IP
472
            subnetSize: integer, ex 8 for slash-8, covering 17M"""
473
        self.ip = ip
474
        self.subnetSize = subnetSize
475

    
476

    
477
class NOX( Controller ):
478
    "Controller to run a NOX application."
479

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

    
489
        if 'NOX_CORE_DIR' not in os.environ:
490
            exit( 'exiting; please set missing NOX_CORE_DIR env var' )
491
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
492

    
493
        Controller.__init__( self, name,
494
            controller=noxCoreDir + '/nox_core',
495
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' +
496
                    ' '.join( noxArgs ),
497
            cdir = noxCoreDir, **kwargs )
498

    
499

    
500
class RemoteController( Controller ):
501
    "Controller running outside of Mininet's control."
502

    
503
    def __init__( self, name, inNamespace=False, ipAddress='127.0.0.1',
504
                 port=6633 ):
505
        """Init.
506
           name: name to give controller
507
           ipAddress: the IP address where the remote controller is
508
           listening
509
           port: the port where the remote controller is listening"""
510
        Controller.__init__( self, name, ipAddress=ipAddress, port=port )
511

    
512
    def start( self ):
513
        "Overridden to do nothing."
514
        return
515

    
516
    def stop( self ):
517
        "Overridden to do nothing."
518
        return