Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 9005ce32

History | View | Annotate | Download (22.4 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 macColonHex, ipStr, ipParse, netParse, ipAdd
101
from mininet.term import cleanUpScreens, makeTerms
102

    
103
class Mininet( object ):
104
    "Network emulation with hosts spawned in network namespaces."
105

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

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

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

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

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

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

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

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

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

    
207
    def addLink( self, src, dst, srcPort=None, dstPort=None,
208
                 cls=None, **params ):
209
        "Add a link from topo"
210
        if self.intf and not 'intf' in params:
211
            params[ 'intf' ] = self.intf
212
        if not cls:
213
            cls = self.link
214
        return cls( src, dst, srcPort, dstPort, **params )
215

    
216
    def configHosts( self ):
217
        "Configure a set of hosts."
218
        for host in self.hosts:
219
            info( host.name + ' ' )
220
            host.configDefault( defaultRoute=host.defaultIntf )
221
            # You're low priority, dude!
222
            # BL: do we want to do this here or not?
223
            # May not make sense if we have CPU lmiting...
224
            # quietRun( 'renice +18 -p ' + repr( host.pid ) )
225
        info( '\n' )
226

    
227
    def buildFromTopo( self, topo=None ):
228
        """Build mininet from a topology object
229
           At the end of this function, everything should be connected
230
           and up."""
231

    
232
        # Possibly we should clean up here and/or validate
233
        # the topo
234
        if self.cleanup:
235
            pass
236

    
237
        info( '*** Creating network\n' )
238

    
239
        if not self.controllers:
240
            # Add a default controller
241
            info( '*** Adding controller\n' )
242
            self.addController( 'c0' )
243

    
244
        info( '*** Adding hosts:\n' )
245
        for hostName in topo.hosts():
246
            self.addHost( hostName, **topo.nodeInfo( hostName ) )
247
            info( hostName + ' ' )
248

    
249
        info( '\n*** Adding switches:\n' )
250
        for switchName in topo.switches():
251
            self.addSwitch( switchName, **topo.nodeInfo( switchName) )
252
            info( switchName + ' ' )
253

    
254
        info( '\n*** Adding links:\n' )
255
        for srcName, dstName in topo.links(sort=True):
256
            src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ]
257
            srcPort, dstPort = topo.port( srcName, dstName )
258
            self.addLink( src, dst, srcPort, dstPort,
259
                          **topo.linkInfo( srcName, dstName ) )
260
            info( '(%s, %s) ' % ( src.name, dst.name ) )
261

    
262
        info( '\n' )
263

    
264
    def configureControlNetwork( self ):
265
        "Control net config hook: override in subclass"
266
        raise Exception( 'configureControlNetwork: '
267
               'should be overriden in subclass', self )
268

    
269
    def build( self ):
270
        "Build mininet."
271
        if self.topo:
272
            self.buildFromTopo( self.topo )
273
        if ( self.inNamespace ):
274
            self.configureControlNetwork()
275
        info( '*** Configuring hosts\n' )
276
        self.configHosts()
277
        if self.xterms:
278
            self.startTerms()
279
        if self.autoStaticArp:
280
            self.staticArp()
281
        self.built = True
282

    
283
    def startTerms( self ):
284
        "Start a terminal for each node."
285
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
286
        cleanUpScreens()
287
        self.terms += makeTerms( self.controllers, 'controller' )
288
        self.terms += makeTerms( self.switches, 'switch' )
289
        self.terms += makeTerms( self.hosts, 'host' )
290

    
291
    def stopXterms( self ):
292
        "Kill each xterm."
293
        for term in self.terms:
294
            os.kill( term.pid, signal.SIGKILL )
295
        cleanUpScreens()
296

    
297
    def staticArp( self ):
298
        "Add all-pairs ARP entries to remove the need to handle broadcast."
299
        for src in self.hosts:
300
            for dst in self.hosts:
301
                if src != dst:
302
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
303

    
304
    def start( self ):
305
        "Start controller and switches."
306
        if not self.built:
307
            self.build()
308
        info( '*** Starting controller\n' )
309
        for controller in self.controllers:
310
            controller.start()
311
        info( '*** Starting %s switches\n' % len( self.switches ) )
312
        for switch in self.switches:
313
            info( switch.name + ' ')
314
            switch.start( self.controllers )
315
        info( '\n' )
316

    
317
    def stop( self ):
318
        "Stop the controller(s), switches and hosts"
319
        if self.terms:
320
            info( '*** Stopping %i terms\n' % len( self.terms ) )
321
            self.stopXterms()
322
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
323
        for host in self.hosts:
324
            info( host.name + ' ' )
325
            host.terminate()
326
        info( '\n' )
327
        info( '*** Stopping %i switches\n' % len( self.switches ) )
328
        for switch in self.switches:
329
            info( switch.name + ' ' )
330
            switch.stop()
331
        info( '\n' )
332
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
333
        for controller in self.controllers:
334
            info( controller.name + ' ' )
335
            controller.stop()
336
        info( '\n*** Done\n' )
337

    
338
    def run( self, test, *args, **kwargs ):
339
        "Perform a complete start/test/stop cycle."
340
        self.start()
341
        info( '*** Running test\n' )
342
        result = test( *args, **kwargs )
343
        self.stop()
344
        return result
345

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

    
370
    # XXX These test methods should be moved out of this class.
371
    # Probably we should create a tests.py for them
372

    
373
    @staticmethod
374
    def _parsePing( pingOutput ):
375
        "Parse ping output and return packets sent, received."
376
        # Check for downed link
377
        if 'connect: Network is unreachable' in pingOutput:
378
            return (1, 0)
379
        r = r'(\d+) packets transmitted, (\d+) received'
380
        m = re.search( r, pingOutput )
381
        if m == None:
382
            error( '*** Error: could not parse ping output: %s\n' %
383
                     pingOutput )
384
            return (1, 0)
385
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
386
        return sent, received
387

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

    
419
    def pingAll( self ):
420
        """Ping between all hosts.
421
           returns: ploss packet loss percentage"""
422
        return self.ping()
423

    
424
    def pingPair( self ):
425
        """Ping between first two hosts, useful for testing.
426
           returns: ploss packet loss percentage"""
427
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
428
        return self.ping( hosts=hosts )
429

    
430
    @staticmethod
431
    def _parseIperf( iperfOutput ):
432
        """Parse iperf output and return bandwidth.
433
           iperfOutput: string
434
           returns: result string"""
435
        r = r'([\d\.]+ \w+/sec)'
436
        m = re.findall( r, iperfOutput )
437
        if m:
438
            return m[-1]
439
        else:
440
            # was: raise Exception(...)
441
            error( 'could not parse iperf output: ' + iperfOutput )
442
            return ''
443

    
444
    # XXX This should be cleaned up
445

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

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

    
517
    def interact( self ):
518
        "Start network and run our simple CLI."
519
        self.start()
520
        result = CLI( self )
521
        self.stop()
522
        return result
523

    
524
    inited = False
525

    
526
    @classmethod
527
    def init( cls ):
528
        "Initialize Mininet"
529
        if cls.inited:
530
            return
531
        if os.getuid() != 0:
532
            # Note: this script must be run as root
533
            # Probably we should only sudo when we need
534
            # to as per Big Switch's patch
535
            print "*** Mininet must run as root."
536
            exit( 1 )
537
        fixLimits()
538
        cls.inited = True
539

    
540

    
541
class MininetWithControlNet( Mininet ):
542

    
543
    """Control network support:
544

545
       Create an explicit control network. Currently this is only
546
       used/usable with the user datapath.
547

548
       Notes:
549

550
       1. If the controller and switches are in the same (e.g. root)
551
          namespace, they can just use the loopback connection.
552

553
       2. If we can get unix domain sockets to work, we can use them
554
          instead of an explicit control network.
555

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

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

562
       5. Basically nobody ever used this code, so it has been moved
563
          into its own class.
564

565
       6. Ultimately we may wish to extend this to allow us to create a
566
          control network which every node's control interface is
567
          attached to."""
568

    
569
    def configureControlNetwork( self ):
570
        "Configure control network."
571
        self.configureRoutedControlNetwork()
572

    
573
    # We still need to figure out the right way to pass
574
    # in the control network location.
575

    
576
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
577
        prefixLen=16 ):
578
        """Configure a routed control network on controller and switches.
579
           For use with the user datapath only right now."""
580
        controller = self.controllers[ 0 ]
581
        info( controller.name + ' <->' )
582
        cip = ip
583
        snum = ipParse( ip )
584
        for switch in self.switches:
585
            info( ' ' + switch.name )
586
            link = self.link( switch, controller, port1=0 )
587
            sintf, cintf = link.intf1, link.intf2
588
            switch.controlIntf = sintf
589
            snum += 1
590
            while snum & 0xff in [ 0, 255 ]:
591
                snum += 1
592
            sip = ipStr( snum )
593
            cintf.setIP( cip, prefixLen )
594
            sintf.setIP( sip, prefixLen )
595
            controller.setHostRoute( sip, cintf )
596
            switch.setHostRoute( cip, sintf )
597
        info( '\n' )
598
        info( '*** Testing control network\n' )
599
        while not cintf.isUp():
600
            info( '*** Waiting for', cintf, 'to come up\n' )
601
            sleep( 1 )
602
        for switch in self.switches:
603
            while not sintf.isUp():
604
                info( '*** Waiting for', sintf, 'to come up\n' )
605
                sleep( 1 )
606
            if self.ping( hosts=[ switch, controller ] ) != 0:
607
                error( '*** Error: control network test failed\n' )
608
                exit( 1 )
609
        info( '\n' )