Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ a9c28885

History | View | Annotate | Download (23 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
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, listenPort=None ):
111
        """Create Mininet object.
112
           topo: Topo (topology) object or None
113
           switch: default Switch class
114
           host: default Host class/constructor
115
           controller: default Controller class/constructor
116
           link: default Link class/constructor
117
           intf: default Intf class/constructor
118
           ipBase: base IP address for hosts,
119
           build: build now from topo?
120
           xterms: if build now, spawn xterms?
121
           cleanup: if build now, cleanup before creating?
122
           inNamespace: spawn switches and controller in net namespaces?
123
           autoSetMacs: set MAC addrs from topo dpid?
124
           autoStaticArp: set all-pairs static MAC addrs?
125
           listenPort: base listening port to open; will be incremented for
126
               each additional switch in the net if inNamespace=False"""
127
        self.topo = topo
128
        self.switch = switch
129
        self.host = host
130
        self.controller = controller
131
        self.link = link
132
        self.intf = intf
133
        self.ipBase = ipBase
134
        self.ipBaseNum, self.prefixLen = netParse( self.ipBase )
135
        self.nextIP = 1  # start for address allocation
136
        self.inNamespace = inNamespace
137
        self.xterms = xterms
138
        self.cleanup = cleanup
139
        self.autoSetMacs = autoSetMacs
140
        self.autoStaticArp = autoStaticArp
141
        self.listenPort = listenPort
142

    
143
        self.hosts = []
144
        self.switches = []
145
        self.controllers = []
146

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

    
149
        self.terms = []  # list of spawned xterm processes
150

    
151
        Mininet.init()  # Initialize Mininet if necessary
152

    
153
        self.built = False
154
        if topo and build:
155
            self.build()
156

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

    
178
    def addSwitch( self, name, cls=None, **params ):
179
        """Add switch.
180
           name: name of switch to add
181
           cls: custom switch class/constructor (optional)
182
           returns: added switch
183
           side effect: increments listenPort ivar ."""
184
        defaults = { 'listenPort': self.listenPort,
185
                     'inNamespace': self.inNamespace }
186
        defaults.update( params )
187
        if not cls:
188
            cls = self.switch
189
        sw = cls( name, **defaults )
190
        if not self.inNamespace and self.listenPort:
191
            self.listenPort += 1
192
        self.switches.append( sw )
193
        self.nameToNode[ name ] = sw
194
        return sw
195

    
196
    def addController( self, name='c0', controller=None, **params ):
197
        """Add controller.
198
           controller: Controller class"""
199
        if not controller:
200
            controller = self.controller
201
        controller_new = controller( name, **params )
202
        if controller_new:  # allow controller-less setups
203
            self.controllers.append( controller_new )
204
            self.nameToNode[ name ] = controller_new
205
        return controller_new
206

    
207
    # BL: is this better than just using nameToNode[] ?
208
    # Should it have a better name?
209
    def getNodeByName( self, nodeName ):
210
        "Return node with given name"
211
        return self.nameToNode[ nodeName ]
212

    
213
    def addLink( self, node1, node2, port1=None, port2=None,
214
                 cls=None, **params ):
215
        """"Add a link from node1 to node2
216
            node1: source node
217
            node2: dest node
218
            port1: source port
219
            port2: dest port
220
            returns: link object"""
221
        defaults = { 'port1': port1,
222
                     'port2': port2,
223
                     'intf': self.intf }
224
        defaults.update( params )
225
        if not cls:
226
            cls = self.link
227
        return cls( node1, node2, **defaults )
228

    
229
    def configHosts( self ):
230
        "Configure a set of hosts."
231
        for host in self.hosts:
232
            info( host.name + ' ' )
233
            host.configDefault( defaultRoute=host.defaultIntf )
234
            # You're low priority, dude!
235
            # BL: do we want to do this here or not?
236
            # May not make sense if we have CPU lmiting...
237
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
238
            # This may not be the right place to do this, but
239
            # it needs to be done somewhere.
240
            host.cmd( 'ifconfig lo up' )
241
        info( '\n' )
242

    
243
    def buildFromTopo( self, topo=None ):
244
        """Build mininet from a topology object
245
           At the end of this function, everything should be connected
246
           and up."""
247

    
248
        # Possibly we should clean up here and/or validate
249
        # the topo
250
        if self.cleanup:
251
            pass
252

    
253
        info( '*** Creating network\n' )
254

    
255
        if not self.controllers:
256
            # Add a default controller
257
            info( '*** Adding controller\n' )
258
            self.addController( 'c0' )
259

    
260
        info( '*** Adding hosts:\n' )
261
        for hostName in topo.hosts():
262
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
263
            info( hostName + ' ' )
264

    
265
        info( '\n*** Adding switches:\n' )
266
        for switchName in topo.switches():
267
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
268
            info( switchName + ' ' )
269

    
270
        info( '\n*** Adding links:\n' )
271
        for srcName, dstName in topo.links(sort=True):
272
            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
273
            params = topo.linkInfo( srcName, dstName )
274
            srcPort, dstPort = topo.port( srcName, dstName )
275
            self.addLink( src, dst, srcPort, dstPort, **params )
276
            info( '(%s, %s) ' % ( src.name, dst.name ) )
277

    
278
        info( '\n' )
279

    
280
    def configureControlNetwork( self ):
281
        "Control net config hook: override in subclass"
282
        raise Exception( 'configureControlNetwork: '
283
               'should be overriden in subclass', self )
284

    
285
    def build( self ):
286
        "Build mininet."
287
        if self.topo:
288
            self.buildFromTopo( self.topo )
289
        if ( self.inNamespace ):
290
            self.configureControlNetwork()
291
        info( '*** Configuring hosts\n' )
292
        self.configHosts()
293
        if self.xterms:
294
            self.startTerms()
295
        if self.autoStaticArp:
296
            self.staticArp()
297
        self.built = True
298

    
299
    def startTerms( self ):
300
        "Start a terminal for each node."
301
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
302
        cleanUpScreens()
303
        self.terms += makeTerms( self.controllers, 'controller' )
304
        self.terms += makeTerms( self.switches, 'switch' )
305
        self.terms += makeTerms( self.hosts, 'host' )
306

    
307
    def stopXterms( self ):
308
        "Kill each xterm."
309
        for term in self.terms:
310
            os.kill( term.pid, signal.SIGKILL )
311
        cleanUpScreens()
312

    
313
    def staticArp( self ):
314
        "Add all-pairs ARP entries to remove the need to handle broadcast."
315
        for src in self.hosts:
316
            for dst in self.hosts:
317
                if src != dst:
318
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
319

    
320
    def start( self ):
321
        "Start controller and switches."
322
        if not self.built:
323
            self.build()
324
        info( '*** Starting controller\n' )
325
        for controller in self.controllers:
326
            controller.start()
327
        info( '*** Starting %s switches\n' % len( self.switches ) )
328
        for switch in self.switches:
329
            info( switch.name + ' ')
330
            switch.start( self.controllers )
331
        info( '\n' )
332

    
333
    def stop( self ):
334
        "Stop the controller(s), switches and hosts"
335
        if self.terms:
336
            info( '*** Stopping %i terms\n' % len( self.terms ) )
337
            self.stopXterms()
338
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
339
        for host in self.hosts:
340
            info( host.name + ' ' )
341
            host.terminate()
342
        info( '\n' )
343
        info( '*** Stopping %i switches\n' % len( self.switches ) )
344
        for switch in self.switches:
345
            info( switch.name + ' ' )
346
            switch.stop()
347
        info( '\n' )
348
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
349
        for controller in self.controllers:
350
            info( controller.name + ' ' )
351
            controller.stop()
352
        info( '\n*** Done\n' )
353

    
354
    def run( self, test, *args, **kwargs ):
355
        "Perform a complete start/test/stop cycle."
356
        self.start()
357
        info( '*** Running test\n' )
358
        result = test( *args, **kwargs )
359
        self.stop()
360
        return result
361

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

    
386
    # XXX These test methods should be moved out of this class.
387
    # Probably we should create a tests.py for them
388

    
389
    @staticmethod
390
    def _parsePing( pingOutput ):
391
        "Parse ping output and return packets sent, received."
392
        # Check for downed link
393
        if 'connect: Network is unreachable' in pingOutput:
394
            return (1, 0)
395
        r = r'(\d+) packets transmitted, (\d+) received'
396
        m = re.search( r, pingOutput )
397
        if m == None:
398
            error( '*** Error: could not parse ping output: %s\n' %
399
                     pingOutput )
400
            return (1, 0)
401
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
402
        return sent, received
403

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

    
435
    def pingAll( self ):
436
        """Ping between all hosts.
437
           returns: ploss packet loss percentage"""
438
        return self.ping()
439

    
440
    def pingPair( self ):
441
        """Ping between first two hosts, useful for testing.
442
           returns: ploss packet loss percentage"""
443
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
444
        return self.ping( hosts=hosts )
445

    
446
    @staticmethod
447
    def _parseIperf( iperfOutput ):
448
        """Parse iperf output and return bandwidth.
449
           iperfOutput: string
450
           returns: result string"""
451
        r = r'([\d\.]+ \w+/sec)'
452
        m = re.findall( r, iperfOutput )
453
        if m:
454
            return m[-1]
455
        else:
456
            # was: raise Exception(...)
457
            error( 'could not parse iperf output: ' + iperfOutput )
458
            return ''
459

    
460
    # XXX This should be cleaned up
461

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

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

    
533
    def interact( self ):
534
        "Start network and run our simple CLI."
535
        self.start()
536
        result = CLI( self )
537
        self.stop()
538
        return result
539

    
540
    inited = False
541

    
542
    @classmethod
543
    def init( cls ):
544
        "Initialize Mininet"
545
        if cls.inited:
546
            return
547
        if os.getuid() != 0:
548
            # Note: this script must be run as root
549
            # Probably we should only sudo when we need
550
            # to as per Big Switch's patch
551
            print "*** Mininet must run as root."
552
            exit( 1 )
553
        fixLimits()
554
        cls.inited = True
555

    
556

    
557
class MininetWithControlNet( Mininet ):
558

    
559
    """Control network support:
560

561
       Create an explicit control network. Currently this is only
562
       used/usable with the user datapath.
563

564
       Notes:
565

566
       1. If the controller and switches are in the same (e.g. root)
567
          namespace, they can just use the loopback connection.
568

569
       2. If we can get unix domain sockets to work, we can use them
570
          instead of an explicit control network.
571

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

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

578
       5. Basically nobody ever used this code, so it has been moved
579
          into its own class.
580

581
       6. Ultimately we may wish to extend this to allow us to create a
582
          control network which every node's control interface is
583
          attached to."""
584

    
585
    def configureControlNetwork( self ):
586
        "Configure control network."
587
        self.configureRoutedControlNetwork()
588

    
589
    # We still need to figure out the right way to pass
590
    # in the control network location.
591

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