Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 548580d8

History | View | Annotate | Download (23.5 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 addLink( self, node1, node2, port1=None, port2=None,
224
                 cls=None, **params ):
225
        """"Add a link from node1 to node2
226
            node1: source node
227
            node2: dest node
228
            port1: source port
229
            port2: dest port
230
            returns: link object"""
231
        defaults = { 'port1': port1,
232
                     'port2': port2,
233
                     'intf': self.intf }
234
        defaults.update( params )
235
        if not cls:
236
            cls = self.link
237
        return cls( node1, node2, **defaults )
238

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

    
253
    def buildFromTopo( self, topo=None ):
254
        """Build mininet from a topology object
255
           At the end of this function, everything should be connected
256
           and up."""
257

    
258
        # Possibly we should clean up here and/or validate
259
        # the topo
260
        if self.cleanup:
261
            pass
262

    
263
        info( '*** Creating network\n' )
264

    
265
        if not self.controllers:
266
            # Add a default controller
267
            info( '*** Adding controller\n' )
268
            self.addController( 'c0' )
269

    
270
        info( '*** Adding hosts:\n' )
271
        for hostName in topo.hosts():
272
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
273
            info( hostName + ' ' )
274

    
275
        info( '\n*** Adding switches:\n' )
276
        for switchName in topo.switches():
277
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
278
            info( switchName + ' ' )
279

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

    
288
        info( '\n' )
289

    
290
    def configureControlNetwork( self ):
291
        "Control net config hook: override in subclass"
292
        raise Exception( 'configureControlNetwork: '
293
               'should be overriden in subclass', self )
294

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

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

    
317
    def stopXterms( self ):
318
        "Kill each xterm."
319
        for term in self.terms:
320
            os.kill( term.pid, signal.SIGKILL )
321
        cleanUpScreens()
322

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

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

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

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

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

    
396
    # XXX These test methods should be moved out of this class.
397
    # Probably we should create a tests.py for them
398

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

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

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

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

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

    
470
    # XXX This should be cleaned up
471

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

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

    
543
    def interact( self ):
544
        "Start network and run our simple CLI."
545
        self.start()
546
        result = CLI( self )
547
        self.stop()
548
        return result
549

    
550
    inited = False
551

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

    
566

    
567
class MininetWithControlNet( Mininet ):
568

    
569
    """Control network support:
570

571
       Create an explicit control network. Currently this is only
572
       used/usable with the user datapath.
573

574
       Notes:
575

576
       1. If the controller and switches are in the same (e.g. root)
577
          namespace, they can just use the loopback connection.
578

579
       2. If we can get unix domain sockets to work, we can use them
580
          instead of an explicit control network.
581

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

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

588
       5. Basically nobody ever used this code, so it has been moved
589
          into its own class.
590

591
       6. Ultimately we may wish to extend this to allow us to create a
592
          control network which every node's control interface is
593
          attached to."""
594

    
595
    def configureControlNetwork( self ):
596
        "Configure control network."
597
        self.configureRoutedControlNetwork()
598

    
599
    # We still need to figure out the right way to pass
600
    # in the control network location.
601

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