Statistics
| Branch: | Tag: | Revision:

mininet / mininet / node.py @ 281f6e59

History | View | Annotate | Download (15.8 KB)

1
#!/usr/bin/env python
2
"Node objects for Mininet."
3

    
4
from subprocess import Popen, PIPE, STDOUT
5
import os
6
import signal
7
import sys
8
import select
9

    
10
flush = sys.stdout.flush
11

    
12
from mininet.log import lg
13
from mininet.util import quietRun, macColonHex, ipStr
14

    
15

    
16
class Node( object ):
17
    """A virtual network node is simply a shell in a network namespace.
18
       We communicate with it using pipes."""
19
    inToNode = {}
20
    outToNode = {}
21

    
22
    def __init__( self, name, inNamespace=True ):
23
        self.name = name
24
        closeFds = False # speed vs. memory use
25
        # xpgEcho is needed so we can echo our sentinel in sendCmd
26
        cmd = [ '/bin/bash', '-O', 'xpg_echo' ]
27
        self.inNamespace = inNamespace
28
        if self.inNamespace:
29
            cmd = [ 'netns' ] + cmd
30
        self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
31
            close_fds=closeFds )
32
        self.stdin = self.shell.stdin
33
        self.stdout = self.shell.stdout
34
        self.pollOut = select.poll()
35
        self.pollOut.register( self.stdout )
36
        # Maintain mapping between file descriptors and nodes
37
        # This could be useful for monitoring multiple nodes
38
        # using select.poll()
39
        self.outToNode[ self.stdout.fileno() ] = self
40
        self.inToNode[ self.stdin.fileno() ] = self
41
        self.pid = self.shell.pid
42
        self.intfCount = 0
43
        self.intfs = [] # list of interface names, as strings
44
        self.ips = {} # dict of interfaces to ip addresses as strings
45
        self.connection = {}
46
        self.waiting = False
47
        self.execed = False
48
        self.ports = {} # dict of ints to interface strings
49
                        # replace with Port object, eventually
50

    
51
    def fdToNode( self, f ):
52
        """Insert docstring.
53
           f: unknown
54
           returns: bool unknown"""
55
        node = self.outToNode.get( f )
56
        return node or self.inToNode.get( f )
57

    
58
    def cleanup( self ):
59
        "Help python collect its garbage."
60
        self.shell = None
61

    
62
    # Subshell I/O, commands and control
63
    def read( self, filenoMax ):
64
        """Insert docstring.
65
           filenoMax: unknown"""
66
        return os.read( self.stdout.fileno(), filenoMax )
67

    
68
    def write( self, data ):
69
        """Write data to node.
70
           data: string"""
71
        os.write( self.stdin.fileno(), data )
72

    
73
    def terminate( self ):
74
        "Send kill signal to Node and cleanup after it."
75
        os.kill( self.pid, signal.SIGKILL )
76
        self.cleanup()
77

    
78
    def stop( self ):
79
        "Stop node."
80
        self.terminate()
81

    
82
    def waitReadable( self ):
83
        "Poll on node."
84
        self.pollOut.poll()
85

    
86
    def sendCmd( self, cmd ):
87
        """Send a command, followed by a command to echo a sentinel,
88
           and return without waiting for the command to complete."""
89
        assert not self.waiting
90
        if cmd[ -1 ] == '&':
91
            separator = '&'
92
            cmd = cmd[ :-1 ]
93
        else:
94
            separator = ';'
95
        if isinstance( cmd, list ):
96
            cmd = ' '.join( cmd )
97
        self.write( cmd + separator + ' echo -n "\\0177" \n' )
98
        self.waiting = True
99

    
100
    def monitor( self ):
101
        "Monitor the output of a command, returning (done, data)."
102
        assert self.waiting
103
        self.waitReadable()
104
        data = self.read( 1024 )
105
        if len( data ) > 0 and data[ -1 ] == chr( 0177 ):
106
            self.waiting = False
107
            return True, data[ :-1 ]
108
        else:
109
            return False, data
110

    
111
    def sendInt( self ):
112
        "Send ^C, hopefully interrupting a running subprocess."
113
        self.write( chr( 3 ) )
114

    
115
    def waitOutput( self ):
116
        """Wait for a command to complete.
117
           Completion is signaled by a sentinel character, ASCII( 127 )
118
           appearing in the output stream.  Wait for the sentinel and return
119
           the output, including trailing newline."""
120
        assert self.waiting
121
        output = ''
122
        while True:
123
            self.waitReadable()
124
            data = self.read( 1024 )
125
            if len( data ) > 0  and data[ -1 ] == chr( 0177 ):
126
                output += data[ :-1 ]
127
                break
128
            else:
129
                output += data
130
        self.waiting = False
131
        return output
132

    
133
    def cmd( self, cmd ):
134
        """Send a command, wait for output, and return it.
135
           cmd: string"""
136
        self.sendCmd( cmd )
137
        return self.waitOutput()
138

    
139
    def cmdPrint( self, cmd ):
140
        """Call cmd and printing its output
141
           cmd: string"""
142
        #lg.info( '*** %s : %s', self.name, cmd )
143
        result = self.cmd( cmd )
144
        #lg.info( '%s\n', result )
145
        return result
146

    
147
    # Interface management, configuration, and routing
148
    def intfName( self, n ):
149
        "Construct a canonical interface name node-intf for interface N."
150
        return self.name + '-eth' + repr( n )
151

    
152
    def newIntf( self ):
153
        "Reserve and return a new interface name."
154
        intfName = self.intfName( self.intfCount )
155
        self.intfCount += 1
156
        self.intfs += [ intfName ]
157
        return intfName
158

    
159
    def setMAC( self, intf, mac ):
160
        """Set the MAC address for an interface.
161
           mac: MAC address as unsigned int"""
162
        macStr = macColonHex( mac )
163
        result = self.cmd( [ 'ifconfig', intf, 'down' ] )
164
        result += self.cmd( [ 'ifconfig', intf, 'hw', 'ether', macStr ] )
165
        result += self.cmd( [ 'ifconfig', intf, 'up' ] )
166
        return result
167

    
168
    def setARP( self, ip, mac ):
169
        """Add an ARP entry.
170
           ip: IP address as unsigned int
171
           mac: MAC address as unsigned int"""
172
        ip = ipStr( ip )
173
        mac = macColonHex( mac )
174
        result = self.cmd( [ 'arp', '-s', ip, mac ] )
175
        return result
176

    
177
    def setIP( self, intf, ip, bits ):
178
        """Set the IP address for an interface.
179
           intf: string, interface name
180
           ip: IP address as a string
181
           bits:"""
182
        result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
183
        self.ips[ intf ] = ip
184
        return result
185

    
186
    def setHostRoute( self, ip, intf ):
187
        """Add route to host.
188
           ip: IP address as dotted decimal
189
           intf: string, interface name"""
190
        return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
191

    
192
    def setDefaultRoute( self, intf ):
193
        """Set the default route to go through intf.
194
           intf: string, interface name"""
195
        self.cmd( 'ip route flush' )
196
        return self.cmd( 'route add default ' + intf )
197

    
198
    def IP( self ):
199
        "Return IP address of first interface"
200
        if len( self.intfs ) > 0:
201
            return self.ips.get( self.intfs[ 0 ], None )
202

    
203
    def intfIsUp( self ):
204
        "Check if one of our interfaces is up."
205
        return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
206

    
207
    # Other methods
208
    def __str__( self ):
209
        result = self.name + ':'
210
        if self.IP():
211
            result += ' IP=' + self.IP()
212
        result += ' intfs=' + ','.join( self.intfs )
213
        result += ' waiting=' + repr( self.waiting )
214
        return result
215

    
216

    
217
class Host( Node ):
218
    "A host is simply a Node."
219
    pass
220

    
221

    
222
class Switch( Node ):
223
    """A Switch is a Node that is running ( or has execed )
224
       an OpenFlow switch."""
225

    
226
    def sendCmd( self, cmd ):
227
        """Send command to Node.
228
           cmd: string"""
229
        if not self.execed:
230
            return Node.sendCmd( self, cmd )
231
        else:
232
            lg.error( '*** Error: %s has execed and cannot accept commands' %
233
                     self.name )
234

    
235
    def monitor( self ):
236
        "Monitor node."
237
        if not self.execed:
238
            return Node.monitor( self )
239
        else:
240
            return True, ''
241

    
242

    
243
class UserSwitch( Switch ):
244
    """User-space switch.
245
       Currently only works in the root namespace."""
246

    
247
    def __init__( self, name ):
248
        """Init.
249
           name: name for the switch"""
250
        Switch.__init__( self, name, inNamespace=False )
251

    
252
    def start( self, controllers ):
253
        """Start OpenFlow reference user datapath.
254
           Log to /tmp/sN-{ ofd,ofp }.log.
255
           controllers: dict of controller names to objects"""
256
        if 'c0' not in controllers:
257
            raise Exception( 'User datapath start() requires controller c0' )
258
        controller = controllers[ 'c0' ]
259
        ofdlog = '/tmp/' + self.name + '-ofd.log'
260
        ofplog = '/tmp/' + self.name + '-ofp.log'
261
        self.cmd( 'ifconfig lo up' )
262
        intfs = self.intfs
263
        self.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) + ' punix:/tmp/' +
264
                      self.name + ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' )
265
        self.cmdPrint( 'ofprotocol unix:/tmp/' + self.name + ' tcp:' +
266
                      controller.IP() + ' --fail=closed 1> ' + ofplog + ' 2>' +
267
                      ofplog + ' &' )
268

    
269
    def stop( self ):
270
        "Stop OpenFlow reference user datapath."
271
        self.cmd( 'kill %ofdatapath' )
272
        self.cmd( 'kill %ofprotocol' )
273

    
274

    
275
class KernelSwitch( Switch ):
276
    """Kernel-space switch.
277
       Currently only works in the root namespace."""
278

    
279
    def __init__( self, name, dp=None, dpid=None ):
280
        """Init.
281
           name:
282
           dp: netlink id ( 0, 1, 2, ... )
283
           dpid: datapath ID as unsigned int; random value if None"""
284
        Switch.__init__( self, name, inNamespace=False )
285
        self.dp = dp
286
        self.dpid = dpid
287

    
288
    def start( self, controllers ):
289
        "Start up reference kernel datapath."
290
        ofplog = '/tmp/' + self.name + '-ofp.log'
291
        quietRun( 'ifconfig lo up' )
292
        # Delete local datapath if it exists;
293
        # then create a new one monitoring the given interfaces
294
        quietRun( 'dpctl deldp nl:%i' % self.dp )
295
        self.cmdPrint( 'dpctl adddp nl:%i' % self.dp )
296
        if self.dpid:
297
            intf = 'of%i' % self.dp
298
            macStr = macColonHex( self.dpid )
299
            self.cmd( [ 'ifconfig', intf, 'hw', 'ether', macStr ] )
300

    
301
        if len( self.ports ) != max( self.ports.keys() ) + 1:
302
            raise Exception( 'only contiguous, zero-indexed port ranges'
303
                            'supported: %s' % self.ports )
304
        intfs = [ self.ports[ port ] for port in self.ports.keys() ]
305
        self.cmdPrint( 'dpctl addif nl:' + str( self.dp ) + ' ' +
306
            ' '.join( intfs ) )
307
        # Run protocol daemon
308
        self.cmdPrint( 'ofprotocol nl:' + str( self.dp ) + ' tcp:' +
309
                      controllers[ 'c0' ].IP() + ':' +
310
                      str( controllers[ 'c0' ].port ) +
311
                      ' --fail=closed 1> ' + ofplog + ' 2>' + ofplog + ' &' )
312
        self.execed = False
313

    
314
    def stop( self ):
315
        "Terminate kernel datapath."
316
        quietRun( 'dpctl deldp nl:%i' % self.dp )
317
        # In theory the interfaces should go away after we shut down.
318
        # However, this takes time, so we're better off to remove them
319
        # explicitly so that we won't get errors if we run before they
320
        # have been removed by the kernel. Unfortunately this is very slow.
321
        self.cmd( 'kill %ofprotocol' )
322
        for intf in self.intfs:
323
            quietRun( 'ip link del ' + intf )
324
            lg.info( '.' )
325

    
326

    
327
class OVSKernelSwitch( Switch ):
328
    """Open VSwitch kernel-space switch.
329
       Currently only works in the root namespace."""
330

    
331
    def __init__( self, name, dp=None, dpid=None ):
332
        """Init.
333
           name:
334
           dp: netlink id ( 0, 1, 2, ... )
335
           dpid: datapath ID as unsigned int; random value if None"""
336
        Switch.__init__( self, name, inNamespace=False )
337
        self.dp = dp
338
        self.dpid = dpid
339

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

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

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

    
377

    
378
class Controller( Node ):
379
    """A Controller is a Node that is running ( or has execed ) an
380
       OpenFlow controller."""
381

    
382
    def __init__( self, name, inNamespace=False, controller='controller',
383
                 cargs='-v ptcp:', cdir=None, ipAddress="127.0.0.1",
384
                 port=6633 ):
385
        self.controller = controller
386
        self.cargs = cargs
387
        self.cdir = cdir
388
        self.ipAddress = ipAddress
389
        self.port = port
390
        Node.__init__( self, name, inNamespace=inNamespace )
391

    
392
    def start( self ):
393
        """Start <controller> <args> on controller.
394
           Log to /tmp/cN.log"""
395
        cout = '/tmp/' + self.name + '.log'
396
        if self.cdir is not None:
397
            self.cmdPrint( 'cd ' + self.cdir )
398
        self.cmdPrint( self.controller + ' ' + self.cargs +
399
            ' 1> ' + cout + ' 2> ' + cout + ' &' )
400
        self.execed = False
401

    
402
    def stop( self ):
403
        "Stop controller."
404
        self.cmd( 'kill %' + self.controller )
405
        self.terminate()
406

    
407
    def IP( self ):
408
        "Return IP address of the Controller"
409
        return self.ipAddress
410

    
411

    
412
class ControllerParams( object ):
413
    "Container for controller IP parameters."
414

    
415
    def __init__( self, ip, subnetSize ):
416
        """Init.
417
           ip: integer, controller IP
418
            subnetSize: integer, ex 8 for slash-8, covering 17M"""
419
        self.ip = ip
420
        self.subnetSize = subnetSize
421

    
422

    
423
class NOX( Controller ):
424
    "Controller to run a NOX application."
425

    
426
    def __init__( self, name, inNamespace=False, noxArgs=None, **kwargs ):
427
        """Init.
428
           name: name to give controller
429
           noxArgs: list of args, or single arg, to pass to NOX"""
430
        if type( noxArgs ) != list:
431
            noxArgs = [ noxArgs ]
432
        if not noxArgs:
433
            noxArgs = [ 'packetdump' ]
434
        noxCoreDir = os.environ[ 'NOX_CORE_DIR' ]
435
        if not noxCoreDir:
436
            raise Exception( 'please set NOX_CORE_DIR env var\n' )
437
        Controller.__init__( self, name,
438
            controller=noxCoreDir + '/nox_core',
439
            cargs='--libdir=/usr/local/lib -v -i ptcp: ' + \
440
                    ' '.join( noxArgs ),
441
            cdir = noxCoreDir, **kwargs )
442

    
443

    
444
class RemoteController( Controller ):
445
    "Controller running outside of Mininet's control."
446

    
447
    def __init__( self, name, inNamespace=False, ipAddress='127.0.0.1',
448
                 port=6633 ):
449
        """Init.
450
           name: name to give controller
451
           ipAddress: the IP address where the remote controller is
452
           listening
453
           port: the port where the remote controller is listening"""
454
        Controller.__init__( self, name, ipAddress=ipAddress, port=port )
455

    
456
    def start( self ):
457
        "Overridden to do nothing."
458
        return
459

    
460
    def stop( self ):
461
        "Overridden to do nothing."
462
        return