Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ ccca871a

History | View | Annotate | Download (21 KB)

1
"""
2

3
    Mininet: A simple networking testbed for OpenFlow!
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, UserSwitch, KernelSwitch, Controller
98
from mininet.node import ControllerParams
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=KernelSwitch, host=Host,
107
                 controller=Controller,
108
                 cparams=ControllerParams( '10.0.0.0', 8 ),
109
                 build=True, xterms=False, cleanup=False,
110
                 inNamespace=False,
111
                 autoSetMacs=False, autoStaticArp=False, listenPort=None ):
112
        """Create Mininet object.
113
           topo: Topo (topology) object or None
114
           switch: Switch class
115
           host: Host class
116
           controller: Controller class
117
           cparams: ControllerParams object
118
           build: build now from topo?
119
           xterms: if build now, spawn xterms?
120
           cleanup: if build now, cleanup before creating?
121
           inNamespace: spawn switches and controller in net namespaces?
122
           autoSetMacs: set MAC addrs from topo?
123
           autoStaticArp: set all-pairs static MAC addrs?
124
           listenPort: base listening port to open; will be incremented for
125
               each additional switch in the net if inNamespace=False"""
126
        self.switch = switch
127
        self.host = host
128
        self.controller = controller
129
        self.cparams = cparams
130
        self.topo = topo
131
        self.inNamespace = inNamespace
132
        self.xterms = xterms
133
        self.cleanup = cleanup
134
        self.autoSetMacs = autoSetMacs
135
        self.autoStaticArp = autoStaticArp
136
        self.listenPort = listenPort
137

    
138
        self.hosts = []
139
        self.switches = []
140
        self.controllers = []
141
        self.nameToNode = {}  # name to Node (Host/Switch) objects
142
        self.idToNode = {}  # dpid to Node (Host/Switch) objects
143
        self.dps = 0  # number of created kernel datapaths
144
        self.terms = []  # list of spawned xterm processes
145

    
146
        init()
147
        switch.setup()
148

    
149
        self.built = False
150
        if topo and build:
151
            self.build()
152

    
153
    def addHost( self, name, mac=None, ip=None ):
154
        """Add host.
155
           name: name of host to add
156
           mac: default MAC address for intf 0
157
           ip: default IP address for intf 0
158
           returns: added host"""
159
        host = self.host( name, defaultMAC=mac, defaultIP=ip )
160
        self.hosts.append( host )
161
        self.nameToNode[ name ] = host
162
        return host
163

    
164
    def addSwitch( self, name, mac=None, ip=None ):
165
        """Add switch.
166
           name: name of switch to add
167
           mac: default MAC address for kernel/OVS switch intf 0
168
           returns: added switch
169
           side effect: increments the listenPort member variable."""
170
        if self.switch == UserSwitch:
171
            sw = self.switch( name, listenPort=self.listenPort,
172
                defaultMAC=mac, defaultIP=ip, inNamespace=self.inNamespace )
173
        else:
174
            sw = self.switch( name, listenPort=self.listenPort,
175
                defaultMAC=mac, defaultIP=ip, dp=self.dps,
176
                inNamespace=self.inNamespace )
177
        if not self.inNamespace:
178
            self.listenPort += 1
179
        self.dps += 1
180
        self.switches.append( sw )
181
        self.nameToNode[ name ] = sw
182
        return sw
183

    
184
    def addController( self, name='c0', **kwargs ):
185
        """Add controller.
186
           controller: Controller class"""
187
        controller_new = self.controller( name, **kwargs )
188
        if controller_new:  # allow controller-less setups
189
            self.controllers.append( controller_new )
190
            self.nameToNode[ name ] = controller_new
191

    
192
    # Control network support:
193
    #
194
    # Create an explicit control network. Currently this is only
195
    # used by the user datapath configuration.
196
    #
197
    # Notes:
198
    #
199
    # 1. If the controller and switches are in the same (e.g. root)
200
    #    namespace, they can just use the loopback connection.
201
    #
202
    # 2. If we can get unix domain sockets to work, we can use them
203
    #    instead of an explicit control network.
204
    #
205
    # 3. Instead of routing, we could bridge or use 'in-band' control.
206
    #
207
    # 4. Even if we dispense with this in general, it could still be
208
    #    useful for people who wish to simulate a separate control
209
    #    network (since real networks may need one!)
210

    
211
    def configureControlNetwork( self ):
212
        "Configure control network."
213
        self.configureRoutedControlNetwork()
214

    
215
    # We still need to figure out the right way to pass
216
    # in the control network location.
217

    
218
    def configureRoutedControlNetwork( self, ip='192.168.123.1',
219
        prefixLen=16 ):
220
        """Configure a routed control network on controller and switches.
221
           For use with the user datapath only right now.
222
           """
223
        controller = self.controllers[ 0 ]
224
        info( controller.name + ' <->' )
225
        cip = ip
226
        snum = ipParse( ip )
227
        for switch in self.switches:
228
            info( ' ' + switch.name )
229
            sintf, cintf = createLink( switch, controller )
230
            snum += 1
231
            while snum & 0xff in [ 0, 255 ]:
232
                snum += 1
233
            sip = ipStr( snum )
234
            controller.setIP( cintf, cip, prefixLen )
235
            switch.setIP( sintf, sip, prefixLen )
236
            controller.setHostRoute( sip, cintf )
237
            switch.setHostRoute( cip, sintf )
238
        info( '\n' )
239
        info( '*** Testing control network\n' )
240
        while not controller.intfIsUp( cintf ):
241
            info( '*** Waiting for', cintf, 'to come up\n' )
242
            sleep( 1 )
243
        for switch in self.switches:
244
            while not switch.intfIsUp( sintf ):
245
                info( '*** Waiting for', sintf, 'to come up\n' )
246
                sleep( 1 )
247
            if self.ping( hosts=[ switch, controller ] ) != 0:
248
                error( '*** Error: control network test failed\n' )
249
                exit( 1 )
250
        info( '\n' )
251

    
252
    def configHosts( self ):
253
        "Configure a set of hosts."
254
        # params were: hosts, ips
255
        for host in self.hosts:
256
            hintf = host.intfs[ 0 ]
257
            host.setIP( hintf, host.defaultIP, self.cparams.prefixLen )
258
            host.setDefaultRoute( hintf )
259
            # You're low priority, dude!
260
            quietRun( 'renice +18 -p ' + repr( host.pid ) )
261
            info( host.name + ' ' )
262
        info( '\n' )
263

    
264
    def buildFromTopo( self, topo ):
265
        """Build mininet from a topology object
266
           At the end of this function, everything should be connected
267
           and up."""
268

    
269
        def addNode( prefix, addMethod, nodeId ):
270
            "Add a host or a switch."
271
            name = prefix + topo.name( nodeId )
272
            mac = macColonHex( nodeId ) if self.setMacs else None
273
            ip = topo.ip( nodeId )
274
            node = addMethod( name, mac=mac, ip=ip )
275
            self.idToNode[ nodeId ] = node
276
            info( name + ' ' )
277

    
278
        # Possibly we should clean up here and/or validate
279
        # the topo
280
        if self.cleanup:
281
            pass
282

    
283
        info( '*** Adding controller\n' )
284
        self.addController( 'c0' )
285
        info( '*** Creating network\n' )
286
        info( '*** Adding hosts:\n' )
287
        for hostId in sorted( topo.hosts() ):
288
            addNode( 'h', self.addHost, hostId )
289
        info( '\n*** Adding switches:\n' )
290
        for switchId in sorted( topo.switches() ):
291
            addNode( 's', self.addSwitch, switchId )
292
        info( '\n*** Adding links:\n' )
293
        for srcId, dstId in sorted( topo.edges() ):
294
            src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
295
            srcPort, dstPort = topo.port( srcId, dstId )
296
            createLink( src, dst, srcPort, dstPort )
297
            info( '(%s, %s) ' % ( src.name, dst.name ) )
298
        info( '\n' )
299

    
300
    def build( self ):
301
        "Build mininet."
302
        if self.topo:
303
            self.buildFromTopo( self.topo )
304
        if self.inNamespace:
305
            info( '*** Configuring control network\n' )
306
            self.configureControlNetwork()
307
        info( '*** Configuring hosts\n' )
308
        self.configHosts()
309
        if self.xterms:
310
            self.startTerms()
311
        if self.autoSetMacs:
312
            self.setMacs()
313
        if self.autoStaticArp:
314
            self.staticArp()
315
        self.built = True
316

    
317
    def startTerms( self ):
318
        "Start a terminal for each node."
319
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
320
        cleanUpScreens()
321
        self.terms += makeTerms( self.controllers, 'controller' )
322
        self.terms += makeTerms( self.switches, 'switch' )
323
        self.terms += makeTerms( self.hosts, 'host' )
324

    
325
    def stopXterms( self ):
326
        "Kill each xterm."
327
        # Kill xterms
328
        for term in self.terms:
329
            os.kill( term.pid, signal.SIGKILL )
330
        cleanUpScreens()
331

    
332
    def setMacs( self ):
333
        """Set MAC addrs to correspond to default MACs on hosts.
334
           Assume that the host only has one interface."""
335
        for host in self.hosts:
336
            host.setMAC( host.intfs[ 0 ], host.defaultMAC )
337

    
338
    def staticArp( self ):
339
        "Add all-pairs ARP entries to remove the need to handle broadcast."
340
        for src in self.hosts:
341
            for dst in self.hosts:
342
                if src != dst:
343
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
344

    
345
    def start( self ):
346
        "Start controller and switches."
347
        if not self.built:
348
            self.build()
349
        info( '*** Starting controller\n' )
350
        for controller in self.controllers:
351
            controller.start()
352
        info( '*** Starting %s switches\n' % len( self.switches ) )
353
        for switch in self.switches:
354
            info( switch.name + ' ')
355
            switch.start( self.controllers )
356
        info( '\n' )
357

    
358
    def stop( self ):
359
        "Stop the controller(s), switches and hosts"
360
        if self.terms:
361
            info( '*** Stopping %i terms\n' % len( self.terms ) )
362
            self.stopXterms()
363
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
364
        for host in self.hosts:
365
            info( '%s ' % host.name )
366
            host.terminate()
367
        info( '\n' )
368
        info( '*** Stopping %i switches\n' % len( self.switches ) )
369
        for switch in self.switches:
370
            info( switch.name )
371
            switch.stop()
372
        info( '\n' )
373
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
374
        for controller in self.controllers:
375
            controller.stop()
376
        info( '*** Done\n' )
377

    
378
    def run( self, test, *args, **kwargs ):
379
        "Perform a complete start/test/stop cycle."
380
        self.start()
381
        info( '*** Running test\n' )
382
        result = test( *args, **kwargs )
383
        self.stop()
384
        return result
385

    
386
    def monitor( self, hosts=None, timeoutms=-1 ):
387
        """Monitor a set of hosts (or all hosts by default),
388
           and return their output, a line at a time.
389
           hosts: (optional) set of hosts to monitor
390
           timeoutms: (optional) timeout value in ms
391
           returns: iterator which returns host, line"""
392
        if hosts is None:
393
            hosts = self.hosts
394
        poller = select.poll()
395
        Node = hosts[ 0 ]  # so we can call class method fdToNode
396
        for host in hosts:
397
            poller.register( host.stdout )
398
        while True:
399
            ready = poller.poll( timeoutms )
400
            for fd, event in ready:
401
                host = Node.fdToNode( fd )
402
                if event & select.POLLIN:
403
                    line = host.readline()
404
                    if line is not None:
405
                        yield host, line
406
            # Return if non-blocking
407
            if not ready and timeoutms >= 0:
408
                yield None, None
409

    
410
    @staticmethod
411
    def _parsePing( pingOutput ):
412
        "Parse ping output and return packets sent, received."
413
        # Check for downed link
414
        if 'connect: Network is unreachable' in pingOutput:
415
            return (1, 0)
416
        r = r'(\d+) packets transmitted, (\d+) received'
417
        m = re.search( r, pingOutput )
418
        if m == None:
419
            error( '*** Error: could not parse ping output: %s\n' %
420
                     pingOutput )
421
            return (1, 0)
422
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
423
        return sent, received
424

    
425
    def ping( self, hosts=None ):
426
        """Ping between all specified hosts.
427
           hosts: list of hosts
428
           returns: ploss packet loss percentage"""
429
        # should we check if running?
430
        packets = 0
431
        lost = 0
432
        ploss = None
433
        if not hosts:
434
            hosts = self.hosts
435
            output( '*** Ping: testing ping reachability\n' )
436
        for node in hosts:
437
            output( '%s -> ' % node.name )
438
            for dest in hosts:
439
                if node != dest:
440
                    result = node.cmd( 'ping -c1 ' + dest.IP() )
441
                    sent, received = self._parsePing( result )
442
                    packets += sent
443
                    if received > sent:
444
                        error( '*** Error: received too many packets' )
445
                        error( '%s' % result )
446
                        node.cmdPrint( 'route' )
447
                        exit( 1 )
448
                    lost += sent - received
449
                    output( ( '%s ' % dest.name ) if received else 'X ' )
450
            output( '\n' )
451
            ploss = 100 * lost / packets
452
        output( "*** Results: %i%% dropped (%d/%d lost)\n" %
453
                ( ploss, lost, packets ) )
454
        return ploss
455

    
456
    def pingAll( self ):
457
        """Ping between all hosts.
458
           returns: ploss packet loss percentage"""
459
        return self.ping()
460

    
461
    def pingPair( self ):
462
        """Ping between first two hosts, useful for testing.
463
           returns: ploss packet loss percentage"""
464
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
465
        return self.ping( hosts=hosts )
466

    
467
    @staticmethod
468
    def _parseIperf( iperfOutput ):
469
        """Parse iperf output and return bandwidth.
470
           iperfOutput: string
471
           returns: result string"""
472
        r = r'([\d\.]+ \w+/sec)'
473
        m = re.search( r, iperfOutput )
474
        if m:
475
            return m.group( 1 )
476
        else:
477
            raise Exception( 'could not parse iperf output: ' + iperfOutput )
478

    
479
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M' ):
480
        """Run iperf between two hosts.
481
           hosts: list of hosts; if None, uses opposite hosts
482
           l4Type: string, one of [ TCP, UDP ]
483
           returns: results two-element array of server and client speeds"""
484
        if not hosts:
485
            hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
486
        else:
487
            assert len( hosts ) == 2
488
        client, server = hosts
489
        output( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
490
        output( "%s and %s\n" % ( client.name, server.name ) )
491
        server.cmd( 'killall -9 iperf' )
492
        iperfArgs = 'iperf '
493
        bwArgs = ''
494
        if l4Type == 'UDP':
495
            iperfArgs += '-u '
496
            bwArgs = '-b ' + udpBw + ' '
497
        elif l4Type != 'TCP':
498
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
499
        server.sendCmd( iperfArgs + '-s', printPid=True )
500
        servout = ''
501
        while server.lastPid is None:
502
            servout += server.monitor()
503
        cliout = client.cmd( iperfArgs + '-t 5 -c ' + server.IP() + ' ' +
504
                           bwArgs )
505
        debug( 'Client output: %s\n' % cliout )
506
        server.sendInt()
507
        servout += server.waitOutput()
508
        debug( 'Server output: %s\n' % servout )
509
        result = [ self._parseIperf( servout ), self._parseIperf( cliout ) ]
510
        if l4Type == 'UDP':
511
            result.insert( 0, udpBw )
512
        output( '*** Results: %s\n' % result )
513
        return result
514

    
515
    def configLinkStatus( self, src, dst, status ):
516
        """Change status of src <-> dst links.
517
           src: node name
518
           dst: node name
519
           status: string {up, down}"""
520
        if src not in self.nameToNode:
521
            error( 'src not in network: %s\n' % src )
522
        elif dst not in self.nameToNode:
523
            error( 'dst not in network: %s\n' % dst )
524
        else:
525
            srcNode, dstNode = self.nameToNode[ src ], self.nameToNode[ dst ]
526
            connections = srcNode.connectionsTo( dstNode )
527
            if len( connections ) == 0:
528
                error( 'src and dst not connected: %s %s\n' % ( src, dst) )
529
            for srcIntf, dstIntf in connections:
530
                result = srcNode.cmd( 'ifconfig', srcIntf, status )
531
                if result:
532
                    error( 'link src status change failed: %s\n' % result )
533
                result = dstNode.cmd( 'ifconfig', dstIntf, status )
534
                if result:
535
                    error( 'link dst status change failed: %s\n' % result )
536

    
537
    def interact( self ):
538
        "Start network and run our simple CLI."
539
        self.start()
540
        result = CLI( self )
541
        self.stop()
542
        return result
543

    
544

    
545
# pylint thinks inited is unused
546
# pylint: disable-msg=W0612
547

    
548
def init():
549
    "Initialize Mininet."
550
    if init.inited:
551
        return
552
    if os.getuid() != 0:
553
        # Note: this script must be run as root
554
        # Perhaps we should do so automatically!
555
        print "*** Mininet must run as root."
556
        exit( 1 )
557
    # If which produces no output, then mnexec is not in the path.
558
    # May want to loosen this to handle mnexec in the current dir.
559
    if not quietRun( 'which mnexec' ):
560
        raise Exception( "Could not find mnexec - check $PATH" )
561
    fixLimits()
562
    init.inited = True
563

    
564
init.inited = False
565

    
566
# pylint: enable-msg=W0612