Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ c26875cb

History | View | Annotate | Download (20.8 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 ):
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
        self.switch = switch
125
        self.host = host
126
        self.controller = controller
127
        self.cparams = cparams
128
        self.topo = topo
129
        self.inNamespace = inNamespace
130
        self.xterms = xterms
131
        self.cleanup = cleanup
132
        self.autoSetMacs = autoSetMacs
133
        self.autoStaticArp = autoStaticArp
134

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

    
143
        init()
144
        switch.setup()
145

    
146
        self.built = False
147
        if topo and build:
148
            self.build()
149

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

    
161
    def addSwitch( self, name, mac=None, ip=None ):
162
        """Add switch.
163
           name: name of switch to add
164
           mac: default MAC address for kernel/OVS switch intf 0
165
           returns: added switch"""
166
        if self.switch == UserSwitch:
167
            sw = self.switch( name, defaultMAC=mac, defaultIP=ip,
168
                inNamespace=self.inNamespace )
169
        else:
170
            sw = self.switch( name, defaultMAC=mac, defaultIP=ip, dp=self.dps,
171
                inNamespace=self.inNamespace )
172
        self.dps += 1
173
        self.switches.append( sw )
174
        self.nameToNode[ name ] = sw
175
        return sw
176

    
177
    def addController( self, name='c0', **kwargs ):
178
        """Add controller.
179
           controller: Controller class"""
180
        controller_new = self.controller( name, **kwargs )
181
        if controller_new:  # allow controller-less setups
182
            self.controllers.append( controller_new )
183
            self.nameToNode[ name ] = controller_new
184

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

    
204
    def configureControlNetwork( self ):
205
        "Configure control network."
206
        self.configureRoutedControlNetwork()
207

    
208
    # We still need to figure out the right way to pass
209
    # in the control network location.
210

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

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

    
257
    def buildFromTopo( self, topo ):
258
        """Build mininet from a topology object
259
           At the end of this function, everything should be connected
260
           and up."""
261

    
262
        def addNode( prefix, addMethod, nodeId ):
263
            "Add a host or a switch."
264
            name = prefix + topo.name( nodeId )
265
            mac = macColonHex( nodeId ) if self.setMacs else None
266
            ip = topo.ip( nodeId )
267
            node = addMethod( name, mac=mac, ip=ip )
268
            self.idToNode[ nodeId ] = node
269
            info( name + ' ' )
270

    
271
        # Possibly we should clean up here and/or validate
272
        # the topo
273
        if self.cleanup:
274
            pass
275

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

    
293
    def build( self ):
294
        "Build mininet."
295
        if self.topo:
296
            self.buildFromTopo( self.topo )
297
        if self.inNamespace:
298
            info( '*** Configuring control network\n' )
299
            self.configureControlNetwork()
300
        info( '*** Configuring hosts\n' )
301
        self.configHosts()
302
        if self.xterms:
303
            self.startTerms()
304
        if self.autoSetMacs:
305
            self.setMacs()
306
        if self.autoStaticArp:
307
            self.staticArp()
308
        self.built = True
309

    
310
    def startTerms( self ):
311
        "Start a terminal for each node."
312
        info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
313
        cleanUpScreens()
314
        self.terms += makeTerms( self.controllers, 'controller' )
315
        self.terms += makeTerms( self.switches, 'switch' )
316
        self.terms += makeTerms( self.hosts, 'host' )
317

    
318
    def stopXterms( self ):
319
        "Kill each xterm."
320
        # Kill xterms
321
        for term in self.terms:
322
            os.kill( term.pid, signal.SIGKILL )
323
        cleanUpScreens()
324

    
325
    def setMacs( self ):
326
        """Set MAC addrs to correspond to default MACs on hosts.
327
           Assume that the host only has one interface."""
328
        for host in self.hosts:
329
            host.setMAC( host.intfs[ 0 ], host.defaultMAC )
330

    
331
    def staticArp( self ):
332
        "Add all-pairs ARP entries to remove the need to handle broadcast."
333
        for src in self.hosts:
334
            for dst in self.hosts:
335
                if src != dst:
336
                    src.setARP( ip=dst.IP(), mac=dst.MAC() )
337

    
338
    def start( self ):
339
        "Start controller and switches."
340
        if not self.built:
341
            self.build()
342
        info( '*** Starting controller\n' )
343
        for controller in self.controllers:
344
            controller.start()
345
        info( '*** Starting %s switches\n' % len( self.switches ) )
346
        for switch in self.switches:
347
            info( switch.name + ' ')
348
            switch.start( self.controllers )
349
        info( '\n' )
350

    
351
    def stop( self ):
352
        "Stop the controller(s), switches and hosts"
353
        if self.terms:
354
            info( '*** Stopping %i terms\n' % len( self.terms ) )
355
            self.stopXterms()
356
        info( '*** Stopping %i hosts\n' % len( self.hosts ) )
357
        for host in self.hosts:
358
            info( '%s ' % host.name )
359
            host.terminate()
360
        info( '\n' )
361
        info( '*** Stopping %i switches\n' % len( self.switches ) )
362
        for switch in self.switches:
363
            info( switch.name )
364
            switch.stop()
365
        info( '\n' )
366
        info( '*** Stopping %i controllers\n' % len( self.controllers ) )
367
        for controller in self.controllers:
368
            controller.stop()
369
        info( '*** Done\n' )
370

    
371
    def run( self, test, *args, **kwargs ):
372
        "Perform a complete start/test/stop cycle."
373
        self.start()
374
        info( '*** Running test\n' )
375
        result = test( *args, **kwargs )
376
        self.stop()
377
        return result
378

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

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

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

    
449
    def pingAll( self ):
450
        """Ping between all hosts.
451
           returns: ploss packet loss percentage"""
452
        return self.ping()
453

    
454
    def pingPair( self ):
455
        """Ping between first two hosts, useful for testing.
456
           returns: ploss packet loss percentage"""
457
        hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
458
        return self.ping( hosts=hosts )
459

    
460
    @staticmethod
461
    def _parseIperf( iperfOutput ):
462
        """Parse iperf output and return bandwidth.
463
           iperfOutput: string
464
           returns: result string"""
465
        r = r'([\d\.]+ \w+/sec)'
466
        m = re.search( r, iperfOutput )
467
        if m:
468
            return m.group( 1 )
469
        else:
470
            raise Exception( 'could not parse iperf output: ' + iperfOutput )
471

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

    
508
    def iperfUdp( self, udpBw='10M' ):
509
        "Run iperf UDP test."
510
        return self.iperf( l4Type='UDP', udpBw=udpBw )
511

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

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

    
541

    
542
# pylint thinks inited is unused
543
# pylint: disable-msg=W0612
544

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

    
561
init.inited = False
562

    
563
# pylint: enable-msg=W0612