Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 089e8130

History | View | Annotate | Download (23.6 KB)

1
"""
2

3
    Mininet: A simple networking testbed for OpenFlow/SDN!
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 (openflowswitch.org)
26
as well as OpenVSwitch (openvswitch.org.)
27

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

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

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

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

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

50
The basic naming scheme is as follows:
51

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

57
Note: If the network topology is created using mininet.topo, then
58
node numbers are unique among hosts and switches (e.g. we have
59
h1..hN and SN..SN+M) and also correspond to their default IP addresses
60
of 10.x.y.z/8 where x.y.z is the base-256 representation of N for
61
hN. This mapping allows easy determination of a node's IP
62
address from its name, e.g. h1 -> 10.0.0.1, h257 -> 10.0.1.1.
63

64
Note also that 10.0.0.1 can often be written as 10.1 for short, e.g.
65
"ping 10.1" is equivalent to "ping 10.0.0.1".
66

67
Currently we wrap the entire network in a 'mininet' object, which
68
constructs a simulated network based on a network topology created
69
using a topology object (e.g. LinearTopo) from mininet.topo or
70
mininet.topolib, and a Controller which the switches will connect
71
to. Several configuration options are provided for functions such as
72
automatically setting MAC addresses, populating the ARP table, or
73
even running a set of terminals to allow direct interaction with nodes.
74

75
After the network is created, it can be started using start(), and a
76
variety of useful tasks maybe performed, including basic connectivity
77
and bandwidth tests and running the mininet CLI.
78

79
Once the network is up and running, test code can easily get access
80
to host and switch objects which can then be used for arbitrary
81
experiments, typically involving running a series of commands on the
82
hosts.
83

84
After all desired tests or activities have been completed, the stop()
85
method may be called to shut down the network.
86

87
"""
88

    
89
import os
90
import re
91
import select
92
import signal
93
from time import sleep
94

    
95
from mininet.cli import CLI
96
from mininet.log import info, error, debug, output
97
from mininet.node import Host, OVSKernelSwitch, Controller
98
from mininet.link import Link, Intf
99
from mininet.util import quietRun, fixLimits, numCores
100
from mininet.util import macColonHex, ipStr, ipParse, netParse, ipAdd
101
from mininet.term import cleanUpScreens, makeTerms
102

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

    
106
    def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host,
107
                 controller=Controller, link=Link, intf=Intf,
108
                 build=True, xterms=False, cleanup=False, ipBase='10.0.0.0/8',
109
                 inNamespace=False,
110
                 autoSetMacs=False, autoStaticArp=False, autoPinCpus=False,
111
                 listenPort=None ):
112
        """Create Mininet object.
113
           topo: Topo (topology) object or None
114
           switch: default Switch class
115
           host: default Host class/constructor
116
           controller: default Controller class/constructor
117
           link: default Link class/constructor
118
           intf: default Intf class/constructor
119
           ipBase: base IP address for hosts,
120
           build: build now from topo?
121
           xterms: if build now, spawn xterms?
122
           cleanup: if build now, cleanup before creating?
123
           inNamespace: spawn switches and controller in net namespaces?
124
           autoSetMacs: set MAC addrs automatically like IP addresses?
125
           autoStaticArp: set all-pairs static MAC addrs?
126
           autoPinCpus: pin hosts to (real) cores (requires CPULimitedHost)?
127
           listenPort: base listening port to open; will be incremented for
128
               each additional switch in the net if inNamespace=False"""
129
        self.topo = topo
130
        self.switch = switch
131
        self.host = host
132
        self.controller = controller
133
        self.link = link
134
        self.intf = intf
135
        self.ipBase = ipBase
136
        self.ipBaseNum, self.prefixLen = netParse( self.ipBase )
137
        self.nextIP = 1  # start for address allocation
138
        self.inNamespace = inNamespace
139
        self.xterms = xterms
140
        self.cleanup = cleanup
141
        self.autoSetMacs = autoSetMacs
142
        self.autoStaticArp = autoStaticArp
143
        self.autoPinCpus = autoPinCpus
144
        self.numCores = numCores()
145
        self.nextCore = 0  # next core for pinning hosts to CPUs
146
        self.listenPort = listenPort
147

    
148
        self.hosts = []
149
        self.switches = []
150
        self.controllers = []
151

    
152
        self.nameToNode = {}  # name to Node (Host/Switch) objects
153

    
154
        self.terms = []  # list of spawned xterm processes
155

    
156
        Mininet.init()  # Initialize Mininet if necessary
157

    
158
        self.built = False
159
        if topo and build:
160
            self.build()
161

    
162
    def addHost( self, name, cls=None, **params ):
163
        """Add host.
164
           name: name of host to add
165
           cls: custom host class/constructor (optional)
166
           params: parameters for host
167
           returns: added host"""
168
        # Default IP and MAC addresses
169
        defaults = { 'ip': ipAdd( self.nextIP,
170
                                  ipBaseNum=self.ipBaseNum,
171
                                  prefixLen=self.prefixLen ) }
172
        if self.autoSetMacs:
173
            defaults[ 'mac'] = macColonHex( self.nextIP )
174
        if self.autoPinCpus:
175
            defaults[ 'cores' ] = self.nextCore
176
            self.nextCore = ( self.nextCore + 1 ) % self.numCores
177
        self.nextIP += 1
178
        defaults.update( params )
179
        if not cls:
180
            cls = self.host
181
        h = cls( name, **defaults )
182
        self.hosts.append( h )
183
        self.nameToNode[ name ] = h
184
        return h
185

    
186
    def addSwitch( self, name, cls=None, **params ):
187
        """Add switch.
188
           name: name of switch to add
189
           cls: custom switch class/constructor (optional)
190
           returns: added switch
191
           side effect: increments listenPort ivar ."""
192
        defaults = { 'listenPort': self.listenPort,
193
                     'inNamespace': self.inNamespace }
194
        defaults.update( params )
195
        if not cls:
196
            cls = self.switch
197
        sw = cls( name, **defaults )
198
        if not self.inNamespace and self.listenPort:
199
            self.listenPort += 1
200
        self.switches.append( sw )
201
        self.nameToNode[ name ] = sw
202
        return sw
203

    
204
    def addController( self, name='c0', controller=None, **params ):
205
        """Add controller.
206
           controller: Controller class"""
207
        if not controller:
208
            controller = self.controller
209
        controller_new = controller( name, **params )
210
        if controller_new:  # allow controller-less setups
211
            self.controllers.append( controller_new )
212
            self.nameToNode[ name ] = controller_new
213
        return controller_new
214

    
215
    # BL: is this better than just using nameToNode[] ?
216
    # Should it have a better name?
217
    def getNodeByName( self, *args ):
218
        "Return node(s) with given name(s)"
219
        if len( args ) == 1:
220
            return self.nameToNode[ args[ 0 ] ]
221
        return [ self.nameToNode[ n ] for n in args ]
222

    
223
    def get( self, *args ):
224
        "Convenience alias for getNodeByName"
225
        return self.getNodeByName( *args )
226

    
227
    def addLink( self, node1, node2, port1=None, port2=None,
228
                 cls=None, **params ):
229
        """"Add a link from node1 to node2
230
            node1: source node
231
            node2: dest node
232
            port1: source port
233
            port2: dest port
234
            returns: link object"""
235
        defaults = { 'port1': port1,
236
                     'port2': port2,
237
                     'intf': self.intf }
238
        defaults.update( params )
239
        if not cls:
240
            cls = self.link
241
        return cls( node1, node2, **defaults )
242

    
243
    def configHosts( self ):
244
        "Configure a set of hosts."
245
        for host in self.hosts:
246
            info( host.name + ' ' )
247
            host.configDefault( defaultRoute=host.defaultIntf() )
248
            # You're low priority, dude!
249
            # BL: do we want to do this here or not?
250
            # May not make sense if we have CPU lmiting...
251
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
252
            # This may not be the right place to do this, but
253
            # it needs to be done somewhere.
254
            host.cmd( 'ifconfig lo up' )
255
        info( '\n' )
256

    
257
    def buildFromTopo( self, topo=None ):
258
        """Build mininet from a topology object
259
           At the end of this function, everything should be connected
260
           and up."""
261

    
262
        # Possibly we should clean up here and/or validate
263
        # the topo
264
        if self.cleanup:
265
            pass
266

    
267
        info( '*** Creating network\n' )
268

    
269
        if not self.controllers:
270
            # Add a default controller
271
            info( '*** Adding controller\n' )
272
            self.addController( 'c0' )
273

    
274
        info( '*** Adding hosts:\n' )
275
        for hostName in topo.hosts():
276
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
277
            info( hostName + ' ' )
278

    
279
        info( '\n*** Adding switches:\n' )
280
        for switchName in topo.switches():
281
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
282
            info( switchName + ' ' )
283

    
284
        info( '\n*** Adding links:\n' )
285
        for srcName, dstName in topo.links(sort=True):
286
            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
287
            params = topo.linkInfo( srcName, dstName )
288
            srcPort, dstPort = topo.port( srcName, dstName )
289
            self.addLink( src, dst, srcPort, dstPort, **params )
290
            info( '(%s, %s) ' % ( src.name, dst.name ) )
291

    
292
        info( '\n' )
293

    
294
    def configureControlNetwork( self ):
295
        "Control net config hook: override in subclass"
296
        raise Exception( 'configureControlNetwork: '
297
               'should be overriden in subclass', self )
298

    
299
    def build( self ):
300
        "Build mininet."
301
        if self.topo:
302
            self.buildFromTopo( self.topo )
303
        if ( self.inNamespace ):
304
            self.configureControlNetwork()
305
        info( '*** Configuring hosts\n' )
306
        self.configHosts()
307
        if self.xterms:
308
            self.startTerms()
309
        if self.autoStaticArp:
310
            self.staticArp()
311
        self.built = True
312

    
313
    def startTerms( self ):
314
        "Start a terminal for each node."
315
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
316
        cleanUpScreens()
317
        self.terms += makeTerms( self.controllers, 'controller' )
318
        self.terms += makeTerms( self.switches, 'switch' )
319
        self.terms += makeTerms( self.hosts, 'host' )
320

    
321
    def stopXterms( self ):
322
        "Kill each xterm."
323
        for term in self.terms:
324
            os.kill( term.pid, signal.SIGKILL )
325
        cleanUpScreens()
326

    
327
    def staticArp( self ):
328
        "Add all-pairs ARP entries to remove the need to handle broadcast."
329
        for src in self.hosts:
330
            for dst in self.hosts:
331
                if src != dst:
332
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
333

    
334
    def start( self ):
335
        "Start controller and switches."
336
        if not self.built:
337
            self.build()
338
        info( '*** Starting controller\n' )
339
        for controller in self.controllers:
340
            controller.start()
341
        info( '*** Starting %s switches\n' % len( self.switches ) )
342
        for switch in self.switches:
343
            info( switch.name + ' ')
344
            switch.start( self.controllers )
345
        info( '\n' )
346

    
347
    def stop( self ):
348
        "Stop the controller(s), switches and hosts"
349
        if self.terms:
350
            info( '*** Stopping %i terms\n' % len( self.terms ) )
351
            self.stopXterms()
352
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
353
        for host in self.hosts:
354
            info( host.name + ' ' )
355
            host.terminate()
356
        info( '\n' )
357
        info( '*** Stopping %i switches\n' % len( self.switches ) )
358
        for switch in self.switches:
359
            info( switch.name + ' ' )
360
            switch.stop()
361
        info( '\n' )
362
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
363
        for controller in self.controllers:
364
            info( controller.name + ' ' )
365
            controller.stop()
366
        info( '\n*** Done\n' )
367

    
368
    def run( self, test, *args, **kwargs ):
369
        "Perform a complete start/test/stop cycle."
370
        self.start()
371
        info( '*** Running test\n' )
372
        result = test( *args, **kwargs )
373
        self.stop()
374
        return result
375

    
376
    def monitor( self, hosts=None, timeoutms=-1 ):
377
        """Monitor a set of hosts (or all hosts by default),
378
           and return their output, a line at a time.
379
           hosts: (optional) set of hosts to monitor
380
           timeoutms: (optional) timeout value in ms
381
           returns: iterator which returns host, line"""
382
        if hosts is None:
383
            hosts = self.hosts
384
        poller = select.poll()
385
        Node = hosts[ 0 ]  # so we can call class method fdToNode
386
        for host in hosts:
387
            poller.register( host.stdout )
388
        while True:
389
            ready = poller.poll( timeoutms )
390
            for fd, event in ready:
391
                host = Node.fdToNode( fd )
392
                if event & select.POLLIN:
393
                    line = host.readline()
394
                    if line is not None:
395
                        yield host, line
396
            # Return if non-blocking
397
            if not ready and timeoutms >= 0:
398
                yield None, None
399

    
400
    # XXX These test methods should be moved out of this class.
401
    # Probably we should create a tests.py for them
402

    
403
    @staticmethod
404
    def _parsePing( pingOutput ):
405
        "Parse ping output and return packets sent, received."
406
        # Check for downed link
407
        if 'connect: Network is unreachable' in pingOutput:
408
            return (1, 0)
409
        r = r'(\d+) packets transmitted, (\d+) received'
410
        m = re.search( r, pingOutput )
411
        if m == None:
412
            error( '*** Error: could not parse ping output: %s\n' %
413
                     pingOutput )
414
            return (1, 0)
415
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
416
        return sent, received
417

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

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

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

    
460
    @staticmethod
461
    def _parseIperf( iperfOutput ):
462
        """Parse iperf output and return bandwidth.
463
           iperfOutput: string
464
           returns: result string"""
465
        r = r'([\d\.]+ \w+/sec)'
466
        m = re.findall( r, iperfOutput )
467
        if m:
468
            return m[-1]
469
        else:
470
            # was: raise Exception(...)
471
            error( 'could not parse iperf output: ' + iperfOutput )
472
            return ''
473

    
474
    # XXX This should be cleaned up
475

    
476
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M' ):
477
        """Run iperf between two hosts.
478
           hosts: list of hosts; if None, uses opposite hosts
479
           l4Type: string, one of [ TCP, UDP ]
480
           returns: results two-element array of server and client speeds"""
481
        if not quietRun( 'which telnet' ):
482
            error( 'Cannot find telnet in $PATH - required for iperf test' )
483
            return
484
        if not hosts:
485
            hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
486
        else:
487
            assert len( hosts ) == 2
488
        client, server = hosts
489
        output( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
490
        output( "%s and %s\n" % ( client.name, server.name ) )
491
        server.cmd( 'killall -9 iperf' )
492
        iperfArgs = 'iperf '
493
        bwArgs = ''
494
        if l4Type == 'UDP':
495
            iperfArgs += '-u '
496
            bwArgs = '-b ' + udpBw + ' '
497
        elif l4Type != 'TCP':
498
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
499
        server.sendCmd( iperfArgs + '-s', printPid=True )
500
        servout = ''
501
        while server.lastPid is None:
502
            servout += server.monitor()
503
        if l4Type == 'TCP':
504
            while 'Connected' not in client.cmd(
505
                'sh -c "echo A | telnet -e A %s 5001"' % server.IP()):
506
                output('waiting for iperf to start up...')
507
                sleep(.5)
508
        cliout = client.cmd( iperfArgs + '-t 5 -c ' + server.IP() + ' ' +
509
                           bwArgs )
510
        debug( 'Client output: %s\n' % cliout )
511
        server.sendInt()
512
        servout += server.waitOutput()
513
        debug( 'Server output: %s\n' % servout )
514
        result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ]
515
        if l4Type == 'UDP':
516
            result.insert( 0, udpBw )
517
        output( '*** Results: %s\n' % result )
518
        return result
519

    
520
    # BL: I think this can be rewritten now that we have
521
    # a real link class.
522
    def configLinkStatus( self, src, dst, status ):
523
        """Change status of src <-> dst links.
524
           src: node name
525
           dst: node name
526
           status: string {up, down}"""
527
        if src not in self.nameToNode:
528
            error( 'src not in network: %s\n' % src )
529
        elif dst not in self.nameToNode:
530
            error( 'dst not in network: %s\n' % dst )
531
        else:
532
            if type( src ) is str:
533
                src = self.nameToNode[ src ]
534
            if type( dst ) is str:
535
                dst = self.nameToNode[ dst ]
536
            connections = src.connectionsTo( dst )
537
            if len( connections ) == 0:
538
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
539
            for srcIntf, dstIntf in connections:
540
                result = srcIntf.ifconfig( status )
541
                if result:
542
                    error( 'link src status change failed: %s\n' % result )
543
                result = dstIntf.ifconfig( status )
544
                if result:
545
                    error( 'link dst status change failed: %s\n' % result )
546

    
547
    def interact( self ):
548
        "Start network and run our simple CLI."
549
        self.start()
550
        result = CLI( self )
551
        self.stop()
552
        return result
553

    
554
    inited = False
555

    
556
    @classmethod
557
    def init( cls ):
558
        "Initialize Mininet"
559
        if cls.inited:
560
            return
561
        if os.getuid() != 0:
562
            # Note: this script must be run as root
563
            # Probably we should only sudo when we need
564
            # to as per Big Switch's patch
565
            print "*** Mininet must run as root."
566
            exit( 1 )
567
        fixLimits()
568
        cls.inited = True
569

    
570

    
571
class MininetWithControlNet( Mininet ):
572

    
573
    """Control network support:
574

575
       Create an explicit control network. Currently this is only
576
       used/usable with the user datapath.
577

578
       Notes:
579

580
       1. If the controller and switches are in the same (e.g. root)
581
          namespace, they can just use the loopback connection.
582

583
       2. If we can get unix domain sockets to work, we can use them
584
          instead of an explicit control network.
585

586
       3. Instead of routing, we could bridge or use 'in-band' control.
587

588
       4. Even if we dispense with this in general, it could still be
589
          useful for people who wish to simulate a separate control
590
          network (since real networks may need one!)
591

592
       5. Basically nobody ever used this code, so it has been moved
593
          into its own class.
594

595
       6. Ultimately we may wish to extend this to allow us to create a
596
          control network which every node's control interface is
597
          attached to."""
598

    
599
    def configureControlNetwork( self ):
600
        "Configure control network."
601
        self.configureRoutedControlNetwork()
602

    
603
    # We still need to figure out the right way to pass
604
    # in the control network location.
605

    
606
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
607
        prefixLen=16 ):
608
        """Configure a routed control network on controller and switches.
609
           For use with the user datapath only right now."""
610
        controller = self.controllers[ 0 ]
611
        info( controller.name + ' <->' )
612
        cip = ip
613
        snum = ipParse( ip )
614
        for switch in self.switches:
615
            info( ' ' + switch.name )
616
            link = self.link( switch, controller, port1=0 )
617
            sintf, cintf = link.intf1, link.intf2
618
            switch.controlIntf = sintf
619
            snum += 1
620
            while snum & 0xff in [ 0, 255 ]:
621
                snum += 1
622
            sip = ipStr( snum )
623
            cintf.setIP( cip, prefixLen )
624
            sintf.setIP( sip, prefixLen )
625
            controller.setHostRoute( sip, cintf )
626
            switch.setHostRoute( cip, sintf )
627
        info( '\n' )
628
        info( '*** Testing control network\n' )
629
        while not cintf.isUp():
630
            info( '*** Waiting for', cintf, 'to come up\n' )
631
            sleep( 1 )
632
        for switch in self.switches:
633
            while not sintf.isUp():
634
                info( '*** Waiting for', sintf, 'to come up\n' )
635
                sleep( 1 )
636
            if self.ping( hosts=[ switch, controller ] ) != 0:
637
                error( '*** Error: control network test failed\n' )
638
                exit( 1 )
639
        info( '\n' )