Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ edf60032

History | View | Annotate | Download (24.1 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
# Mininet version: should be consistent with README and LICENSE
104
VERSION = "2.0.0rc1"
105

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

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

    
151
        self.hosts = []
152
        self.switches = []
153
        self.controllers = []
154

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

    
157
        self.terms = []  # list of spawned xterm processes
158

    
159
        Mininet.init()  # Initialize Mininet if necessary
160

    
161
        self.built = False
162
        if topo and build:
163
            self.build()
164

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

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

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

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

    
227
    def get( self, *args ):
228
        "Convenience alias for getNodeByName"
229
        return self.getNodeByName( *args )
230

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

    
247
    def configHosts( self ):
248
        "Configure a set of hosts."
249
        for host in self.hosts:
250
            info( host.name + ' ' )
251
            intf = host.defaultIntf()
252
            if intf:
253
                host.configDefault( defaultRoute=intf )
254
            else:
255
                # Don't configure nonexistent intf
256
                host.configDefault( ip=None, mac=None )
257
            # You're low priority, dude!
258
            # BL: do we want to do this here or not?
259
            # May not make sense if we have CPU lmiting...
260
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
261
            # This may not be the right place to do this, but
262
            # it needs to be done somewhere.
263
            host.cmd( 'ifconfig lo up' )
264
        info( '\n' )
265

    
266
    def buildFromTopo( self, topo=None ):
267
        """Build mininet from a topology object
268
           At the end of this function, everything should be connected
269
           and up."""
270

    
271
        # Possibly we should clean up here and/or validate
272
        # the topo
273
        if self.cleanup:
274
            pass
275

    
276
        info( '*** Creating network\n' )
277

    
278
        if not self.controllers:
279
            # Add a default controller
280
            info( '*** Adding controller\n' )
281
            classes = self.controller
282
            if type( classes ) is not list:
283
                classes = [ classes ]
284
            for i, cls in enumerate( classes ):
285
                self.addController( 'c%d' % i, cls )
286

    
287
        info( '*** Adding hosts:\n' )
288
        for hostName in topo.hosts():
289
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
290
            info( hostName + ' ' )
291

    
292
        info( '\n*** Adding switches:\n' )
293
        for switchName in topo.switches():
294
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
295
            info( switchName + ' ' )
296

    
297
        info( '\n*** Adding links:\n' )
298
        for srcName, dstName in topo.links(sort=True):
299
            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
300
            params = topo.linkInfo( srcName, dstName )
301
            srcPort, dstPort = topo.port( srcName, dstName )
302
            self.addLink( src, dst, srcPort, dstPort, **params )
303
            info( '(%s, %s) ' % ( src.name, dst.name ) )
304

    
305
        info( '\n' )
306

    
307
    def configureControlNetwork( self ):
308
        "Control net config hook: override in subclass"
309
        raise Exception( 'configureControlNetwork: '
310
                         'should be overriden in subclass', self )
311

    
312
    def build( self ):
313
        "Build mininet."
314
        if self.topo:
315
            self.buildFromTopo( self.topo )
316
        if ( self.inNamespace ):
317
            self.configureControlNetwork()
318
        info( '*** Configuring hosts\n' )
319
        self.configHosts()
320
        if self.xterms:
321
            self.startTerms()
322
        if self.autoStaticArp:
323
            self.staticArp()
324
        self.built = True
325

    
326
    def startTerms( self ):
327
        "Start a terminal for each node."
328
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
329
        cleanUpScreens()
330
        self.terms += makeTerms( self.controllers, 'controller' )
331
        self.terms += makeTerms( self.switches, 'switch' )
332
        self.terms += makeTerms( self.hosts, 'host' )
333

    
334
    def stopXterms( self ):
335
        "Kill each xterm."
336
        for term in self.terms:
337
            os.kill( term.pid, signal.SIGKILL )
338
        cleanUpScreens()
339

    
340
    def staticArp( self ):
341
        "Add all-pairs ARP entries to remove the need to handle broadcast."
342
        for src in self.hosts:
343
            for dst in self.hosts:
344
                if src != dst:
345
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
346

    
347
    def start( self ):
348
        "Start controller and switches."
349
        if not self.built:
350
            self.build()
351
        info( '*** Starting controller\n' )
352
        for controller in self.controllers:
353
            controller.start()
354
        info( '*** Starting %s switches\n' % len( self.switches ) )
355
        for switch in self.switches:
356
            info( switch.name + ' ')
357
            switch.start( self.controllers )
358
        info( '\n' )
359

    
360
    def stop( self ):
361
        "Stop the controller(s), switches and hosts"
362
        if self.terms:
363
            info( '*** Stopping %i terms\n' % len( self.terms ) )
364
            self.stopXterms()
365
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
366
        for host in self.hosts:
367
            info( host.name + ' ' )
368
            host.terminate()
369
        info( '\n' )
370
        info( '*** Stopping %i switches\n' % len( self.switches ) )
371
        for switch in self.switches:
372
            info( switch.name + ' ' )
373
            switch.stop()
374
        info( '\n' )
375
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
376
        for controller in self.controllers:
377
            info( controller.name + ' ' )
378
            controller.stop()
379
        info( '\n*** Done\n' )
380

    
381
    def run( self, test, *args, **kwargs ):
382
        "Perform a complete start/test/stop cycle."
383
        self.start()
384
        info( '*** Running test\n' )
385
        result = test( *args, **kwargs )
386
        self.stop()
387
        return result
388

    
389
    def monitor( self, hosts=None, timeoutms=-1 ):
390
        """Monitor a set of hosts (or all hosts by default),
391
           and return their output, a line at a time.
392
           hosts: (optional) set of hosts to monitor
393
           timeoutms: (optional) timeout value in ms
394
           returns: iterator which returns host, line"""
395
        if hosts is None:
396
            hosts = self.hosts
397
        poller = select.poll()
398
        Node = hosts[ 0 ]  # so we can call class method fdToNode
399
        for host in hosts:
400
            poller.register( host.stdout )
401
        while True:
402
            ready = poller.poll( timeoutms )
403
            for fd, event in ready:
404
                host = Node.fdToNode( fd )
405
                if event & select.POLLIN:
406
                    line = host.readline()
407
                    if line is not None:
408
                        yield host, line
409
            # Return if non-blocking
410
            if not ready and timeoutms >= 0:
411
                yield None, None
412

    
413
    # XXX These test methods should be moved out of this class.
414
    # Probably we should create a tests.py for them
415

    
416
    @staticmethod
417
    def _parsePing( pingOutput ):
418
        "Parse ping output and return packets sent, received."
419
        # Check for downed link
420
        if 'connect: Network is unreachable' in pingOutput:
421
            return (1, 0)
422
        r = r'(\d+) packets transmitted, (\d+) received'
423
        m = re.search( r, pingOutput )
424
        if m is None:
425
            error( '*** Error: could not parse ping output: %s\n' %
426
                     pingOutput )
427
            return (1, 0)
428
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
429
        return sent, received
430

    
431
    def ping( self, hosts=None ):
432
        """Ping between all specified hosts.
433
           hosts: list of hosts
434
           returns: ploss packet loss percentage"""
435
        # should we check if running?
436
        packets = 0
437
        lost = 0
438
        ploss = None
439
        if not hosts:
440
            hosts = self.hosts
441
            output( '*** Ping: testing ping reachability\n' )
442
        for node in hosts:
443
            output( '%s -> ' % node.name )
444
            for dest in hosts:
445
                if node != dest:
446
                    result = node.cmd( 'ping -c1 ' + dest.IP() )
447
                    sent, received = self._parsePing( result )
448
                    packets += sent
449
                    if received > sent:
450
                        error( '*** Error: received too many packets' )
451
                        error( '%s' % result )
452
                        node.cmdPrint( 'route' )
453
                        exit( 1 )
454
                    lost += sent - received
455
                    output( ( '%s ' % dest.name ) if received else 'X ' )
456
            output( '\n' )
457
            ploss = 100 * lost / packets
458
        output( "*** Results: %i%% dropped (%d/%d lost)\n" %
459
                ( ploss, lost, packets ) )
460
        return ploss
461

    
462
    def pingAll( self ):
463
        """Ping between all hosts.
464
           returns: ploss packet loss percentage"""
465
        return self.ping()
466

    
467
    def pingPair( self ):
468
        """Ping between first two hosts, useful for testing.
469
           returns: ploss packet loss percentage"""
470
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
471
        return self.ping( hosts=hosts )
472

    
473
    @staticmethod
474
    def _parseIperf( iperfOutput ):
475
        """Parse iperf output and return bandwidth.
476
           iperfOutput: string
477
           returns: result string"""
478
        r = r'([\d\.]+ \w+/sec)'
479
        m = re.findall( r, iperfOutput )
480
        if m:
481
            return m[-1]
482
        else:
483
            # was: raise Exception(...)
484
            error( 'could not parse iperf output: ' + iperfOutput )
485
            return ''
486

    
487
    # XXX This should be cleaned up
488

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

    
533
    # BL: I think this can be rewritten now that we have
534
    # a real link class.
535
    def configLinkStatus( self, src, dst, status ):
536
        """Change status of src <-> dst links.
537
           src: node name
538
           dst: node name
539
           status: string {up, down}"""
540
        if src not in self.nameToNode:
541
            error( 'src not in network: %s\n' % src )
542
        elif dst not in self.nameToNode:
543
            error( 'dst not in network: %s\n' % dst )
544
        else:
545
            if type( src ) is str:
546
                src = self.nameToNode[ src ]
547
            if type( dst ) is str:
548
                dst = self.nameToNode[ dst ]
549
            connections = src.connectionsTo( dst )
550
            if len( connections ) == 0:
551
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
552
            for srcIntf, dstIntf in connections:
553
                result = srcIntf.ifconfig( status )
554
                if result:
555
                    error( 'link src status change failed: %s\n' % result )
556
                result = dstIntf.ifconfig( status )
557
                if result:
558
                    error( 'link dst status change failed: %s\n' % result )
559

    
560
    def interact( self ):
561
        "Start network and run our simple CLI."
562
        self.start()
563
        result = CLI( self )
564
        self.stop()
565
        return result
566

    
567
    inited = False
568

    
569
    @classmethod
570
    def init( cls ):
571
        "Initialize Mininet"
572
        if cls.inited:
573
            return
574
        if os.getuid() != 0:
575
            # Note: this script must be run as root
576
            # Probably we should only sudo when we need
577
            # to as per Big Switch's patch
578
            print "*** Mininet must run as root."
579
            exit( 1 )
580
        fixLimits()
581
        cls.inited = True
582

    
583

    
584
class MininetWithControlNet( Mininet ):
585

    
586
    """Control network support:
587

588
       Create an explicit control network. Currently this is only
589
       used/usable with the user datapath.
590

591
       Notes:
592

593
       1. If the controller and switches are in the same (e.g. root)
594
          namespace, they can just use the loopback connection.
595

596
       2. If we can get unix domain sockets to work, we can use them
597
          instead of an explicit control network.
598

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

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

605
       5. Basically nobody ever used this code, so it has been moved
606
          into its own class.
607

608
       6. Ultimately we may wish to extend this to allow us to create a
609
          control network which every node's control interface is
610
          attached to."""
611

    
612
    def configureControlNetwork( self ):
613
        "Configure control network."
614
        self.configureRoutedControlNetwork()
615

    
616
    # We still need to figure out the right way to pass
617
    # in the control network location.
618

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