Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ a4e93368

History | View | Annotate | Download (35.8 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
import random
94

    
95
from time import sleep
96
from itertools import chain, groupby
97
from math import ceil
98

    
99
from mininet.cli import CLI
100
from mininet.log import info, error, debug, output, warn
101
from mininet.node import ( Node, Host, OVSKernelSwitch, DefaultController,
102
                           Controller )
103
from mininet.nodelib import NAT
104
from mininet.link import Link, Intf
105
from mininet.util import quietRun, fixLimits, numCores, ensureRoot
106
from mininet.util import macColonHex, ipStr, ipParse, netParse, ipAdd
107
from mininet.term import cleanUpScreens, makeTerms
108

    
109
# Mininet version: should be consistent with README and LICENSE
110
VERSION = "2.2.0+"
111

    
112
class Mininet( object ):
113
    "Network emulation with hosts spawned in network namespaces."
114

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

    
158
        self.hosts = []
159
        self.switches = []
160
        self.controllers = []
161
        self.links = []
162

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

    
165
        self.terms = []  # list of spawned xterm processes
166

    
167
        Mininet.init()  # Initialize Mininet if necessary
168

    
169
        self.built = False
170
        if topo and build:
171
            self.build()
172

    
173
    def waitConnected( self, timeout=None, delay=.5 ):
174
        """wait for each switch to connect to a controller,
175
           up to 5 seconds
176
           timeout: time to wait, or None to wait indefinitely
177
           delay: seconds to sleep per iteration
178
           returns: True if all switches are connected"""
179
        info( '*** Waiting for switches to connect\n' )
180
        time = 0
181
        remaining = list( self.switches )
182
        while True:
183
            for switch in tuple( remaining ):
184
                if switch.connected():
185
                    info( '%s ' % switch )
186
                    remaining.remove( switch )
187
            if not remaining:
188
                info( '\n' )
189
                return True
190
            if time > timeout and timeout is not None:
191
                break
192
            sleep( delay )
193
            time += delay
194
        warn( 'Timed out after %d seconds\n' % time )
195
        for switch in remaining:
196
            if not switch.connected():
197
                warn( 'Warning: %s is not connected to a controller\n'
198
                      % switch.name )
199
            else:
200
                remaining.remove( switch )
201
        return not remaining
202

    
203
    def addHost( self, name, cls=None, **params ):
204
        """Add host.
205
           name: name of host to add
206
           cls: custom host class/constructor (optional)
207
           params: parameters for host
208
           returns: added host"""
209
        # Default IP and MAC addresses
210
        defaults = { 'ip': ipAdd( self.nextIP,
211
                                  ipBaseNum=self.ipBaseNum,
212
                                  prefixLen=self.prefixLen ) +
213
                                  '/%s' % self.prefixLen }
214
        if self.autoSetMacs:
215
            defaults[ 'mac' ] = macColonHex( self.nextIP )
216
        if self.autoPinCpus:
217
            defaults[ 'cores' ] = self.nextCore
218
            self.nextCore = ( self.nextCore + 1 ) % self.numCores
219
        self.nextIP += 1
220
        defaults.update( params )
221
        if not cls:
222
            cls = self.host
223
        h = cls( name, **defaults )
224
        self.hosts.append( h )
225
        self.nameToNode[ name ] = h
226
        return h
227

    
228
    def addSwitch( self, name, cls=None, **params ):
229
        """Add switch.
230
           name: name of switch to add
231
           cls: custom switch class/constructor (optional)
232
           returns: added switch
233
           side effect: increments listenPort ivar ."""
234
        defaults = { 'listenPort': self.listenPort,
235
                     'inNamespace': self.inNamespace }
236
        defaults.update( params )
237
        if not cls:
238
            cls = self.switch
239
        sw = cls( name, **defaults )
240
        if not self.inNamespace and self.listenPort:
241
            self.listenPort += 1
242
        self.switches.append( sw )
243
        self.nameToNode[ name ] = sw
244
        return sw
245

    
246
    def addController( self, name='c0', controller=None, **params ):
247
        """Add controller.
248
           controller: Controller class"""
249
        # Get controller class
250
        if not controller:
251
            controller = self.controller
252
        # Construct new controller if one is not given
253
        if isinstance( name, Controller ):
254
            controller_new = name
255
            # Pylint thinks controller is a str()
256
            # pylint: disable=maybe-no-member
257
            name = controller_new.name
258
            # pylint: enable=maybe-no-member
259
        else:
260
            controller_new = controller( name, **params )
261
        # Add new controller to net
262
        if controller_new:  # allow controller-less setups
263
            self.controllers.append( controller_new )
264
            self.nameToNode[ name ] = controller_new
265
        return controller_new
266

    
267
    def addNAT( self, name='nat0', connect=True, inNamespace=False,
268
                **params):
269
        """Add a NAT to the Mininet network
270
           name: name of NAT node
271
           connect: switch to connect to | True (s1) | None
272
           inNamespace: create in a network namespace
273
           params: other NAT node params, notably:
274
               ip: used as default gateway address"""
275
        nat = self.addHost( name, cls=NAT, inNamespace=inNamespace,
276
                            subnet=self.ipBase, **params )
277
        # find first switch and create link
278
        if connect:
279
            if not isinstance( connect, Node ):
280
                # Use first switch if not specified
281
                connect = self.switches[ 0 ]
282
            # Connect the nat to the switch
283
            self.addLink( nat, self.switches[ 0 ] )
284
            # Set the default route on hosts
285
            natIP = nat.params[ 'ip' ].split('/')[ 0 ]
286
            for host in self.hosts:
287
                if host.inNamespace:
288
                    host.setDefaultRoute( 'via %s' % natIP )
289
        return nat
290

    
291
    # BL: We now have four ways to look up nodes
292
    # This may (should?) be cleaned up in the future.
293
    def getNodeByName( self, *args ):
294
        "Return node(s) with given name(s)"
295
        if len( args ) == 1:
296
            return self.nameToNode[ args[ 0 ] ]
297
        return [ self.nameToNode[ n ] for n in args ]
298

    
299
    def get( self, *args ):
300
        "Convenience alias for getNodeByName"
301
        return self.getNodeByName( *args )
302

    
303
    # Even more convenient syntax for node lookup and iteration
304
    def __getitem__( self, key ):
305
        """net [ name ] operator: Return node(s) with given name(s)"""
306
        return self.nameToNode[ key ]
307

    
308
    def __iter__( self ):
309
        "return iterator over node names"
310
        for node in chain( self.hosts, self.switches, self.controllers ):
311
            yield node.name
312

    
313
    def __len__( self ):
314
        "returns number of nodes in net"
315
        return ( len( self.hosts ) + len( self.switches ) +
316
                 len( self.controllers ) )
317

    
318
    def __contains__( self, item ):
319
        "returns True if net contains named node"
320
        return item in self.nameToNode
321

    
322
    def keys( self ):
323
        "return a list of all node names or net's keys"
324
        return list( self )
325

    
326
    def values( self ):
327
        "return a list of all nodes or net's values"
328
        return [ self[name] for name in self ]
329

    
330
    def items( self ):
331
        "return (key,value) tuple list for every node in net"
332
        return zip( self.keys(), self.values() )
333

    
334
    @staticmethod
335
    def randMac():
336
        "Return a random, non-multicast MAC address"
337
        return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff |
338
                            0x020000000000 )
339

    
340
    def addLink( self, node1, node2, port1=None, port2=None,
341
                 cls=None, **params ):
342
        """"Add a link from node1 to node2
343
            node1: source node (or name)
344
            node2: dest node (or name)
345
            port1: source port (optional)
346
            port2: dest port (optional)
347
            cls: link class (optional)
348
            params: additional link params (optional)
349
            returns: link object"""
350
        # Accept node objects or names
351
        node1 = node1 if not isinstance( node1, basestring ) else self[ node1 ]
352
        node2 = node2 if not isinstance( node2, basestring ) else self[ node2 ]
353
        options = dict( params )
354
        # Port is optional
355
        if port1 is not None:
356
            options.setdefault( 'port1', port1 )
357
        if port2 is not None:
358
            options.setdefault( 'port2', port2 )
359
        # Set default MAC - this should probably be in Link
360
        options.setdefault( 'addr1', self.randMac() )
361
        options.setdefault( 'addr2', self.randMac() )
362
        cls = self.link if cls is None else cls
363
        link = cls( node1, node2, **options )
364
        self.links.append( link )
365
        return link
366

    
367
    def configHosts( self ):
368
        "Configure a set of hosts."
369
        for host in self.hosts:
370
            info( host.name + ' ' )
371
            intf = host.defaultIntf()
372
            if intf:
373
                host.configDefault()
374
            else:
375
                # Don't configure nonexistent intf
376
                host.configDefault( ip=None, mac=None )
377
            # You're low priority, dude!
378
            # BL: do we want to do this here or not?
379
            # May not make sense if we have CPU lmiting...
380
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
381
            # This may not be the right place to do this, but
382
            # it needs to be done somewhere.
383
        info( '\n' )
384

    
385
    def buildFromTopo( self, topo=None ):
386
        """Build mininet from a topology object
387
           At the end of this function, everything should be connected
388
           and up."""
389

    
390
        # Possibly we should clean up here and/or validate
391
        # the topo
392
        if self.cleanup:
393
            pass
394

    
395
        info( '*** Creating network\n' )
396

    
397
        if not self.controllers and self.controller:
398
            # Add a default controller
399
            info( '*** Adding controller\n' )
400
            classes = self.controller
401
            if not isinstance( classes, list ):
402
                classes = [ classes ]
403
            for i, cls in enumerate( classes ):
404
                # Allow Controller objects because nobody understands currying
405
                if isinstance( cls, Controller ):
406
                    self.addController( cls )
407
                else:
408
                    self.addController( 'c%d' % i, cls )
409

    
410
        info( '*** Adding hosts:\n' )
411
        for hostName in topo.hosts():
412
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
413
            info( hostName + ' ' )
414

    
415
        info( '\n*** Adding switches:\n' )
416
        for switchName in topo.switches():
417
            # A bit ugly: add batch parameter if appropriate
418
            params = topo.nodeInfo( switchName)
419
            cls = params.get( 'cls', self.switch )
420
            if hasattr( cls, 'batchStartup' ):
421
                params.setdefault( 'batch', True )
422
            self.addSwitch( switchName, **params )
423
            info( switchName + ' ' )
424

    
425
        info( '\n*** Adding links:\n' )
426
        for srcName, dstName, params in topo.links(
427
                sort=True, withInfo=True ):
428
            self.addLink( **params )
429
            info( '(%s, %s) ' % ( srcName, dstName ) )
430

    
431
        info( '\n' )
432

    
433
    def configureControlNetwork( self ):
434
        "Control net config hook: override in subclass"
435
        raise Exception( 'configureControlNetwork: '
436
                         'should be overriden in subclass', self )
437

    
438
    def build( self ):
439
        "Build mininet."
440
        if self.topo:
441
            self.buildFromTopo( self.topo )
442
        if self.inNamespace:
443
            self.configureControlNetwork()
444
        info( '*** Configuring hosts\n' )
445
        self.configHosts()
446
        if self.xterms:
447
            self.startTerms()
448
        if self.autoStaticArp:
449
            self.staticArp()
450
        self.built = True
451

    
452
    def startTerms( self ):
453
        "Start a terminal for each node."
454
        if 'DISPLAY' not in os.environ:
455
            error( "Error starting terms: Cannot connect to display\n" )
456
            return
457
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
458
        cleanUpScreens()
459
        self.terms += makeTerms( self.controllers, 'controller' )
460
        self.terms += makeTerms( self.switches, 'switch' )
461
        self.terms += makeTerms( self.hosts, 'host' )
462

    
463
    def stopXterms( self ):
464
        "Kill each xterm."
465
        for term in self.terms:
466
            os.kill( term.pid, signal.SIGKILL )
467
        cleanUpScreens()
468

    
469
    def staticArp( self ):
470
        "Add all-pairs ARP entries to remove the need to handle broadcast."
471
        for src in self.hosts:
472
            for dst in self.hosts:
473
                if src != dst:
474
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
475

    
476
    def start( self ):
477
        "Start controller and switches."
478
        if not self.built:
479
            self.build()
480
        info( '*** Starting controller\n' )
481
        for controller in self.controllers:
482
            info( controller.name + ' ')
483
            controller.start()
484
        info( '\n' )
485
        info( '*** Starting %s switches\n' % len( self.switches ) )
486
        for switch in self.switches:
487
            info( switch.name + ' ')
488
            switch.start( self.controllers )
489
        started = {}
490
        for swclass, switches in groupby(
491
            sorted( self.switches, key=type ), type ):
492
            switches = tuple( switches )
493
            if hasattr( swclass, 'batchStartup' ):
494
                success = swclass.batchStartup( switches )
495
                started.update( { s: s for s in success } )
496
        info( '\n' )
497
        if self.waitConn:
498
            self.waitConnected()
499

    
500
    def stop( self ):
501
        "Stop the controller(s), switches and hosts"
502
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
503
        for controller in self.controllers:
504
            info( controller.name + ' ' )
505
            controller.stop()
506
        info( '\n' )
507
        if self.terms:
508
            info( '*** Stopping %i terms\n' % len( self.terms ) )
509
            self.stopXterms()
510
        info( '*** Stopping %i links\n' % len( self.links ) )
511
        for link in self.links:
512
            info( '.' )
513
            link.stop()
514
        info( '\n' )
515
        info( '*** Stopping %i switches\n' % len( self.switches ) )
516
        stopped = {}
517
        for swclass, switches in groupby(
518
                sorted( self.switches, key=type ), type ):
519
            switches = tuple( switches )
520
            if hasattr( swclass, 'batchShutdown' ):
521
                success = swclass.batchShutdown( switches )
522
                stopped.update( { s: s for s in success } )
523
        for switch in self.switches:
524
            info( switch.name + ' ' )
525
            if switch not in stopped:
526
                switch.stop()
527
            switch.terminate()
528
        info( '\n' )
529
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
530
        for host in self.hosts:
531
            info( host.name + ' ' )
532
            host.terminate()
533
        info( '\n*** Done\n' )
534

    
535
    def run( self, test, *args, **kwargs ):
536
        "Perform a complete start/test/stop cycle."
537
        self.start()
538
        info( '*** Running test\n' )
539
        result = test( *args, **kwargs )
540
        self.stop()
541
        return result
542

    
543
    def monitor( self, hosts=None, timeoutms=-1 ):
544
        """Monitor a set of hosts (or all hosts by default),
545
           and return their output, a line at a time.
546
           hosts: (optional) set of hosts to monitor
547
           timeoutms: (optional) timeout value in ms
548
           returns: iterator which returns host, line"""
549
        if hosts is None:
550
            hosts = self.hosts
551
        poller = select.poll()
552
        h1 = hosts[ 0 ]  # so we can call class method fdToNode
553
        for host in hosts:
554
            poller.register( host.stdout )
555
        while True:
556
            ready = poller.poll( timeoutms )
557
            for fd, event in ready:
558
                host = h1.fdToNode( fd )
559
                if event & select.POLLIN:
560
                    line = host.readline()
561
                    if line is not None:
562
                        yield host, line
563
            # Return if non-blocking
564
            if not ready and timeoutms >= 0:
565
                yield None, None
566

    
567
    # XXX These test methods should be moved out of this class.
568
    # Probably we should create a tests.py for them
569

    
570
    @staticmethod
571
    def _parsePing( pingOutput ):
572
        "Parse ping output and return packets sent, received."
573
        # Check for downed link
574
        if 'connect: Network is unreachable' in pingOutput:
575
            return 1, 0
576
        r = r'(\d+) packets transmitted, (\d+) received'
577
        m = re.search( r, pingOutput )
578
        if m is None:
579
            error( '*** Error: could not parse ping output: %s\n' %
580
                   pingOutput )
581
            return 1, 0
582
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
583
        return sent, received
584

    
585
    def ping( self, hosts=None, timeout=None ):
586
        """Ping between all specified hosts.
587
           hosts: list of hosts
588
           timeout: time to wait for a response, as string
589
           returns: ploss packet loss percentage"""
590
        # should we check if running?
591
        packets = 0
592
        lost = 0
593
        ploss = None
594
        if not hosts:
595
            hosts = self.hosts
596
            output( '*** Ping: testing ping reachability\n' )
597
        for node in hosts:
598
            output( '%s -> ' % node.name )
599
            for dest in hosts:
600
                if node != dest:
601
                    opts = ''
602
                    if timeout:
603
                        opts = '-W %s' % timeout
604
                    if dest.intfs:
605
                        result = node.cmd( 'ping -c1 %s %s' %
606
                                           (opts, dest.IP()) )
607
                        sent, received = self._parsePing( result )
608
                    else:
609
                        sent, received = 0, 0
610
                    packets += sent
611
                    if received > sent:
612
                        error( '*** Error: received too many packets' )
613
                        error( '%s' % result )
614
                        node.cmdPrint( 'route' )
615
                        exit( 1 )
616
                    lost += sent - received
617
                    output( ( '%s ' % dest.name ) if received else 'X ' )
618
            output( '\n' )
619
        if packets > 0:
620
            ploss = 100.0 * lost / packets
621
            received = packets - lost
622
            output( "*** Results: %i%% dropped (%d/%d received)\n" %
623
                    ( ploss, received, packets ) )
624
        else:
625
            ploss = 0
626
            output( "*** Warning: No packets sent\n" )
627
        return ploss
628

    
629
    @staticmethod
630
    def _parsePingFull( pingOutput ):
631
        "Parse ping output and return all data."
632
        errorTuple = (1, 0, 0, 0, 0, 0)
633
        # Check for downed link
634
        r = r'[uU]nreachable'
635
        m = re.search( r, pingOutput )
636
        if m is not None:
637
            return errorTuple
638
        r = r'(\d+) packets transmitted, (\d+) received'
639
        m = re.search( r, pingOutput )
640
        if m is None:
641
            error( '*** Error: could not parse ping output: %s\n' %
642
                   pingOutput )
643
            return errorTuple
644
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
645
        r = r'rtt min/avg/max/mdev = '
646
        r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms'
647
        m = re.search( r, pingOutput )
648
        if m is None:
649
            if received == 0:
650
                return errorTuple
651
            error( '*** Error: could not parse ping output: %s\n' %
652
                   pingOutput )
653
            return errorTuple
654
        rttmin = float( m.group( 1 ) )
655
        rttavg = float( m.group( 2 ) )
656
        rttmax = float( m.group( 3 ) )
657
        rttdev = float( m.group( 4 ) )
658
        return sent, received, rttmin, rttavg, rttmax, rttdev
659

    
660
    def pingFull( self, hosts=None, timeout=None ):
661
        """Ping between all specified hosts and return all data.
662
           hosts: list of hosts
663
           timeout: time to wait for a response, as string
664
           returns: all ping data; see function body."""
665
        # should we check if running?
666
        # Each value is a tuple: (src, dsd, [all ping outputs])
667
        all_outputs = []
668
        if not hosts:
669
            hosts = self.hosts
670
            output( '*** Ping: testing ping reachability\n' )
671
        for node in hosts:
672
            output( '%s -> ' % node.name )
673
            for dest in hosts:
674
                if node != dest:
675
                    opts = ''
676
                    if timeout:
677
                        opts = '-W %s' % timeout
678
                    result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
679
                    outputs = self._parsePingFull( result )
680
                    sent, received, rttmin, rttavg, rttmax, rttdev = outputs
681
                    all_outputs.append( (node, dest, outputs) )
682
                    output( ( '%s ' % dest.name ) if received else 'X ' )
683
            output( '\n' )
684
        output( "*** Results: \n" )
685
        for outputs in all_outputs:
686
            src, dest, ping_outputs = outputs
687
            sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs
688
            output( " %s->%s: %s/%s, " % (src, dest, sent, received ) )
689
            output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" %
690
                    (rttmin, rttavg, rttmax, rttdev) )
691
        return all_outputs
692

    
693
    def pingAll( self, timeout=None ):
694
        """Ping between all hosts.
695
           returns: ploss packet loss percentage"""
696
        return self.ping( timeout=timeout )
697

    
698
    def pingPair( self ):
699
        """Ping between first two hosts, useful for testing.
700
           returns: ploss packet loss percentage"""
701
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
702
        return self.ping( hosts=hosts )
703

    
704
    def pingAllFull( self ):
705
        """Ping between all hosts.
706
           returns: ploss packet loss percentage"""
707
        return self.pingFull()
708

    
709
    def pingPairFull( self ):
710
        """Ping between first two hosts, useful for testing.
711
           returns: ploss packet loss percentage"""
712
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
713
        return self.pingFull( hosts=hosts )
714

    
715
    @staticmethod
716
    def _parseIperf( iperfOutput ):
717
        """Parse iperf output and return bandwidth.
718
           iperfOutput: string
719
           returns: result string"""
720
        r = r'([\d\.]+ \w+/sec)'
721
        m = re.findall( r, iperfOutput )
722
        if m:
723
            return m[-1]
724
        else:
725
            # was: raise Exception(...)
726
            error( 'could not parse iperf output: ' + iperfOutput )
727
            return ''
728

    
729
    # XXX This should be cleaned up
730

    
731
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M', fmt=None,
732
               seconds=5):
733
        """Run iperf between two hosts.
734
           hosts: list of hosts; if None, uses first and last hosts
735
           l4Type: string, one of [ TCP, UDP ]
736
           udpBw: bandwidth target for UDP test
737
           fmt: iperf format argument if any
738
           seconds: iperf time to transmit
739
           returns: two-element array of [ server, client ] speeds
740
           note: send() is buffered, so client rate can be much higher than
741
           the actual transmission rate; on an unloaded system, server
742
           rate should be much closer to the actual receive rate"""
743
        if not quietRun( 'which telnet' ):
744
            error( 'Cannot find telnet in $PATH - required for iperf test' )
745
            return
746
        hosts = hosts or [ self.hosts[ 0 ], self.hosts[ -1 ] ]
747
        assert len( hosts ) == 2
748
        client, server = hosts
749
        output( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
750
        output( "%s and %s\n" % ( client.name, server.name ) )
751
        server.cmd( 'killall -9 iperf' )
752
        iperfArgs = 'iperf '
753
        bwArgs = ''
754
        if l4Type == 'UDP':
755
            iperfArgs += '-u '
756
            bwArgs = '-b ' + udpBw + ' '
757
        elif l4Type != 'TCP':
758
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
759
        if fmt:
760
            iperfArgs += '-f %s ' % fmt
761
        server.sendCmd( iperfArgs + '-s', printPid=True )
762
        servout = ''
763
        while server.lastPid is None:
764
            servout += server.monitor()
765
        if l4Type == 'TCP':
766
            while 'Connected' not in client.cmd(
767
                    'sh -c "echo A | telnet -e A %s 5001"' % server.IP()):
768
                info( 'Waiting for iperf to start up...' )
769
                sleep(.5)
770
        cliout = client.cmd( iperfArgs + '-t %d -c ' % seconds +
771
                             server.IP() + ' ' + bwArgs )
772
        debug( 'Client output: %s\n' % cliout )
773
        server.sendInt()
774
        servout += server.waitOutput()
775
        debug( 'Server output: %s\n' % servout )
776
        result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ]
777
        if l4Type == 'UDP':
778
            result.insert( 0, udpBw )
779
        output( '*** Results: %s\n' % result )
780
        return result
781

    
782
    def runCpuLimitTest( self, cpu, duration=5 ):
783
        """run CPU limit test with 'while true' processes.
784
        cpu: desired CPU fraction of each host
785
        duration: test duration in seconds (integer)
786
        returns a single list of measured CPU fractions as floats.
787
        """
788
        cores = int( quietRun( 'nproc' ) )
789
        pct = cpu * 100
790
        info( '*** Testing CPU %.0f%% bandwidth limit\n' % pct )
791
        hosts = self.hosts
792
        cores = int( quietRun( 'nproc' ) )
793
        # number of processes to run a while loop on per host
794
        num_procs = int( ceil( cores * cpu ) )
795
        pids = {}
796
        for h in hosts:
797
            pids[ h ] = []
798
            for _core in range( num_procs ):
799
                h.cmd( 'while true; do a=1; done &' )
800
                pids[ h ].append( h.cmd( 'echo $!' ).strip() )
801
        outputs = {}
802
        time = {}
803
        # get the initial cpu time for each host
804
        for host in hosts:
805
            outputs[ host ] = []
806
            with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' %
807
                       host, 'r' ) as f:
808
                time[ host ] = float( f.read() )
809
        for _ in range( duration ):
810
            sleep( 1 )
811
            for host in hosts:
812
                with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' %
813
                           host, 'r' ) as f:
814
                    readTime = float( f.read() )
815
                outputs[ host ].append( ( ( readTime - time[ host ] )
816
                                        / 1000000000 ) / cores * 100 )
817
                time[ host ] = readTime
818
        for h, pids in pids.items():
819
            for pid in pids:
820
                h.cmd( 'kill -9 %s' % pid )
821
        cpu_fractions = []
822
        for _host, outputs in outputs.items():
823
            for pct in outputs:
824
                cpu_fractions.append( pct )
825
        output( '*** Results: %s\n' % cpu_fractions )
826
        return cpu_fractions
827

    
828
    # BL: I think this can be rewritten now that we have
829
    # a real link class.
830
    def configLinkStatus( self, src, dst, status ):
831
        """Change status of src <-> dst links.
832
           src: node name
833
           dst: node name
834
           status: string {up, down}"""
835
        if src not in self.nameToNode:
836
            error( 'src not in network: %s\n' % src )
837
        elif dst not in self.nameToNode:
838
            error( 'dst not in network: %s\n' % dst )
839
        else:
840
            if isinstance( src, basestring ):
841
                src = self.nameToNode[ src ]
842
            if isinstance( dst, basestring ):
843
                dst = self.nameToNode[ dst ]
844
            connections = src.connectionsTo( dst )
845
            if len( connections ) == 0:
846
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
847
            for srcIntf, dstIntf in connections:
848
                result = srcIntf.ifconfig( status )
849
                if result:
850
                    error( 'link src status change failed: %s\n' % result )
851
                result = dstIntf.ifconfig( status )
852
                if result:
853
                    error( 'link dst status change failed: %s\n' % result )
854

    
855
    def interact( self ):
856
        "Start network and run our simple CLI."
857
        self.start()
858
        result = CLI( self )
859
        self.stop()
860
        return result
861

    
862
    inited = False
863

    
864
    @classmethod
865
    def init( cls ):
866
        "Initialize Mininet"
867
        if cls.inited:
868
            return
869
        ensureRoot()
870
        fixLimits()
871
        cls.inited = True
872

    
873

    
874
class MininetWithControlNet( Mininet ):
875

    
876
    """Control network support:
877

878
       Create an explicit control network. Currently this is only
879
       used/usable with the user datapath.
880

881
       Notes:
882

883
       1. If the controller and switches are in the same (e.g. root)
884
          namespace, they can just use the loopback connection.
885

886
       2. If we can get unix domain sockets to work, we can use them
887
          instead of an explicit control network.
888

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

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

895
       5. Basically nobody ever used this code, so it has been moved
896
          into its own class.
897

898
       6. Ultimately we may wish to extend this to allow us to create a
899
          control network which every node's control interface is
900
          attached to."""
901

    
902
    def configureControlNetwork( self ):
903
        "Configure control network."
904
        self.configureRoutedControlNetwork()
905

    
906
    # We still need to figure out the right way to pass
907
    # in the control network location.
908

    
909
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
910
                                       prefixLen=16 ):
911
        """Configure a routed control network on controller and switches.
912
           For use with the user datapath only right now."""
913
        controller = self.controllers[ 0 ]
914
        info( controller.name + ' <->' )
915
        cip = ip
916
        snum = ipParse( ip )
917
        for switch in self.switches:
918
            info( ' ' + switch.name )
919
            link = self.link( switch, controller, port1=0 )
920
            sintf, cintf = link.intf1, link.intf2
921
            switch.controlIntf = sintf
922
            snum += 1
923
            while snum & 0xff in [ 0, 255 ]:
924
                snum += 1
925
            sip = ipStr( snum )
926
            cintf.setIP( cip, prefixLen )
927
            sintf.setIP( sip, prefixLen )
928
            controller.setHostRoute( sip, cintf )
929
            switch.setHostRoute( cip, sintf )
930
        info( '\n' )
931
        info( '*** Testing control network\n' )
932
        while not cintf.isUp():
933
            info( '*** Waiting for', cintf, 'to come up\n' )
934
            sleep( 1 )
935
        for switch in self.switches:
936
            while not sintf.isUp():
937
                info( '*** Waiting for', sintf, 'to come up\n' )
938
                sleep( 1 )
939
            if self.ping( hosts=[ switch, controller ] ) != 0:
940
                error( '*** Error: control network test failed\n' )
941
                exit( 1 )
942
        info( '\n' )