Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 13d25b41

History | View | Annotate | Download (31.7 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 copy
94
from time import sleep
95
from itertools import chain, groupby
96

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

    
105
# Mininet version: should be consistent with README and LICENSE
106
VERSION = "2.1.0+"
107

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

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

    
154
        self.hosts = []
155
        self.switches = []
156
        self.controllers = []
157

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

    
160
        self.terms = []  # list of spawned xterm processes
161

    
162
        Mininet.init()  # Initialize Mininet if necessary
163

    
164
        self.built = False
165
        if topo and build:
166
            self.build()
167

    
168

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

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

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

    
239
    def addController( self, name='c0', controller=None, **params ):
240
        """Add controller.
241
           controller: Controller class"""
242
        # Get controller class
243
        if not controller:
244
            controller = self.controller
245
        # Construct new controller if one is not given
246
        if isinstance(name, Controller):
247
            controller_new = name
248
            # Pylint thinks controller is a str()
249
            # pylint: disable=E1103
250
            name = controller_new.name
251
            # pylint: enable=E1103
252
        else:
253
            controller_new = controller( name, **params )
254
        # Add new controller to net
255
        if controller_new:  # allow controller-less setups
256
            self.controllers.append( controller_new )
257
            self.nameToNode[ name ] = controller_new
258
        return controller_new
259

    
260
    # BL: We now have four ways to look up nodes
261
    # This may (should?) be cleaned up in the future.
262
    def getNodeByName( self, *args ):
263
        "Return node(s) with given name(s)"
264
        if len( args ) == 1:
265
            return self.nameToNode[ args[ 0 ] ]
266
        return [ self.nameToNode[ n ] for n in args ]
267

    
268
    def get( self, *args ):
269
        "Convenience alias for getNodeByName"
270
        return self.getNodeByName( *args )
271

    
272
    # Even more convenient syntax for node lookup and iteration
273
    def __getitem__( self, key ):
274
        """net [ name ] operator: Return node(s) with given name(s)"""
275
        return self.nameToNode[ key ]
276

    
277
    def __iter__( self ):
278
        "return iterator over node names"
279
        for node in chain( self.hosts, self.switches, self.controllers ):
280
            yield node.name
281

    
282
    def __len__( self ):
283
        "returns number of nodes in net"
284
        return ( len( self.hosts ) + len( self.switches ) +
285
                 len( self.controllers ) )
286

    
287
    def __contains__( self, item ):
288
        "returns True if net contains named node"
289
        return item in self.nameToNode
290

    
291
    def keys( self ):
292
        "return a list of all node names or net's keys"
293
        return list( self )
294

    
295
    def values( self ):
296
        "return a list of all nodes or net's values"
297
        return [ self[name] for name in self ]
298

    
299
    def items( self ):
300
        "return (key,value) tuple list for every node in net"
301
        return zip( self.keys(), self.values() )
302

    
303
    def addLink( self, node1, node2, port1=None, port2=None,
304
                 cls=None, **params ):
305
        """"Add a link from node1 to node2
306
            node1: source node
307
            node2: dest node
308
            port1: source port
309
            port2: dest port
310
            returns: link object"""
311
        defaults = { 'port1': port1,
312
                     'port2': port2,
313
                     'intf': self.intf }
314
        defaults.update( params )
315
        if not cls:
316
            cls = self.link
317
        return cls( node1, node2, **defaults )
318

    
319
    def configHosts( self ):
320
        "Configure a set of hosts."
321
        for host in self.hosts:
322
            info( host.name + ' ' )
323
            intf = host.defaultIntf()
324
            if intf:
325
                host.configDefault()
326
            else:
327
                # Don't configure nonexistent intf
328
                host.configDefault( ip=None, mac=None )
329
            # You're low priority, dude!
330
            # BL: do we want to do this here or not?
331
            # May not make sense if we have CPU lmiting...
332
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
333
            # This may not be the right place to do this, but
334
            # it needs to be done somewhere.
335
            host.cmd( 'ifconfig lo up' )
336
        info( '\n' )
337

    
338
    def buildFromTopo( self, topo=None ):
339
        """Build mininet from a topology object
340
           At the end of this function, everything should be connected
341
           and up."""
342

    
343
        # Possibly we should clean up here and/or validate
344
        # the topo
345
        if self.cleanup:
346
            pass
347

    
348
        info( '*** Creating network\n' )
349

    
350
        if not self.controllers and self.controller:
351
            # Add a default controller
352
            info( '*** Adding controller\n' )
353
            classes = self.controller
354
            if type( classes ) is not list:
355
                classes = [ classes ]
356
            for i, cls in enumerate( classes ):
357
                self.addController( 'c%d' % i, cls )
358

    
359
        info( '*** Adding hosts:\n' )
360
        for hostName in topo.hosts():
361
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
362
            info( hostName + ' ' )
363

    
364
        info( '\n*** Adding switches:\n' )
365
        for switchName in topo.switches():
366
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
367
            info( switchName + ' ' )
368

    
369
        info( '\n*** Adding links:\n' )
370
        for srcName, dstName in topo.links(sort=True):
371
            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
372
            params = topo.linkInfo( srcName, dstName )
373
            srcPort, dstPort = topo.port( srcName, dstName )
374
            self.addLink( src, dst, srcPort, dstPort, **params )
375
            info( '(%s, %s) ' % ( src.name, dst.name ) )
376

    
377
        info( '\n' )
378

    
379
    def configureControlNetwork( self ):
380
        "Control net config hook: override in subclass"
381
        raise Exception( 'configureControlNetwork: '
382
                         'should be overriden in subclass', self )
383

    
384
    def build( self ):
385
        "Build mininet."
386
        if self.topo:
387
            self.buildFromTopo( self.topo )
388
        if self.inNamespace:
389
            self.configureControlNetwork()
390
        info( '*** Configuring hosts\n' )
391
        self.configHosts()
392
        if self.xterms:
393
            self.startTerms()
394
        if self.autoStaticArp:
395
            self.staticArp()
396
        self.built = True
397

    
398
    def startTerms( self ):
399
        "Start a terminal for each node."
400
        if 'DISPLAY' not in os.environ:
401
            error( "Error starting terms: Cannot connect to display\n" )
402
            return
403
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
404
        cleanUpScreens()
405
        self.terms += makeTerms( self.controllers, 'controller' )
406
        self.terms += makeTerms( self.switches, 'switch' )
407
        self.terms += makeTerms( self.hosts, 'host' )
408

    
409
    def stopXterms( self ):
410
        "Kill each xterm."
411
        for term in self.terms:
412
            os.kill( term.pid, signal.SIGKILL )
413
        cleanUpScreens()
414

    
415
    def staticArp( self ):
416
        "Add all-pairs ARP entries to remove the need to handle broadcast."
417
        for src in self.hosts:
418
            for dst in self.hosts:
419
                if src != dst:
420
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
421

    
422
    def start( self ):
423
        "Start controller and switches."
424
        if not self.built:
425
            self.build()
426
        info( '*** Starting controller\n' )
427
        for controller in self.controllers:
428
            controller.start()
429
        info( '*** Starting %s switches\n' % len( self.switches ) )
430
        for switch in self.switches:
431
            info( switch.name + ' ')
432
            switch.start( self.controllers )
433
        info( '\n' )
434
        if self.waitConn:
435
            self.waitConnected()
436

    
437
    def stop( self ):
438
        "Stop the controller(s), switches and hosts"
439
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
440
        for controller in self.controllers:
441
            info( controller.name + ' ' )
442
            controller.stop()
443
        info( '\n' )
444
        if self.terms:
445
            info( '*** Stopping %i terms\n' % len( self.terms ) )
446
            self.stopXterms()
447
        info( '*** Stopping %i switches\n' % len( self.switches ) )
448
        for swclass, switches in groupby( sorted( self.switches, key=type ), type ):
449
            if hasattr( swclass, 'batchShutdown' ):
450
                swclass.batchShutdown( switches )
451
        for switch in self.switches:
452
            info( switch.name + ' ' )
453
            switch.stop()
454
        info( '\n' )
455
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
456
        for host in self.hosts:
457
            info( host.name + ' ' )
458
            host.terminate()
459
        info( '\n*** Done\n' )
460

    
461
    def run( self, test, *args, **kwargs ):
462
        "Perform a complete start/test/stop cycle."
463
        self.start()
464
        info( '*** Running test\n' )
465
        result = test( *args, **kwargs )
466
        self.stop()
467
        return result
468

    
469
    def monitor( self, hosts=None, timeoutms=-1 ):
470
        """Monitor a set of hosts (or all hosts by default),
471
           and return their output, a line at a time.
472
           hosts: (optional) set of hosts to monitor
473
           timeoutms: (optional) timeout value in ms
474
           returns: iterator which returns host, line"""
475
        if hosts is None:
476
            hosts = self.hosts
477
        poller = select.poll()
478
        Node = hosts[ 0 ]  # so we can call class method fdToNode
479
        for host in hosts:
480
            poller.register( host.stdout )
481
        while True:
482
            ready = poller.poll( timeoutms )
483
            for fd, event in ready:
484
                host = Node.fdToNode( fd )
485
                if event & select.POLLIN:
486
                    line = host.readline()
487
                    if line is not None:
488
                        yield host, line
489
            # Return if non-blocking
490
            if not ready and timeoutms >= 0:
491
                yield None, None
492

    
493
    # XXX These test methods should be moved out of this class.
494
    # Probably we should create a tests.py for them
495

    
496
    @staticmethod
497
    def _parsePing( pingOutput ):
498
        "Parse ping output and return packets sent, received."
499
        # Check for downed link
500
        if 'connect: Network is unreachable' in pingOutput:
501
            return 1, 0
502
        r = r'(\d+) packets transmitted, (\d+) received'
503
        m = re.search( r, pingOutput )
504
        if m is None:
505
            error( '*** Error: could not parse ping output: %s\n' %
506
                   pingOutput )
507
            return 1, 0
508
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
509
        return sent, received
510

    
511
    def ping( self, hosts=None, timeout=None ):
512
        """Ping between all specified hosts.
513
           hosts: list of hosts
514
           timeout: time to wait for a response, as string
515
           returns: ploss packet loss percentage"""
516
        # should we check if running?
517
        packets = 0
518
        lost = 0
519
        ploss = None
520
        if not hosts:
521
            hosts = self.hosts
522
            output( '*** Ping: testing ping reachability\n' )
523
        for node in hosts:
524
            output( '%s -> ' % node.name )
525
            for dest in hosts:
526
                if node != dest:
527
                    opts = ''
528
                    if timeout:
529
                        opts = '-W %s' % timeout
530
                    result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
531
                    sent, received = self._parsePing( result )
532
                    packets += sent
533
                    if received > sent:
534
                        error( '*** Error: received too many packets' )
535
                        error( '%s' % result )
536
                        node.cmdPrint( 'route' )
537
                        exit( 1 )
538
                    lost += sent - received
539
                    output( ( '%s ' % dest.name ) if received else 'X ' )
540
            output( '\n' )
541
        if packets > 0:
542
            ploss = 100.0 * lost / packets
543
            received = packets - lost
544
            output( "*** Results: %i%% dropped (%d/%d received)\n" %
545
                    ( ploss, received, packets ) )
546
        else:
547
            ploss = 0
548
            output( "*** Warning: No packets sent\n" )
549
        return ploss
550

    
551
    @staticmethod
552
    def _parsePingFull( pingOutput ):
553
        "Parse ping output and return all data."
554
        errorTuple = (1, 0, 0, 0, 0, 0)
555
        # Check for downed link
556
        r = r'[uU]nreachable'
557
        m = re.search( r, pingOutput )
558
        if m is not None:
559
            return errorTuple
560
        r = r'(\d+) packets transmitted, (\d+) received'
561
        m = re.search( r, pingOutput )
562
        if m is None:
563
            error( '*** Error: could not parse ping output: %s\n' %
564
                   pingOutput )
565
            return errorTuple
566
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
567
        r = r'rtt min/avg/max/mdev = '
568
        r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms'
569
        m = re.search( r, pingOutput )
570
        if m is None:
571
            error( '*** Error: could not parse ping output: %s\n' %
572
                   pingOutput )
573
            return errorTuple
574
        rttmin = float( m.group( 1 ) )
575
        rttavg = float( m.group( 2 ) )
576
        rttmax = float( m.group( 3 ) )
577
        rttdev = float( m.group( 4 ) )
578
        return sent, received, rttmin, rttavg, rttmax, rttdev
579

    
580
    def pingFull( self, hosts=None, timeout=None ):
581
        """Ping between all specified hosts and return all data.
582
           hosts: list of hosts
583
           timeout: time to wait for a response, as string
584
           returns: all ping data; see function body."""
585
        # should we check if running?
586
        # Each value is a tuple: (src, dsd, [all ping outputs])
587
        all_outputs = []
588
        if not hosts:
589
            hosts = self.hosts
590
            output( '*** Ping: testing ping reachability\n' )
591
        for node in hosts:
592
            output( '%s -> ' % node.name )
593
            for dest in hosts:
594
                if node != dest:
595
                    opts = ''
596
                    if timeout:
597
                        opts = '-W %s' % timeout
598
                    result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
599
                    outputs = self._parsePingFull( result )
600
                    sent, received, rttmin, rttavg, rttmax, rttdev = outputs
601
                    all_outputs.append( (node, dest, outputs) )
602
                    output( ( '%s ' % dest.name ) if received else 'X ' )
603
            output( '\n' )
604
        output( "*** Results: \n" )
605
        for outputs in all_outputs:
606
            src, dest, ping_outputs = outputs
607
            sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs
608
            output( " %s->%s: %s/%s, " % (src, dest, sent, received ) )
609
            output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" %
610
                    (rttmin, rttavg, rttmax, rttdev) )
611
        return all_outputs
612

    
613
    def pingAll( self, timeout=None ):
614
        """Ping between all hosts.
615
           returns: ploss packet loss percentage"""
616
        return self.ping( timeout=timeout )
617

    
618
    def pingPair( self ):
619
        """Ping between first two hosts, useful for testing.
620
           returns: ploss packet loss percentage"""
621
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
622
        return self.ping( hosts=hosts )
623

    
624
    def pingAllFull( self ):
625
        """Ping between all hosts.
626
           returns: ploss packet loss percentage"""
627
        return self.pingFull()
628

    
629
    def pingPairFull( self ):
630
        """Ping between first two hosts, useful for testing.
631
           returns: ploss packet loss percentage"""
632
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
633
        return self.pingFull( hosts=hosts )
634

    
635
    @staticmethod
636
    def _parseIperf( iperfOutput ):
637
        """Parse iperf output and return bandwidth.
638
           iperfOutput: string
639
           returns: result string"""
640
        r = r'([\d\.]+ \w+/sec)'
641
        m = re.findall( r, iperfOutput )
642
        if m:
643
            return m[-1]
644
        else:
645
            # was: raise Exception(...)
646
            error( 'could not parse iperf output: ' + iperfOutput )
647
            return ''
648

    
649
    # XXX This should be cleaned up
650

    
651
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M' ):
652
        """Run iperf between two hosts.
653
           hosts: list of hosts; if None, uses opposite hosts
654
           l4Type: string, one of [ TCP, UDP ]
655
           returns: results two-element array of server and client speeds"""
656
        if not quietRun( 'which telnet' ):
657
            error( 'Cannot find telnet in $PATH - required for iperf test' )
658
            return
659
        if not hosts:
660
            hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
661
        else:
662
            assert len( hosts ) == 2
663
        client, server = hosts
664
        output( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
665
        output( "%s and %s\n" % ( client.name, server.name ) )
666
        server.cmd( 'killall -9 iperf' )
667
        iperfArgs = 'iperf '
668
        bwArgs = ''
669
        if l4Type == 'UDP':
670
            iperfArgs += '-u '
671
            bwArgs = '-b ' + udpBw + ' '
672
        elif l4Type != 'TCP':
673
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
674
        server.sendCmd( iperfArgs + '-s', printPid=True )
675
        servout = ''
676
        while server.lastPid is None:
677
            servout += server.monitor()
678
        if l4Type == 'TCP':
679
            while 'Connected' not in client.cmd(
680
                    'sh -c "echo A | telnet -e A %s 5001"' % server.IP()):
681
                info( 'Waiting for iperf to start up...' )
682
                sleep(.5)
683
        cliout = client.cmd( iperfArgs + '-t 5 -c ' + server.IP() + ' ' +
684
                             bwArgs )
685
        debug( 'Client output: %s\n' % cliout )
686
        server.sendInt()
687
        servout += server.waitOutput()
688
        debug( 'Server output: %s\n' % servout )
689
        result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ]
690
        if l4Type == 'UDP':
691
            result.insert( 0, udpBw )
692
        output( '*** Results: %s\n' % result )
693
        return result
694

    
695
    def runCpuLimitTest( self, cpu, duration=5 ):
696
        """run CPU limit test with 'while true' processes.
697
        cpu: desired CPU fraction of each host
698
        duration: test duration in seconds
699
        returns a single list of measured CPU fractions as floats.
700
        """
701
        pct = cpu * 100
702
        info('*** Testing CPU %.0f%% bandwidth limit\n' % pct)
703
        hosts = self.hosts
704
        for h in hosts:
705
            h.cmd( 'while true; do a=1; done &' )
706
        pids = [h.cmd( 'echo $!' ).strip() for h in hosts]
707
        pids_str = ",".join(["%s" % pid for pid in pids])
708
        cmd = 'ps -p %s -o pid,%%cpu,args' % pids_str
709
        # It's a shame that this is what pylint prefers
710
        outputs = []
711
        for _ in range( duration ):
712
            sleep( 1 )
713
            outputs.append( quietRun( cmd ).strip() )
714
        for h in hosts:
715
            h.cmd( 'kill %1' )
716
        cpu_fractions = []
717
        for test_output in outputs:
718
            # Split by line.  Ignore first line, which looks like this:
719
            # PID %CPU COMMAND\n
720
            for line in test_output.split('\n')[1:]:
721
                r = r'\d+\s*(\d+\.\d+)'
722
                m = re.search( r, line )
723
                if m is None:
724
                    error( '*** Error: could not extract CPU fraction: %s\n' %
725
                           line )
726
                    return None
727
                cpu_fractions.append( float( m.group( 1 ) ) )
728
        output( '*** Results: %s\n' % cpu_fractions )
729
        return cpu_fractions
730

    
731
    # BL: I think this can be rewritten now that we have
732
    # a real link class.
733
    def configLinkStatus( self, src, dst, status ):
734
        """Change status of src <-> dst links.
735
           src: node name
736
           dst: node name
737
           status: string {up, down}"""
738
        if src not in self.nameToNode:
739
            error( 'src not in network: %s\n' % src )
740
        elif dst not in self.nameToNode:
741
            error( 'dst not in network: %s\n' % dst )
742
        else:
743
            if type( src ) is str:
744
                src = self.nameToNode[ src ]
745
            if type( dst ) is str:
746
                dst = self.nameToNode[ dst ]
747
            connections = src.connectionsTo( dst )
748
            if len( connections ) == 0:
749
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
750
            for srcIntf, dstIntf in connections:
751
                result = srcIntf.ifconfig( status )
752
                if result:
753
                    error( 'link src status change failed: %s\n' % result )
754
                result = dstIntf.ifconfig( status )
755
                if result:
756
                    error( 'link dst status change failed: %s\n' % result )
757

    
758
    def interact( self ):
759
        "Start network and run our simple CLI."
760
        self.start()
761
        result = CLI( self )
762
        self.stop()
763
        return result
764

    
765
    inited = False
766

    
767
    @classmethod
768
    def init( cls ):
769
        "Initialize Mininet"
770
        if cls.inited:
771
            return
772
        ensureRoot()
773
        fixLimits()
774
        cls.inited = True
775

    
776

    
777
class MininetWithControlNet( Mininet ):
778

    
779
    """Control network support:
780

781
       Create an explicit control network. Currently this is only
782
       used/usable with the user datapath.
783

784
       Notes:
785

786
       1. If the controller and switches are in the same (e.g. root)
787
          namespace, they can just use the loopback connection.
788

789
       2. If we can get unix domain sockets to work, we can use them
790
          instead of an explicit control network.
791

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

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

798
       5. Basically nobody ever used this code, so it has been moved
799
          into its own class.
800

801
       6. Ultimately we may wish to extend this to allow us to create a
802
          control network which every node's control interface is
803
          attached to."""
804

    
805
    def configureControlNetwork( self ):
806
        "Configure control network."
807
        self.configureRoutedControlNetwork()
808

    
809
    # We still need to figure out the right way to pass
810
    # in the control network location.
811

    
812
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
813
                                       prefixLen=16 ):
814
        """Configure a routed control network on controller and switches.
815
           For use with the user datapath only right now."""
816
        controller = self.controllers[ 0 ]
817
        info( controller.name + ' <->' )
818
        cip = ip
819
        snum = ipParse( ip )
820
        for switch in self.switches:
821
            info( ' ' + switch.name )
822
            link = self.link( switch, controller, port1=0 )
823
            sintf, cintf = link.intf1, link.intf2
824
            switch.controlIntf = sintf
825
            snum += 1
826
            while snum & 0xff in [ 0, 255 ]:
827
                snum += 1
828
            sip = ipStr( snum )
829
            cintf.setIP( cip, prefixLen )
830
            sintf.setIP( sip, prefixLen )
831
            controller.setHostRoute( sip, cintf )
832
            switch.setHostRoute( cip, sintf )
833
        info( '\n' )
834
        info( '*** Testing control network\n' )
835
        while not cintf.isUp():
836
            info( '*** Waiting for', cintf, 'to come up\n' )
837
            sleep( 1 )
838
        for switch in self.switches:
839
            while not sintf.isUp():
840
                info( '*** Waiting for', sintf, 'to come up\n' )
841
                sleep( 1 )
842
            if self.ping( hosts=[ switch, controller ] ) != 0:
843
                error( '*** Error: control network test failed\n' )
844
                exit( 1 )
845
        info( '\n' )