Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ c70aab0a

History | View | Annotate | Download (20.1 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. 10.0.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
Currently we wrap the entire network in a 'mininet' object, which
65
constructs a simulated network based on a network topology created
66
using a topology object (e.g. LinearTopo) from mininet.topo or
67
mininet.topolib, and a Controller which the switches will connect
68
to. Several configuration options are provided for functions such as
69
automatically setting MAC addresses, populating the ARP table, or
70
even running a set of xterms to allow direct interaction with nodes.
71

72
After the network is created, it can be started using start(), and a
73
variety of useful tasks maybe performed, including basic connectivity
74
and bandwidth tests and running the mininet CLI.
75

76
Once the network is up and running, test code can easily get access
77
to host and switch objects which can then be used for arbitrary
78
experiments, typically involving running a series of commands on the
79
hosts.
80

81
After all desired tests or activities have been completed, the stop()
82
method may be called to shut down the network.
83

84
"""
85

    
86
import os
87
import re
88
import select
89
import signal
90
from time import sleep
91

    
92
from mininet.cli import CLI
93
from mininet.log import info, error, debug, output
94
from mininet.node import Host, UserSwitch, KernelSwitch, Controller
95
from mininet.node import ControllerParams
96
from mininet.util import quietRun, fixLimits
97
from mininet.util import createLink, macColonHex, ipStr, ipParse
98
from mininet.xterm import cleanUpScreens, makeXterms
99

    
100
DATAPATHS = [ 'kernel' ] #[ 'user', 'kernel' ]
101

    
102
def init():
103
    "Initialize Mininet."
104
    if os.getuid() != 0:
105
        # Note: this script must be run as root
106
        # Perhaps we should do so automatically!
107
        print "*** Mininet must run as root."
108
        exit( 1 )
109
    # If which produces no output, then mnexec is not in the path.
110
    # May want to loosen this to handle mnexec in the current dir.
111
    if not quietRun( [ 'which', 'mnexec' ] ):
112
        raise Exception( "Could not find mnexec - check $PATH" )
113
    fixLimits()
114

    
115
class Mininet( object ):
116
    "Network emulation with hosts spawned in network namespaces."
117

    
118
    def __init__( self, topo, switch=KernelSwitch, host=Host,
119
                 controller=Controller,
120
                 cparams=ControllerParams( '10.0.0.0', 8 ),
121
                 build=True, xterms=False, cleanup=False,
122
                 inNamespace=False,
123
                 autoSetMacs=False, autoStaticArp=False ):
124
        """Create Mininet object.
125
           topo: Topo (topology) object or None
126
           switch: Switch class
127
           host: Host class
128
           controller: Controller class
129
           cparams: ControllerParams object
130
           build: build now from topo?
131
           xterms: if build now, spawn xterms?
132
           cleanup: if build now, cleanup before creating?
133
           inNamespace: spawn switches and controller in net namespaces?
134
           autoSetMacs: set MAC addrs from topo?
135
           autoStaticArp: set all-pairs static MAC addrs?"""
136
        self.switch = switch
137
        self.host = host
138
        self.controller = controller
139
        self.cparams = cparams
140
        self.topo = topo
141
        self.inNamespace = inNamespace
142
        self.xterms = xterms
143
        self.cleanup = cleanup
144
        self.autoSetMacs = autoSetMacs
145
        self.autoStaticArp = autoStaticArp
146

    
147
        self.hosts = []
148
        self.switches = []
149
        self.controllers = []
150
        self.nameToNode = {} # name to Node (Host/Switch) objects
151
        self.idToNode = {} # dpid to Node (Host/Switch) objects
152
        self.dps = 0 # number of created kernel datapaths
153
        self.terms = [] # list of spawned xterm processes
154

    
155
        switch.setup()
156

    
157
        if build:
158
            self.build()
159

    
160
    def addHost( self, name, mac=None, ip=None ):
161
        """Add host.
162
           name: name of host to add
163
           mac: default MAC address for intf 0
164
           ip: default IP address for intf 0
165
           returns: added host"""
166
        host = self.host( name, defaultMAC=mac, defaultIP=ip )
167
        self.hosts.append( host )
168
        self.nameToNode[ name ] = host
169
        return host
170

    
171
    def addSwitch( self, name, mac=None, ip=None ):
172
        """Add switch.
173
           name: name of switch to add
174
           mac: default MAC address for kernel/OVS switch intf 0
175
           returns: added switch"""
176
        if self.switch == UserSwitch:
177
            sw = self.switch( name, defaultMAC=mac, defaultIP=ip,
178
                inNamespace=self.inNamespace )
179
        else:
180
            sw = self.switch( name, defaultMAC=mac, defaultIP=ip, dp=self.dps,
181
                inNamespace=self.inNamespace )
182
        self.dps += 1
183
        self.switches.append( sw )
184
        self.nameToNode[ name ] = sw
185
        return sw
186

    
187
    def addController( self, controller ):
188
        """Add controller.
189
           controller: Controller class"""
190
        controller = self.controller( 'c0', self.inNamespace )
191
        if controller: # allow controller-less setups
192
            self.controllers.append( controller )
193
            self.nameToNode[ 'c0' ] = controller
194

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

    
214
    def configureControlNetwork( self ):
215
        "Configure control network."
216
        self.configureRoutedControlNetwork()
217

    
218
    # We still need to figure out the right way to pass
219
    # in the control network location.
220

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

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

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

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

    
281
        # Possibly we should clean up here and/or validate
282
        # the topo
283
        if self.cleanup:
284
            pass
285

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

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

    
319
    def startXterms( self ):
320
        "Start an xterm for each node."
321
        info( "*** Running xterms on %s\n" % os.environ[ 'DISPLAY' ] )
322
        cleanUpScreens()
323
        self.terms += makeXterms( self.controllers, 'controller' )
324
        self.terms += makeXterms( self.switches, 'switch' )
325
        self.terms += makeXterms( self.hosts, 'host' )
326

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

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

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

    
347
    def start( self ):
348
        "Start controller and switches"
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( '*** Test complete\n' )
377

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

    
386
    def monitor( self, hosts=None ):
387
        """Monitor a set of hosts (or all hosts by default),
388
           and return their output, a line at a time.
389
           returns: host, line"""
390
        if hosts is None:
391
            hosts = self.hosts
392
        poller = select.poll()
393
        Node = hosts[ 0 ] # so we can call class method fdToNode
394
        for host in hosts:
395
            poller.register( host.stdout )
396
        while True:
397
            ready = poller.poll()
398
            for fd, event in ready:
399
                host = Node.fdToNode( fd )
400
                if event & select.POLLIN:
401
                    line = host.readline()
402
                    if line:
403
                        yield host, line
404

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

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

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

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

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

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

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

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

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