Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 7d4b7b7f

History | View | Annotate | Download (16.6 KB)

1
#!/usr/bin/env python
2

    
3
"""
4
Node objects for Mininet.
5

6
Nodes provide a simple abstraction for interacting with
7
hosts, switches and controllers. Local nodes are simply
8
one or more processes on the local machine.
9

10
Node: superclass for all (currently only local) network nodes.
11

12
Host: a virtual host.
13

14
Switch: superclass for switch nodes.
15

16
UserSwitch: a switch using the user-space switch from the OpenFlow
17
    reference implementation.
18

19
KernelSwitch: a switch using the kernel switch from the OpenFlow reference
20
    implementation.
21

22
OVSSwitch: a switch using the OpenVSwitch OpenFlow-compatible switch
23
    implementation (openvswitch.org).
24

25
Controller: superclass for OpenFlow controllers. The default controller
26
    is controller(8) from the reference implementation.
27

28
NOXController: a controller node using NOX (noxrepo.org).
29

30
RemoteController: a remote controller node, which may use any
31
    arbitrary OpenFlow-compatible controller.
32
"""
33

    
34
from subprocess import Popen, PIPE, STDOUT
35
import os
36
import signal
37
import sys
38
import select
39

    
40
flush = sys.stdout.flush
41

    
42
from mininet.log import lg
43
from mininet.util import quietRun, macColonHex, ipStr
44

    
45

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

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

    
81
    def fdToNode( self, f ):
82
        """Insert docstring.
83
           f: unknown
84
           returns: bool unknown"""
85
        node = self.outToNode.get( f )
86
        return node or self.inToNode.get( f )
87

    
88
    def cleanup( self ):
89
        "Help python collect its garbage."
90
        self.shell = None
91

    
92
    # Subshell I/O, commands and control
93
    def read( self, filenoMax ):
94
        """Insert docstring.
95
           filenoMax: unknown"""
96
        return os.read( self.stdout.fileno(), filenoMax )
97

    
98
    def write( self, data ):
99
        """Write data to node.
100
           data: string"""
101
        os.write( self.stdin.fileno(), data )
102

    
103
    def terminate( self ):
104
        "Send kill signal to Node and cleanup after it."
105
        os.kill( self.pid, signal.SIGKILL )
106
        self.cleanup()
107

    
108
    def stop( self ):
109
        "Stop node."
110
        self.terminate()
111

    
112
    def waitReadable( self ):
113
        "Poll on node."
114
        self.pollOut.poll()
115

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

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

    
141
    def sendInt( self ):
142
        "Send ^C, hopefully interrupting a running subprocess."
143
        self.write( chr( 3 ) )
144

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

    
163
    def cmd( self, cmd ):
164
        """Send a command, wait for output, and return it.
165
           cmd: string"""
166
        self.sendCmd( cmd )
167
        return self.waitOutput()
168

    
169
    def cmdPrint( self, cmd ):
170
        """Call cmd and printing its output
171
           cmd: string"""
172
        #lg.info( '*** %s : %s', self.name, cmd )
173
        result = self.cmd( cmd )
174
        #lg.info( '%s\n', result )
175
        return result
176

    
177
    # Interface management, configuration, and routing
178
    def intfName( self, n ):
179
        "Construct a canonical interface name node-intf for interface N."
180
        return self.name + '-eth' + repr( n )
181

    
182
    def newIntf( self ):
183
        "Reserve and return a new interface name."
184
        intfName = self.intfName( self.intfCount )
185
        self.intfCount += 1
186
        self.intfs += [ intfName ]
187
        return intfName
188

    
189
    def setMAC( self, intf, mac ):
190
        """Set the MAC address for an interface.
191
           mac: MAC address as unsigned int"""
192
        macStr = macColonHex( mac )
193
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
194
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', macStr ] )
195
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
196
        return result
197

    
198
    def setARP( self, ip, mac ):
199
        """Add an ARP entry.
200
           ip: IP address as unsigned int
201
           mac: MAC address as unsigned int"""
202
        ip = ipStr( ip )
203
        mac = macColonHex( mac )
204
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
205
        return result
206

    
207
    def setIP( self, intf, ip, bits ):
208
        """Set the IP address for an interface.
209
           intf: string, interface name
210
           ip: IP address as a string
211
           bits:"""
212
        result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
213
        self.ips[ intf ] = ip
214
        return result
215

    
216
    def setHostRoute( self, ip, intf ):
217
        """Add route to host.
218
           ip: IP address as dotted decimal
219
           intf: string, interface name"""
220
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
221

    
222
    def setDefaultRoute( self, intf ):
223
        """Set the default route to go through intf.
224
           intf: string, interface name"""
225
        self.cmd( 'ip route flush' )
226
        return self.cmd( 'route add default ' + intf )
227

    
228
    def IP( self ):
229
        "Return IP address of first interface"
230
        if len( self.intfs ) > 0:
231
            return self.ips.get( self.intfs[ 0 ], None )
232

    
233
    def intfIsUp( self ):
234
        "Check if one of our interfaces is up."
235
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
236

    
237
    # Other methods
238
    def __str__( self ):
239
        result = self.name + ':'
240
        if self.IP():
241
            result += ' IP=' + self.IP()
242
        result += ' intfs=' + ','.join( self.intfs )
243
        result += ' waiting=' + repr( self.waiting )
244
        return result
245

    
246

    
247
class Host( Node ):
248
    "A host is simply a Node."
249
    pass
250

    
251

    
252
class Switch( Node ):
253
    """A Switch is a Node that is running ( or has execed )
254
       an OpenFlow switch."""
255

    
256
    def sendCmd( self, cmd ):
257
        """Send command to Node.
258
           cmd: string"""
259
        if not self.execed:
260
            return Node.sendCmd( self, cmd )
261
        else:
262
            lg.error( '*** Error: %s has execed and cannot accept commands' %
263
                     self.name )
264

    
265
    def monitor( self ):
266
        "Monitor node."
267
        if not self.execed:
268
            return Node.monitor( self )
269
        else:
270
            return True, ''
271

    
272

    
273
class UserSwitch( Switch ):
274
    """User-space switch.
275
       Currently only works in the root namespace."""
276

    
277
    def __init__( self, name ):
278
        """Init.
279
           name: name for the switch"""
280
        Switch.__init__( self, name, inNamespace=False )
281

    
282
    def start( self, controllers ):
283
        """Start OpenFlow reference user datapath.
284
           Log to /tmp/sN-{ ofd,ofp }.log.
285
           controllers: dict of controller names to objects"""
286
        if 'c0' not in controllers:
287
            raise Exception( 'User datapath start() requires controller c0' )
288
        controller = controllers[ 'c0' ]
289
        ofdlog = '/tmp/' + self.name + '-ofd.log'
290
        ofplog = '/tmp/' + self.name + '-ofp.log'
291
        self.cmd( 'ifconfig lo up' )
292
        intfs = self.intfs
293
        self.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) + ' punix:/tmp/' +
294
                      self.name + ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
295
        self.cmdPrint( 'ofprotocol unix:/tmp/' + self.name + ' tcp:' +
296
                      controller.IP() + ' --fail=closed 1> ' + ofplog + ' 2>' +
297
                      ofplog + ' &' )
298

    
299
    def stop( self ):
300
        "Stop OpenFlow reference user datapath."
301
        self.cmd( 'kill %ofdatapath' )
302
        self.cmd( 'kill %ofprotocol' )
303

    
304

    
305
class KernelSwitch( Switch ):
306
    """Kernel-space switch.
307
       Currently only works in the root namespace."""
308

    
309
    def __init__( self, name, dp=None, dpid=None ):
310
        """Init.
311
           name:
312
           dp: netlink id ( 0, 1, 2, ... )
313
           dpid: datapath ID as unsigned int; random value if None"""
314
        Switch.__init__( self, name, inNamespace=False )
315
        self.dp = dp
316
        self.dpid = dpid
317

    
318
    def start( self, controllers ):
319
        "Start up reference kernel datapath."
320
        ofplog = '/tmp/' + self.name + '-ofp.log'
321
        quietRun( 'ifconfig lo up' )
322
        # Delete local datapath if it exists;
323
        # then create a new one monitoring the given interfaces
324
        quietRun( 'dpctl deldp nl:%i' % self.dp )
325
        self.cmdPrint( 'dpctl adddp nl:%i' % self.dp )
326
        if self.dpid:
327
            intf = 'of%i' % self.dp
328
            macStr = macColonHex( self.dpid )
329
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', macStr ] )
330

    
331
        if len( self.ports ) != max( self.ports.keys() ) + 1:
332
            raise Exception( 'only contiguous, zero-indexed port ranges'
333
                            'supported: %s' % self.ports )
334
        intfs = [ self.ports[ port ] for port in self.ports.keys() ]
335
        self.cmdPrint( 'dpctl addif nl:' + str( self.dp ) + ' ' +
336
            ' '.join( intfs ) )
337
        # Run protocol daemon
338
        self.cmdPrint( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
339
                      controllers[ 'c0' ].IP() + ':' +
340
                      str( controllers[ 'c0' ].port ) +
341
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
342
        self.execed = False
343

    
344
    def stop( self ):
345
        "Terminate kernel datapath."
346
        quietRun( 'dpctl deldp nl:%i' % self.dp )
347
        # In theory the interfaces should go away after we shut down.
348
        # However, this takes time, so we're better off to remove them
349
        # explicitly so that we won't get errors if we run before they
350
        # have been removed by the kernel. Unfortunately this is very slow.
351
        self.cmd( 'kill %ofprotocol' )
352
        for intf in self.intfs:
353
            quietRun( 'ip link del ' + intf )
354
            lg.info( '.' )
355

    
356

    
357
class OVSKernelSwitch( Switch ):
358
    """Open VSwitch kernel-space switch.
359
       Currently only works in the root namespace."""
360

    
361
    def __init__( self, name, dp=None, dpid=None ):
362
        """Init.
363
           name:
364
           dp: netlink id ( 0, 1, 2, ... )
365
           dpid: datapath ID as unsigned int; random value if None"""
366
        Switch.__init__( self, name, inNamespace=False )
367
        self.dp = dp
368
        self.dpid = dpid
369

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

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

    
395
    def stop( self ):
396
        "Terminate kernel datapath."
397
        quietRun( 'ovs-dpctl del-dp dp%i' % self.dp )
398
        # In theory the interfaces should go away after we shut down.
399
        # However, this takes time, so we're better off to remove them
400
        # explicitly so that we won't get errors if we run before they
401
        # have been removed by the kernel. Unfortunately this is very slow.
402
        self.cmd( 'kill %ovs-openflowd' )
403
        for intf in self.intfs:
404
            quietRun( 'ip link del ' + intf )
405
            lg.info( '.' )
406

    
407

    
408
class Controller( Node ):
409
    """A Controller is a Node that is running ( or has execed ) an
410
       OpenFlow controller."""
411

    
412
    def __init__( self, name, inNamespace=False, controller='controller',
413
                 cargs='-v ptcp:', cdir=None, ipAddress="127.0.0.1",
414
                 port=6633 ):
415
        self.controller = controller
416
        self.cargs = cargs
417
        self.cdir = cdir
418
        self.ipAddress = ipAddress
419
        self.port = port
420
        Node.__init__( self, name, inNamespace=inNamespace )
421

    
422
    def start( self ):
423
        """Start <controller> <args> on controller.
424
           Log to /tmp/cN.log"""
425
        cout = '/tmp/' + self.name + '.log'
426
        if self.cdir is not None:
427
            self.cmdPrint( 'cd ' + self.cdir )
428
        self.cmdPrint( self.controller + ' ' + self.cargs +
429
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
430
        self.execed = False
431

    
432
    def stop( self ):
433
        "Stop controller."
434
        self.cmd( 'kill %' + self.controller )
435
        self.terminate()
436

    
437
    def IP( self ):
438
        "Return IP address of the Controller"
439
        return self.ipAddress
440

    
441

    
442
class ControllerParams( object ):
443
    "Container for controller IP parameters."
444

    
445
    def __init__( self, ip, subnetSize ):
446
        """Init.
447
           ip: integer, controller IP
448
            subnetSize: integer, ex 8 for slash-8, covering 17M"""
449
        self.ip = ip
450
        self.subnetSize = subnetSize
451

    
452

    
453
class NOX( Controller ):
454
    "Controller to run a NOX application."
455

    
456
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
457
        """Init.
458
           name: name to give controller
459
           noxArgs: list of args, or single arg, to pass to NOX"""
460
        if type( noxArgs ) != list:
461
            noxArgs = [ noxArgs ]
462
        if not noxArgs:
463
            noxArgs = [ 'packetdump' ]
464
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
465
        if not noxCoreDir:
466
            raise Exception( 'please set NOX_CORE_DIR env var\n' )
467
        Controller.__init__( self, name,
468
            controller=noxCoreDir + '/nox_core',
469
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' + \
470
                    ' '.join( noxArgs ),
471
            cdir = noxCoreDir, **kwargs )
472

    
473

    
474
class RemoteController( Controller ):
475
    "Controller running outside of Mininet's control."
476

    
477
    def __init__( self, name, inNamespace=False, ipAddress='127.0.0.1',
478
                 port=6633 ):
479
        """Init.
480
           name: name to give controller
481
           ipAddress: the IP address where the remote controller is
482
           listening
483
           port: the port where the remote controller is listening"""
484
        Controller.__init__( self, name, ipAddress=ipAddress, port=port )
485

    
486
    def start( self ):
487
        "Overridden to do nothing."
488
        return
489

    
490
    def stop( self ):
491
        "Overridden to do nothing."
492
        return