Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 80be5642

History | View | Annotate | Download (18.3 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 signal
89
from time import sleep
90

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

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

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

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

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

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

    
154
        if topo and build:
155
            self.buildFromTopo( self.topo )
156

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

    
168
    def addSwitch( self, name, mac=None ):
169
        """Add switch.
170
           name: name of switch to add
171
           mac: default MAC address for kernel/OVS switch intf 0
172
           returns: added switch"""
173
        if self.switch is KernelSwitch or self.switch is OVSKernelSwitch:
174
            sw = self.switch( name, dp=self.dps, defaultMAC=mac )
175
            self.dps += 1
176
        else:
177
            sw = self.switch( name )
178
        self.switches.append( sw )
179
        self.nameToNode[ name ] = sw
180
        return sw
181

    
182
    def addController( self, controller ):
183
        """Add controller.
184
           controller: Controller class"""
185
        controller = self.controller( 'c0', self.inNamespace )
186
        if controller: # allow controller-less setups
187
            self.controllers.append( controller )
188
            self.nameToNode[ 'c0' ] = controller
189

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

    
209
    def _configureControlNetwork( self ):
210
        "Configure control network."
211
        self._configureRoutedControlNetwork()
212

    
213
    def _configureRoutedControlNetwork( self ):
214
        """Configure a routed control network on controller and switches.
215
           For use with the user datapath only right now.
216
           TODO( brandonh ) test this code!
217
           """
218
        # params were: controller, switches, ips
219

    
220
        controller = self.controllers[ 0 ]
221
        info( '%s <-> ' % controller.name )
222
        for switch in self.switches:
223
            info( '%s ' % switch.name )
224
            sip = switch.defaultIP
225
            sintf = switch.intfs[ 0 ]
226
            node, cintf = switch.connection[ sintf ]
227
            if node != controller:
228
                error( '*** Error: switch %s not connected to correct'
229
                         'controller' %
230
                         switch.name )
231
                exit( 1 )
232
            controller.setIP( cintf, self.cparams.ip, self.cparams.prefixLen )
233
            switch.setIP( sintf, sip, self.cparams.prefixLen )
234
            controller.setHostRoute( sip, cintf )
235
            switch.setHostRoute( self.cparams.ip, sintf )
236
        info( '\n' )
237
        info( '*** Testing control network\n' )
238
        while not controller.intfIsUp( controller.intfs[ 0 ] ):
239
            info( '*** Waiting for %s to come up\n',
240
                controller.intfs[ 0 ] )
241
            sleep( 1 )
242
        for switch in self.switches:
243
            while not switch.intfIsUp( switch.intfs[ 0 ] ):
244
                info( '*** Waiting for %s to come up\n' %
245
                    switch.intfs[ 0 ] )
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
        if self.cleanup:
269
            pass # cleanup
270
        # validate topo?
271
        info( '*** Adding controller\n' )
272
        self.addController( self.controller )
273
        info( '*** Creating network\n' )
274
        info( '*** Adding hosts:\n' )
275
        for hostId in sorted( topo.hosts() ):
276
            name = 'h' + topo.name( hostId )
277
            mac = macColonHex( hostId ) if self.setMacs else None
278
            ip = topo.ip( hostId )
279
            host = self.addHost( name, ip=ip, mac=mac )
280
            self.idToNode[ hostId ] = host
281
            info( name + ' ' )
282
        info( '\n*** Adding switches:\n' )
283
        for switchId in sorted( topo.switches() ):
284
            name = 's' + topo.name( switchId )
285
            mac = macColonHex( switchId) if self.setMacs else None
286
            switch = self.addSwitch( name, mac=mac )
287
            self.idToNode[ switchId ] = switch
288
            info( name + ' ' )
289
        info( '\n*** Adding edges:\n' )
290
        for srcId, dstId in sorted( topo.edges() ):
291
            src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
292
            srcPort, dstPort = topo.port( srcId, dstId )
293
            createLink( src, srcPort, dst, dstPort )
294
            info( '(%s, %s) ' % ( src.name, dst.name ) )
295
        info( '\n' )
296

    
297
        if self.inNamespace:
298
            info( '*** Configuring control network\n' )
299
            self._configureControlNetwork()
300

    
301
        info( '*** Configuring hosts\n' )
302
        self.configHosts()
303

    
304
        if self.xterms:
305
            self.startXterms()
306
        if self.autoSetMacs:
307
            self.setMacs()
308
        if self.autoStaticArp:
309
            self.staticArp()
310

    
311
    def startXterms( self ):
312
        "Start an xterm for each node."
313
        info( "*** Running xterms on %s\n" % os.environ[ 'DISPLAY' ] )
314
        cleanUpScreens()
315
        self.terms += makeXterms( self.controllers, 'controller' )
316
        self.terms += makeXterms( self.switches, 'switch' )
317
        self.terms += makeXterms( self.hosts, 'host' )
318

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

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

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

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

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

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

    
378
    @staticmethod
379
    def _parsePing( pingOutput ):
380
        "Parse ping output and return packets sent, received."
381
        r = r'(\d+) packets transmitted, (\d+) received'
382
        m = re.search( r, pingOutput )
383
        if m == None:
384
            error( '*** Error: could not parse ping output: %s\n' %
385
                     pingOutput )
386
            exit( 1 )
387
        sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
388
        return sent, received
389

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

    
422
    def pingAll( self ):
423
        """Ping between all hosts.
424
           returns: ploss packet loss percentage"""
425
        return self.ping()
426

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

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

    
445
    def iperf( self, hosts=None, l4Type='TCP', udpBw='10M',
446
              verbose=False ):
447
        """Run iperf between two hosts.
448
           hosts: list of hosts; if None, uses opposite hosts
449
           l4Type: string, one of [ TCP, UDP ]
450
           verbose: verbose printing
451
           returns: results two-element array of server and client speeds"""
452
        log = info if verbose else debug
453
        if not hosts:
454
            hosts = [ self.hosts[ 0 ], self.hosts[ -1 ] ]
455
        else:
456
            assert len( hosts ) == 2
457
        host0, host1 = hosts
458
        log( '*** Iperf: testing ' + l4Type + ' bandwidth between ' )
459
        log( "%s and %s\n" % ( host0.name, host1.name ) )
460
        host0.cmd( 'killall -9 iperf' )
461
        iperfArgs = 'iperf '
462
        bwArgs = ''
463
        if l4Type == 'UDP':
464
            iperfArgs += '-u '
465
            bwArgs = '-b ' + udpBw + ' '
466
        elif l4Type != 'TCP':
467
            raise Exception( 'Unexpected l4 type: %s' % l4Type )
468
        server = host0.cmd( iperfArgs + '-s &' )
469
        log( '%s\n' % server )
470
        client = host1.cmd( iperfArgs + '-t 5 -c ' + host0.IP() + ' ' +
471
                           bwArgs )
472
        log( '%s\n' % client )
473
        server = host0.cmd( 'killall -9 iperf' )
474
        log( '%s\n' % server )
475
        result = [ self._parseIperf( server ), self._parseIperf( client ) ]
476
        if l4Type == 'UDP':
477
            result.insert( 0, udpBw )
478
        log( '*** Results: %s\n' % result )
479
        return result
480

    
481
    def iperfUdp( self, udpBw='10M' ):
482
        "Run iperf UDP test."
483
        return self.iperf( l4Type='UDP', udpBw=udpBw )
484

    
485
    def interact( self ):
486
        "Start network and run our simple CLI."
487
        self.start()
488
        result = CLI( self )
489
        self.stop()
490
        return result