Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ dba3b599

History | View | Annotate | Download (19.1 KB)

1
"""
2

3
    Mininet: A simple networking testbed for OpenFlow!
4

5
author: Bob Lantz (rlantz@cs.stanford.edu)
6
author: Brandon Heller (brandonh@stanford.edu)
7

8
Mininet creates scalable OpenFlow test networks by using
9
process-based virtualization and network namespaces.
10

11
Simulated hosts are created as processes in separate network
12
namespaces. This allows a complete OpenFlow network to be simulated on
13
top of a single Linux kernel.
14

15
Each host has:
16

17
A virtual console (pipes to a shell)
18
A virtual interfaces (half of a veth pair)
19
A parent shell (and possibly some child processes) in a namespace
20

21
Hosts have a network interface which is configured via ifconfig/ip
22
link/etc.
23

24
This version supports both the kernel and user space datapaths
25
from the OpenFlow reference implementation.
26

27
In kernel datapath mode, the controller and switches are simply
28
processes in the root namespace.
29

30
Kernel OpenFlow datapaths are instantiated using dpctl(8), and are
31
attached to the one side of a veth pair; the other side resides in the
32
host namespace. In this mode, switch processes can simply connect to the
33
controller via the loopback interface.
34

35
In user datapath mode, the controller and switches can be full-service
36
nodes that live in their own network namespaces and have management
37
interfaces and IP addresses on a control network (e.g. 10.0.123.1,
38
currently routed although it could be bridged.)
39

40
In addition to a management interface, user mode switches also have
41
several switch interfaces, halves of veth pairs whose other halves
42
reside in the host nodes that the switches are connected to.
43

44
Consistent, straightforward naming is important in order to easily
45
identify hosts, switches and controllers, both from the CLI and
46
from program code. Interfaces are named to make it easy to identify
47
which interfaces belong to which node.
48

49
The basic naming scheme is as follows:
50

51
    Host nodes are named h1-hN
52
    Switch nodes are named s0-sN
53
    Controller nodes are named c0-cN
54
    Interfaces are named {nodename}-eth0 .. {nodename}-ethN
55

56
Currently we wrap the entire network in a 'mininet' object, which
57
constructs a simulated network based on a network topology created
58
using a topology object (e.g. LinearTopo) from topo.py and a Controller
59
node which the switches will connect to.  Several
60
configuration options are provided for functions such as
61
automatically setting MAC addresses, populating the ARP table, or
62
even running a set of xterms to allow direct interaction with nodes.
63

64
After the mininet is created, it can be started using start(), and a variety
65
of useful tasks maybe performed, including basic connectivity and
66
bandwidth tests and running the mininet CLI.
67

68
Once the network is up and running, test code can easily get access
69
to its host and switch objects, which can then be used
70
for arbitrary experiments, which typically involve running a series of
71
commands on the hosts.
72

73
After all desired tests or activities have been completed, the stop()
74
method may be called to shut down the network.
75

76
"""
77

    
78
import os
79
import re
80
import signal
81
from time import sleep
82

    
83
from mininet.cli import CLI
84
from mininet.log import info, error
85
from mininet.node import KernelSwitch, OVSKernelSwitch
86
from mininet.util import quietRun, fixLimits
87
from mininet.util import makeIntfPair, moveIntf, macColonHex
88
from mininet.xterm import cleanUpScreens, makeXterms
89

    
90
DATAPATHS = [ 'kernel' ] #[ 'user', 'kernel' ]
91

    
92
def init():
93
    "Initialize Mininet."
94
    if os.getuid() != 0:
95
        # Note: this script must be run as root
96
        # Perhaps we should do so automatically!
97
        print "*** Mininet must run as root."
98
        exit( 1 )
99
    # If which produces no output, then netns is not in the path.
100
    # May want to loosen this to handle netns in the current dir.
101
    if not quietRun( [ 'which', 'netns' ] ):
102
        raise Exception( "Could not find netns; see INSTALL" )
103
    fixLimits()
104

    
105
class Mininet( object ):
106
    "Network emulation with hosts spawned in network namespaces."
107

    
108
    def __init__( self, topo, switch, host, controller, cparams,
109
                 build=True, xterms=False, cleanup=False,
110
                 inNamespace=False,
111
                 autoSetMacs=False, autoStaticArp=False ):
112
        """Create Mininet object.
113
           topo: Topo (topology) object or None
114
           switch: Switch class
115
           host: Host class
116
           controller: Controller class
117
           cparams: ControllerParams object
118
           now: build now from topo?
119
           xterms: if build now, spawn xterms?
120
           cleanup: if build now, cleanup before creating?
121
           inNamespace: spawn switches and controller in net namespaces?
122
           autoSetMacs: set MAC addrs to DPIDs?
123
           autoStaticArp: set all-pairs static MAC addrs?"""
124
        self.switch = switch
125
        self.host = host
126
        self.controller = controller
127
        self.cparams = cparams
128
        self.topo = topo
129
        self.inNamespace = inNamespace
130
        self.xterms = xterms
131
        self.cleanup = cleanup
132
        self.autoSetMacs = autoSetMacs
133
        self.autoStaticArp = autoStaticArp
134

    
135
        self.hosts = []
136
        self.switches = []
137
        self.controllers = []
138
        self.nameToNode = {} # name to Node (Host/Switch) objects
139
        self.idToNode = {} # dpid to Node (Host/Switch) objects
140
        self.dps = 0 # number of created kernel datapaths
141
        self.terms = [] # list of spawned xterm processes
142

    
143
        if topo and build:
144
            self.buildFromTopo( self.topo )
145

    
146
    def addHost( self, name, defaultMac=None, defaultIp=None ):
147
        """Add host.
148
           name: name of host to add
149
           defaultMac: default MAC address for intf 0
150
           defaultIp: default IP address for intf 0
151
           returns: added host"""
152
        host = self.host( name )
153
        # for now, assume one interface per host.
154
        host.intfs.append( name + '-eth0' )
155
        self.hosts.append( host )
156
        self.nameToNode[ name ] = host
157
        # May wish to add this to actual object
158
        if defaultMac:
159
            host.defaultMac = defaultMac
160
        if defaultIp:
161
            host.defaultIP = defaultIp
162
        return host
163

    
164
    def addSwitch( self, name, defaultMac=None ):
165
        """Add switch.
166
           name: name of switch to add
167
           defaultMac: default MAC address for kernel/OVS switch intf 0
168
           returns: added switch"""
169
        if self.switch is KernelSwitch or self.switch is OVSKernelSwitch:
170
            sw = self.switch( name, dp=self.dps, defaultMac=defaultMac )
171
            self.dps += 1
172
        else:
173
            sw = self.switch( name )
174
        self.switches.append( sw )
175
        self.nameToNode[ name ] = sw
176
        return sw
177

    
178
    def addLink( self, src, srcPort, dst, dstPort ):
179
        """Add link.
180
           src: source Node
181
           srcPort: source port
182
           dst: destination Node
183
           dstPort: destination port"""
184
        srcIntf = src.intfName( srcPort )
185
        dstIntf = dst.intfName( dstPort )
186
        makeIntfPair( srcIntf, dstIntf )
187
        src.intfs.append( srcIntf )
188
        dst.intfs.append( dstIntf )
189
        src.ports[ srcPort ] = srcIntf
190
        dst.ports[ dstPort ] = dstIntf
191
        #info( '\n' )
192
        #info( 'added intf %s to src node %x\n' % ( srcIntf, src ) )
193
        #info( 'added intf %s to dst node %x\n' % ( dstIntf, dst ) )
194
        if src.inNamespace:
195
            #info( 'moving src w/inNamespace set\n' )
196
            moveIntf( srcIntf, src )
197
        if dst.inNamespace:
198
            #info( 'moving dst w/inNamespace set\n' )
199
            moveIntf( dstIntf, dst )
200
        src.connection[ srcIntf ] = ( dst, dstIntf )
201
        dst.connection[ dstIntf ] = ( src, srcIntf )
202

    
203
    def addController( self, controller ):
204
        """Add controller.
205
           controller: Controller class"""
206
        controller = self.controller( 'c0', self.inNamespace )
207
        if controller: # allow controller-less setups
208
            self.controllers.append( controller )
209
            self.nameToNode[ 'c0' ] = controller
210

    
211
    # Control network support:
212
    #
213
    # Create an explicit control network. Currently this is only
214
    # used by the user datapath configuration.
215
    #
216
    # Notes:
217
    #
218
    # 1. If the controller and switches are in the same ( e.g. root )
219
    #    namespace, they can just use the loopback connection.
220
    #    We may wish to do this for the user datapath as well as the
221
    #    kernel datapath.
222
    #
223
    # 2. If we can get unix domain sockets to work, we can use them
224
    #    instead of an explicit control network.
225
    #
226
    # 3. Instead of routing, we could bridge or use 'in-band' control.
227
    #
228
    # 4. Even if we dispense with this in general, it could still be
229
    #    useful for people who wish to simulate a separate control
230
    #    network (since real networks may need one!)
231

    
232
    def _configureControlNetwork( self ):
233
        "Configure control network."
234
        self._configureRoutedControlNetwork()
235

    
236
    def _configureRoutedControlNetwork( self ):
237
        """Configure a routed control network on controller and switches.
238
           For use with the user datapath only right now.
239
           TODO( brandonh ) test this code!
240
           """
241
        # params were: controller, switches, ips
242

    
243
        controller = self.controllers[ 0 ]
244
        info( '%s <-> ' % controller.name )
245
        for switch in self.switches:
246
            info( '%s ' % switch.name )
247
            sip = switch.defaultIP
248
            sintf = switch.intfs[ 0 ]
249
            node, cintf = switch.connection[ sintf ]
250
            if node != controller:
251
                error( '*** Error: switch %s not connected to correct'
252
                         'controller' %
253
                         switch.name )
254
                exit( 1 )
255
            controller.setIP( cintf, self.cparams.ip, '/' +
256
                             self.cparams.subnetSize )
257
            switch.setIP( sintf, sip, '/' + self.cparams.subnetSize )
258
            controller.setHostRoute( sip, cintf )
259
            switch.setHostRoute( self.cparams.ip, sintf )
260
        info( '\n' )
261
        info( '*** Testing control network\n' )
262
        while not controller.intfIsUp( controller.intfs[ 0 ] ):
263
            info( '*** Waiting for %s to come up\n',
264
                controller.intfs[ 0 ] )
265
            sleep( 1 )
266
        for switch in self.switches:
267
            while not switch.intfIsUp( switch.intfs[ 0 ] ):
268
                info( '*** Waiting for %s to come up\n' %
269
                    switch.intfs[ 0 ] )
270
                sleep( 1 )
271
            if self.ping( hosts=[ switch, controller ] ) != 0:
272
                error( '*** Error: control network test failed\n' )
273
                exit( 1 )
274
        info( '\n' )
275

    
276
    def _configHosts( self ):
277
        "Configure a set of hosts."
278
        # params were: hosts, ips
279
        for host in self.hosts:
280
            hintf = host.intfs[ 0 ]
281
            host.setIP( hintf, host.defaultIP,
282
                       '/' + str( self.cparams.subnetSize ) )
283
            host.setDefaultRoute( hintf )
284
            # You're low priority, dude!
285
            quietRun( 'renice +18 -p ' + repr( host.pid ) )
286
            info( '%s ', host.name )
287
        info( '\n' )
288

    
289
    def buildFromTopo( self, topo ):
290
        """Build mininet from a topology object
291
           At the end of this function, everything should be connected
292
           and up."""
293
        if self.cleanup:
294
            pass # cleanup
295
        # validate topo?
296
        info( '*** Adding controller\n' )
297
        self.addController( self.controller )
298
        info( '*** Creating network\n' )
299
        info( '*** Adding hosts:\n' )
300
        for hostId in sorted( topo.hosts() ):
301
            name = 'h' + topo.name( hostId )
302
            mac = macColonHex( hostId ) if self.setMacs else None
303
            ip = topo.ip( hostId )
304
            host = self.addHost( name, defaultIp=ip, defaultMac=mac )
305
            self.idToNode[ hostId ] = host
306
            info( name )
307
        info( '\n*** Adding switches:\n' )
308
        for switchId in sorted( topo.switches() ):
309
            name = 's' + topo.name( switchId )
310
            mac = macColonHex( switchId) if self.setMacs else None
311
            switch = self.addSwitch( name, defaultMac=mac )
312
            self.idToNode[ switchId ] = switch
313
            info( name )
314
        info( '\n*** Adding edges:\n' )
315
        for srcId, dstId in sorted( topo.edges() ):
316
            src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
317
            srcPort, dstPort = topo.port( srcId, dstId )
318
            self.addLink( src, srcPort, dst, dstPort )
319
            info( '(%s, %s) ' % ( src.name, dst.name ) )
320
        info( '\n' )
321

    
322
        if self.inNamespace:
323
            info( '*** Configuring control network\n' )
324
            self._configureControlNetwork()
325

    
326
        info( '*** Configuring hosts\n' )
327
        self._configHosts()
328

    
329
        if self.xterms:
330
            self.startXterms()
331
        if self.autoSetMacs:
332
            self.setMacs()
333
        if self.autoStaticArp:
334
            self.staticArp()
335

    
336
    def startXterms( self ):
337
        "Start an xterm for each node."
338
        info( "*** Running xterms on %s\n" % os.environ[ 'DISPLAY' ] )
339
        cleanUpScreens()
340
        self.terms += makeXterms( self.controllers, 'controller' )
341
        self.terms += makeXterms( self.switches, 'switch' )
342
        self.terms += makeXterms( self.hosts, 'host' )
343

    
344
    def stopXterms( self ):
345
        "Kill each xterm."
346
        # Kill xterms
347
        for term in self.terms:
348
            os.kill( term.pid, signal.SIGKILL )
349
        cleanUpScreens()
350

    
351
    def setMacs( self ):
352
        """Set MAC addrs to correspond to datapath IDs on hosts.
353
           Assume that the host only has one interface."""
354
        for host in self.hosts:
355
            host.setMAC( host.intfs[ 0 ], host.defaultMac )
356

    
357
    def staticArp( self ):
358
        "Add all-pairs ARP entries to remove the need to handle broadcast."
359
        for src in self.hosts:
360
            for dst in self.hosts:
361
                if src != dst:
362
                    src.setARP( ip=dst.IP(), mac=dst.defaultMac )
363

    
364
    def start( self ):
365
        "Start controller and switches"
366
        info( '*** Starting controller\n' )
367
        for controller in self.controllers:
368
            controller.start()
369
        info( '*** Starting %s switches\n' % len( self.switches ) )
370
        for switch in self.switches:
371
            info( switch.name )
372
            switch.start( self.controllers )
373
        info( '\n' )
374

    
375
    def stop( self ):
376
        "Stop the controller(s), switches and hosts"
377
        if self.terms:
378
            info( '*** Stopping %i terms\n' % len( self.terms ) )
379
            self.stopXterms()
380
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
381
        for host in self.hosts:
382
            info( '%s ' % host.name )
383
            host.terminate()
384
        info( '\n' )
385
        info( '*** Stopping %i switches\n' % len( self.switches ) )
386
        for switch in self.switches:
387
            info( '%s ' % switch.name )
388
            switch.stop()
389
        info( '\n' )
390
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
391
        for controller in self.controllers:
392
            controller.stop()
393
        info( '*** Test complete\n' )
394

    
395
    def run( self, test, **params ):
396
        "Perform a complete start/test/stop cycle."
397
        self.start()
398
        info( '*** Running test\n' )
399
        result = getattr( self, test )( **params )
400
        self.stop()
401
        return result
402

    
403
    @staticmethod
404
    def _parsePing( pingOutput ):
405
        "Parse ping output and return packets sent, received."
406
        r = r'(\d+) packets transmitted, (\d+) received'
407
        m = re.search( r, pingOutput )
408
        if m == None:
409
            error( '*** Error: could not parse ping output: %s\n' %
410
                     pingOutput )
411
            exit( 1 )
412
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
413
        return sent, received
414

    
415
    def ping( self, hosts=None ):
416
        """Ping between all specified hosts.
417
           hosts: list of hosts
418
           returns: ploss packet loss percentage"""
419
        #self.start()
420
        # check if running - only then, start?
421
        packets = 0
422
        lost = 0
423
        ploss = None
424
        if not hosts:
425
            hosts = self.hosts
426
            info( '*** Ping: testing ping reachability\n' )
427
        for node in hosts:
428
            info( '%s -> ' % node.name )
429
            for dest in hosts:
430
                if node != dest:
431
                    result = node.cmd( 'ping -c1 ' + dest.IP() )
432
                    sent, received = self._parsePing( result )
433
                    packets += sent
434
                    if received > sent:
435
                        error( '*** Error: received too many packets' )
436
                        error( '%s' % result )
437
                        node.cmdPrint( 'route' )
438
                        exit( 1 )
439
                    lost += sent - received
440
                    info( ( '%s ' % dest.name ) if received else 'X ' )
441
            info( '\n' )
442
            ploss = 100 * lost / packets
443
        info( "*** Results: %i%% dropped (%d/%d lost)\n" %
444
                ( ploss, lost, packets ) )
445
        return ploss
446

    
447
    def pingAll( self ):
448
        """Ping between all hosts.
449
           returns: ploss packet loss percentage"""
450
        return self.ping()
451

    
452
    def pingPair( self ):
453
        """Ping between first two hosts, useful for testing.
454
           returns: ploss packet loss percentage"""
455
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
456
        return self.ping( hosts=hosts )
457

    
458
    @staticmethod
459
    def _parseIperf( iperfOutput ):
460
        """Parse iperf output and return bandwidth.
461
           iperfOutput: string
462
           returns: result string"""
463
        r = r'([\d\.]+ \w+/sec)'
464
        m = re.search( r, iperfOutput )
465
        if m:
466
            return m.group( 1 )
467
        else:
468
            raise Exception( 'could not parse iperf output: ' + iperfOutput )
469

    
470
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M',
471
              verbose=False ):
472
        """Run iperf between two hosts.
473
           hosts: list of hosts; if None, uses opposite hosts
474
           l4Type: string, one of [ TCP, UDP ]
475
           verbose: verbose printing
476
           returns: results two-element array of server and client speeds"""
477
        if not hosts:
478
            hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
479
        else:
480
            assert len( hosts ) == 2
481
        host0, host1 = hosts
482
        info( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
483
        info( "%s and %s\n" % ( host0.name, host1.name ) )
484
        host0.cmd( 'killall -9 iperf' )
485
        iperfArgs = 'iperf '
486
        bwArgs = ''
487
        if l4Type == 'UDP':
488
            iperfArgs += '-u '
489
            bwArgs = '-b ' + udpBw + ' '
490
        elif l4Type != 'TCP':
491
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
492
        server = host0.cmd( iperfArgs + '-s &' )
493
        if verbose:
494
            info( '%s\n' % server )
495
        client = host1.cmd( iperfArgs + '-t 5 -c ' + host0.IP() + ' ' +
496
                           bwArgs )
497
        if verbose:
498
            info( '%s\n' % client )
499
        server = host0.cmd( 'killall -9 iperf' )
500
        if verbose:
501
            info( '%s\n' % server )
502
        result = [ self._parseIperf( server ), self._parseIperf( client ) ]
503
        if l4Type == 'UDP':
504
            result.insert( 0, udpBw )
505
        info( '*** Results: %s\n' % result )
506
        return result
507

    
508
    def iperfUdp( self, udpBw='10M' ):
509
        "Run iperf UDP test."
510
        return self.iperf( l4Type='UDP', udpBw=udpBw )
511

    
512
    def interact( self ):
513
        "Start network and run our simple CLI."
514
        self.start()
515
        result = CLI( self )
516
        self.stop()
517
        return result