Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 216a4b7c

History | View | Annotate | Download (22.3 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
99
from mininet.util import quietRun, fixLimits
100
from mininet.util import createLink, macColonHex, ipStr, ipParse
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=None, 
108
                 build=True, xterms=False, cleanup=False,
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.inNamespace = inNamespace
134
        self.xterms = xterms
135
        self.cleanup = cleanup
136
        self.autoSetMacs = autoSetMacs
137
        self.autoStaticArp = autoStaticArp
138
        self.listenPort = listenPort
139

    
140
        self.hosts = []
141
        self.switches = []
142
        self.controllers = []
143

    
144
        self.nameToNode = {}  # name to Node (Host/Switch) objects
145
        self.idToNode = {}  # dpid to Node (Host/Switch) objects
146

    
147
        self.terms = []  # list of spawned xterm processes
148

    
149
        init()  # Initialize Mininet if necessary
150

    
151
        self.built = False
152
        if topo and build:
153
            self.build()
154

    
155
    # BL Note:
156
    # The specific items for host/switch/etc. should probably be
157
    # handled in the node classes rather than here!!
158

    
159
    def addHost( self, name, host=None, **params ):
160
        """Add host.
161
           name: name of host to add
162
           host: custom host constructor (optional)
163
           params: parameters for host
164
           returns: added host"""
165
        if not host:
166
            host = self.host
167
        h = host( name, **params)
168
        self.hosts.append( h )
169
        self.nameToNode[ name ] = h
170
        return h
171

    
172
    def addSwitch( self, name, switch=None, **params ):
173
        """Add switch.
174
           name: name of switch to add
175
           switch: custom switch constructor (optional)
176
           returns: added switch
177
           side effect: increments listenPort ivar ."""
178
        defaults = { 'listenPort': self.listenPort, 
179
                     'inNamespace': self.inNamespace }
180
        defaults.update( params )
181
        if not switch:
182
            switch = self.switch
183
        sw = self.switch( name, **defaults )
184
        if not self.inNamespace and self.listenPort:
185
            self.listenPort += 1
186
        self.switches.append( sw )
187
        self.nameToNode[ name ] = sw
188
        return sw
189

    
190
    def addController( self, name='c0', controller=None, **params ):
191
        """Add controller.
192
           controller: Controller class"""
193
        if not controller:
194
            controller = self.controller
195
        controller_new = controller( name, **params )
196
        if controller_new:  # allow controller-less setups
197
            self.controllers.append( controller_new )
198
            self.nameToNode[ name ] = controller_new
199
        return controller_new
200

    
201
    def configHosts( self ):
202
        "Configure a set of hosts."
203
        for host in self.hosts:
204
            info( host.name + ' ' )
205
            host.configDefault( defaultRoute=host.defaultIntf )
206
            # You're low priority, dude!
207
            # BL: do we want to do this here or not?
208
            # May not make sense if we have CPU lmiting...
209
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
210
        info( '\n' )
211

    
212
    def buildFromTopo( self, topo=None ):
213
        """Build mininet from a topology object
214
           At the end of this function, everything should be connected
215
           and up."""
216

    
217
        if not topo:
218
            topo = self.topo()
219

    
220
        def addNode( prefix, addMethod, nodeId ):
221
            "Add a host or a switch from topo"
222
            name = prefix + topo.name( nodeId )
223
            ni = topo.nodeInfo( nodeId )
224
            # Default IP and MAC addresses
225
            defaults = { 'ip': topo.ip( nodeId ) }
226
            if self.autoSetMacs:
227
                defaults[ 'mac'] = macColonHex( nodeId ) 
228
            defaults.update( ni.params )
229
            node = addMethod( name, cls=ni.cls, **defaults )
230
            self.idToNode[ nodeId ] = node
231
            info( name + ' ' )
232

    
233
        def addLink( srcId, dstId, link=None ):
234
            "Add a link from topo"
235
            src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
236
            srcPort, dstPort = topo.port( srcId, dstId )
237
            ei = topo.edgeInfo( srcId, dstId )
238
            link = getattr( ei, 'cls', link )
239
            params = ei.params
240
            if self.intf and not 'intf' in params:
241
                params[ 'intf' ] = self.intf
242
            if not link:
243
                link = self.link
244
            info( '(%s, %s) ' % ( src.name, dst.name ) )
245
            link( src, dst, srcPort, dstPort, **params )
246

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

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

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

    
259
        info( '*** Adding hosts:\n' )
260
        for hostId in sorted( topo.hosts() ):
261
            addNode( 'h', self.addHost, hostId )
262

    
263
        info( '\n*** Adding switches:\n' )
264
        for switchId in sorted( topo.switches() ):
265
            addNode( 's', self.addSwitch, switchId )
266

    
267
        info( '\n*** Adding links:\n' )
268
        for srcId, dstId in sorted( topo.edges() ):
269
            addLink( srcId, dstId )
270

    
271
        info( '\n' )
272

    
273

    
274
    def configureControlNetwork( self ):
275
        error( "configureControlNetwork: override in subclass, or use" 
276
               "MininetWithControlNet class" )
277

    
278
    def build( self ):
279
        "Build mininet."
280
        if self.topo:
281
            self.buildFromTopo( self.topo )
282
        if self.inNamespace:
283
            info( '*** Configuring control network\n' )
284
            self.configureControlNetwork()
285
        info( '*** Configuring hosts\n' )
286
        self.configHosts()
287
        if self.xterms:
288
            self.startTerms()
289
        if self.autoStaticArp:
290
            self.staticArp()
291
        self.built = True
292

    
293
    def startTerms( self ):
294
        "Start a terminal for each node."
295
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
296
        cleanUpScreens()
297
        self.terms += makeTerms( self.controllers, 'controller' )
298
        self.terms += makeTerms( self.switches, 'switch' )
299
        self.terms += makeTerms( self.hosts, 'host' )
300

    
301
    def stopXterms( self ):
302
        "Kill each xterm."
303
        for term in self.terms:
304
            os.kill( term.pid, signal.SIGKILL )
305
        cleanUpScreens()
306

    
307
    def staticArp( self ):
308
        "Add all-pairs ARP entries to remove the need to handle broadcast."
309
        for src in self.hosts:
310
            for dst in self.hosts:
311
                if src != dst:
312
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
313

    
314
    def start( self ):
315
        "Start controller and switches."
316
        if not self.built:
317
            self.build()
318
        info( '*** Starting controller\n' )
319
        for controller in self.controllers:
320
            controller.start()
321
        info( '*** Starting %s switches\n' % len( self.switches ) )
322
        for switch in self.switches:
323
            info( switch.name + ' ')
324
            switch.start( self.controllers )
325
        info( '\n' )
326

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

    
348
    def run( self, test, *args, **kwargs ):
349
        "Perform a complete start/test/stop cycle."
350
        self.start()
351
        info( '*** Running test\n' )
352
        result = test( *args, **kwargs )
353
        self.stop()
354
        return result
355

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

    
380
    # XXX These test methods should be moved out of this class.
381
    # Probably we should create a tests.py for them
382

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

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

    
429
    def pingAll( self ):
430
        """Ping between all hosts.
431
           returns: ploss packet loss percentage"""
432
        return self.ping()
433

    
434
    def pingPair( self ):
435
        """Ping between first two hosts, useful for testing.
436
           returns: ploss packet loss percentage"""
437
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
438
        return self.ping( hosts=hosts )
439

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

    
454
    # XXX This should be cleaned up
455

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

    
499
    # BL: I think this can be rewritten now that we have
500
    # a real link class.
501
    def configLinkStatus( self, src, dst, status ):
502
        """Change status of src <-> dst links.
503
           src: node name
504
           dst: node name
505
           status: string {up, down}"""
506
        if src not in self.nameToNode:
507
            error( 'src not in network: %s\n' % src )
508
        elif dst not in self.nameToNode:
509
            error( 'dst not in network: %s\n' % dst )
510
        else:
511
            srcNode, dstNode = self.nameToNode[ src ], self.nameToNode[ dst ]
512
            connections = srcNode.connectionsTo( dstNode )
513
            if len( connections ) == 0:
514
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
515
            for srcIntf, dstIntf in connections:
516
                result = srcNode.cmd( 'ifconfig', srcIntf, status )
517
                if result:
518
                    error( 'link src status change failed: %s\n' % result )
519
                result = dstNode.cmd( 'ifconfig', dstIntf, status )
520
                if result:
521
                    error( 'link dst status change failed: %s\n' % result )
522

    
523
    def interact( self ):
524
        "Start network and run our simple CLI."
525
        self.start()
526
        result = CLI( self )
527
        self.stop()
528
        return result
529

    
530

    
531
class MininetWithControlNet( Mininet ):
532

    
533
    """Control network support:
534

535
       Create an explicit control network. Currently this is only
536
       used/usable with the user datapath.
537

538
       Notes:
539

540
       1. If the controller and switches are in the same (e.g. root)
541
          namespace, they can just use the loopback connection.
542

543
       2. If we can get unix domain sockets to work, we can use them
544
          instead of an explicit control network.
545

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

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

552
       5. Basically nobody ever used this code, so it has been moved
553
          into its own class."""
554

    
555
    def configureControlNetwork( self ):
556
        "Configure control network."
557
        self.configureRoutedControlNetwork()
558

    
559
    # We still need to figure out the right way to pass
560
    # in the control network location.
561

    
562
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
563
        prefixLen=16 ):
564
        """Configure a routed control network on controller and switches.
565
           For use with the user datapath only right now."""
566
        controller = self.controllers[ 0 ]
567
        info( controller.name + ' <->' )
568
        cip = ip
569
        snum = ipParse( ip )
570
        for switch in self.switches:
571
            info( ' ' + switch.name )
572
            sintf, cintf = createLink( switch, controller )
573
            snum += 1
574
            while snum & 0xff in [ 0, 255 ]:
575
                snum += 1
576
            sip = ipStr( snum )
577
            controller.setIP( cintf, cip, prefixLen )
578
            switch.setIP( sintf, sip, prefixLen )
579
            controller.setHostRoute( sip, cintf )
580
            switch.setHostRoute( cip, sintf )
581
        info( '\n' )
582
        info( '*** Testing control network\n' )
583
        while not controller.intfIsUp( cintf ):
584
            info( '*** Waiting for', cintf, 'to come up\n' )
585
            sleep( 1 )
586
        for switch in self.switches:
587
            while not switch.intfIsUp( sintf ):
588
                info( '*** Waiting for', sintf, 'to come up\n' )
589
                sleep( 1 )
590
            if self.ping( hosts=[ switch, controller ] ) != 0:
591
                error( '*** Error: control network test failed\n' )
592
                exit( 1 )
593
        info( '\n' )
594

    
595
# pylint thinks inited is unused
596
# pylint: disable-msg=W0612
597

    
598
def init():
599
    "Initialize Mininet."
600
    if init.inited:
601
        return
602
    if os.getuid() != 0:
603
        # Note: this script must be run as root
604
        # Perhaps we should do so automatically!
605
        print "*** Mininet must run as root."
606
        exit( 1 )
607
    fixLimits()
608
    init.inited = True
609

    
610
init.inited = False
611

    
612
# pylint: enable-msg=W0612