Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 086ef80e

History | View | Annotate | Download (17.9 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.
26

27
In kernel datapath mode, the controller and switches are simply
28
processes in the root namespace.
29

30
Kernel OpenFlow datapaths are instantiated using dpctl(8), and are
31
attached to the one side of a veth pair; the other side resides in the
32
host namespace. In this mode, switch processes can simply connect to the
33
controller via the loopback interface.
34

35
In user datapath mode, the controller and switches can be full-service
36
nodes that live in their own network namespaces and have management
37
interfaces and IP addresses on a control network (e.g. 10.0.123.1,
38
currently routed although it could be bridged.)
39

40
In addition to a management interface, user mode switches also have
41
several switch interfaces, halves of veth pairs whose other halves
42
reside in the host nodes that the switches are connected to.
43

44
Consistent, straightforward naming is important in order to easily
45
identify hosts, switches and controllers, both from the CLI and
46
from program code. Interfaces are named to make it easy to identify
47
which interfaces belong to which node.
48

49
The basic naming scheme is as follows:
50

51
    Host nodes are named h1-hN
52
    Switch nodes are named s0-sN
53
    Controller nodes are named c0-cN
54
    Interfaces are named {nodename}-eth0 .. {nodename}-ethN
55

56
Currently we wrap the entire network in a 'mininet' object, which
57
constructs a simulated network based on a network topology created
58
using a topology object (e.g. LinearTopo) from topo.py and a Controller
59
node which the switches will connect to.  Several
60
configuration options are provided for functions such as
61
automatically setting MAC addresses, populating the ARP table, or
62
even running a set of xterms to allow direct interaction with nodes.
63

64
After the mininet is created, it can be started using start(), and a variety
65
of useful tasks maybe performed, including basic connectivity and
66
bandwidth tests and running the mininet CLI.
67

68
Once the network is up and running, test code can easily get access
69
to its host and switch objects, which can then be used
70
for arbitrary experiments, which typically involve running a series of
71
commands on the hosts.
72

73
After all desired tests or activities have been completed, the stop()
74
method may be called to shut down the network.
75

76
"""
77

    
78
import os
79
import re
80
import signal
81
from time import sleep
82

    
83
from mininet.cli import CLI
84
from mininet.log import info, error, debug
85
from mininet.node import Host, KernelSwitch, OVSKernelSwitch, Controller
86
from mininet.node import ControllerParams
87
from mininet.util import quietRun, fixLimits
88
from mininet.util import createLink, macColonHex
89
from mininet.xterm import cleanUpScreens, makeXterms
90

    
91
DATAPATHS = [ 'kernel' ] #[ 'user', 'kernel' ]
92

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

    
106
class Mininet( object ):
107
    "Network emulation with hosts spawned in network namespaces."
108

    
109
    def __init__( self, topo, switch=KernelSwitch, host=Host,
110
                 controller=Controller,
111
                 cparams=ControllerParams( '10.0.0.0', 8 ),
112
                 build=True, xterms=False, cleanup=False,
113
                 inNamespace=False,
114
                 autoSetMacs=False, autoStaticArp=False ):
115
        """Create Mininet object.
116
           topo: Topo (topology) object or None
117
           switch: Switch class
118
           host: Host class
119
           controller: Controller class
120
           cparams: ControllerParams object
121
           now: build now from topo?
122
           xterms: if build now, spawn xterms?
123
           cleanup: if build now, cleanup before creating?
124
           inNamespace: spawn switches and controller in net namespaces?
125
           autoSetMacs: set MAC addrs to DPIDs?
126
           autoStaticArp: set all-pairs static MAC addrs?"""
127
        self.switch = switch
128
        self.host = host
129
        self.controller = controller
130
        self.cparams = cparams
131
        self.topo = topo
132
        self.inNamespace = inNamespace
133
        self.xterms = xterms
134
        self.cleanup = cleanup
135
        self.autoSetMacs = autoSetMacs
136
        self.autoStaticArp = autoStaticArp
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
        if topo and build:
147
            self.buildFromTopo( self.topo )
148

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

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

    
174
    def addController( self, controller ):
175
        """Add controller.
176
           controller: Controller class"""
177
        controller = self.controller( 'c0', self.inNamespace )
178
        if controller: # allow controller-less setups
179
            self.controllers.append( controller )
180
            self.nameToNode[ 'c0' ] = controller
181

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

    
203
    def _configureControlNetwork( self ):
204
        "Configure control network."
205
        self._configureRoutedControlNetwork()
206

    
207
    def _configureRoutedControlNetwork( self ):
208
        """Configure a routed control network on controller and switches.
209
           For use with the user datapath only right now.
210
           TODO( brandonh ) test this code!
211
           """
212
        # params were: controller, switches, ips
213

    
214
        controller = self.controllers[ 0 ]
215
        info( '%s <-> ' % controller.name )
216
        for switch in self.switches:
217
            info( '%s ' % switch.name )
218
            sip = switch.defaultIP
219
            sintf = switch.intfs[ 0 ]
220
            node, cintf = switch.connection[ sintf ]
221
            if node != controller:
222
                error( '*** Error: switch %s not connected to correct'
223
                         'controller' %
224
                         switch.name )
225
                exit( 1 )
226
            controller.setIP( cintf, self.cparams.ip, self.cparams.prefixLen )
227
            switch.setIP( sintf, sip, self.cparams.prefixLen )
228
            controller.setHostRoute( sip, cintf )
229
            switch.setHostRoute( self.cparams.ip, sintf )
230
        info( '\n' )
231
        info( '*** Testing control network\n' )
232
        while not controller.intfIsUp( controller.intfs[ 0 ] ):
233
            info( '*** Waiting for %s to come up\n',
234
                controller.intfs[ 0 ] )
235
            sleep( 1 )
236
        for switch in self.switches:
237
            while not switch.intfIsUp( switch.intfs[ 0 ] ):
238
                info( '*** Waiting for %s to come up\n' %
239
                    switch.intfs[ 0 ] )
240
                sleep( 1 )
241
            if self.ping( hosts=[ switch, controller ] ) != 0:
242
                error( '*** Error: control network test failed\n' )
243
                exit( 1 )
244
        info( '\n' )
245

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

    
258
    def buildFromTopo( self, topo ):
259
        """Build mininet from a topology object
260
           At the end of this function, everything should be connected
261
           and up."""
262
        if self.cleanup:
263
            pass # cleanup
264
        # validate topo?
265
        info( '*** Adding controller\n' )
266
        self.addController( self.controller )
267
        info( '*** Creating network\n' )
268
        info( '*** Adding hosts:\n' )
269
        for hostId in sorted( topo.hosts() ):
270
            name = 'h' + topo.name( hostId )
271
            mac = macColonHex( hostId ) if self.setMacs else None
272
            ip = topo.ip( hostId )
273
            host = self.addHost( name, ip=ip, mac=mac )
274
            self.idToNode[ hostId ] = host
275
            info( name + ' ' )
276
        info( '\n*** Adding switches:\n' )
277
        for switchId in sorted( topo.switches() ):
278
            name = 's' + topo.name( switchId )
279
            mac = macColonHex( switchId) if self.setMacs else None
280
            switch = self.addSwitch( name, mac=mac )
281
            self.idToNode[ switchId ] = switch
282
            info( name + ' ' )
283
        info( '\n*** Adding edges:\n' )
284
        for srcId, dstId in sorted( topo.edges() ):
285
            src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ]
286
            srcPort, dstPort = topo.port( srcId, dstId )
287
            createLink( src, srcPort, dst, dstPort )
288
            info( '(%s, %s) ' % ( src.name, dst.name ) )
289
        info( '\n' )
290

    
291
        if self.inNamespace:
292
            info( '*** Configuring control network\n' )
293
            self._configureControlNetwork()
294

    
295
        info( '*** Configuring hosts\n' )
296
        self._configHosts()
297

    
298
        if self.xterms:
299
            self.startXterms()
300
        if self.autoSetMacs:
301
            self.setMacs()
302
        if self.autoStaticArp:
303
            self.staticArp()
304

    
305
    def startXterms( self ):
306
        "Start an xterm for each node."
307
        info( "*** Running xterms on %s\n" % os.environ[ 'DISPLAY' ] )
308
        cleanUpScreens()
309
        self.terms += makeXterms( self.controllers, 'controller' )
310
        self.terms += makeXterms( self.switches, 'switch' )
311
        self.terms += makeXterms( self.hosts, 'host' )
312

    
313
    def stopXterms( self ):
314
        "Kill each xterm."
315
        # Kill xterms
316
        for term in self.terms:
317
            os.kill( term.pid, signal.SIGKILL )
318
        cleanUpScreens()
319

    
320
    def setMacs( self ):
321
        """Set MAC addrs to correspond to datapath IDs on hosts.
322
           Assume that the host only has one interface."""
323
        for host in self.hosts:
324
            host.setMAC( host.intfs[ 0 ], host.defaultMAC )
325

    
326
    def staticArp( self ):
327
        "Add all-pairs ARP entries to remove the need to handle broadcast."
328
        for src in self.hosts:
329
            for dst in self.hosts:
330
                if src != dst:
331
                    src.setARP( ip=dst.IP(), mac=dst.defaultMAC )
332

    
333
    def start( self ):
334
        "Start controller and switches"
335
        info( '*** Starting controller\n' )
336
        for controller in self.controllers:
337
            controller.start()
338
        info( '*** Starting %s switches\n' % len( self.switches ) )
339
        for switch in self.switches:
340
            info( switch.name + ' ')
341
            switch.start( self.controllers )
342
        info( '\n' )
343

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

    
364
    def run( self, test, **params ):
365
        "Perform a complete start/test/stop cycle."
366
        self.start()
367
        info( '*** Running test\n' )
368
        result = getattr( self, test )( **params )
369
        self.stop()
370
        return result
371

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

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

    
416
    def pingAll( self ):
417
        """Ping between all hosts.
418
           returns: ploss packet loss percentage"""
419
        return self.ping()
420

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

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

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

    
475
    def iperfUdp( self, udpBw='10M' ):
476
        "Run iperf UDP test."
477
        return self.iperf( l4Type='UDP', udpBw=udpBw )
478

    
479
    def interact( self ):
480
        "Start network and run our simple CLI."
481
        self.start()
482
        result = CLI( self )
483
        self.stop()
484
        return result