Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ efc9a01c

History | View | Annotate | Download (18 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 KernelSwitch, OVSKernelSwitch
86
from mininet.util import quietRun, fixLimits
87
from mininet.util import createLink, macColonHex
88
from mininet.xterm import cleanUpScreens, makeXterms
89

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

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

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

    
108
    def __init__( self, topo, switch, host, controller, cparams,
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
           now: 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 to DPIDs?
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
        if topo and build:
144
            self.buildFromTopo( self.topo )
145

    
146
    def addHost( self, name, defaultMac=None, defaultIp=None ):
147
        """Add host.
148
           name: name of host to add
149
           defaultMac: default MAC address for intf 0
150
           defaultIp: default IP address for intf 0
151
           returns: added host"""
152
        host = self.host( name )
153
        self.hosts.append( host )
154
        self.nameToNode[ name ] = host
155
        # May wish to add this to actual object
156
        if defaultMac:
157
            host.defaultMac = defaultMac
158
        if defaultIp:
159
            host.defaultIP = defaultIp
160
        return host
161

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

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

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

    
205
    def _configureControlNetwork( self ):
206
        "Configure control network."
207
        self._configureRoutedControlNetwork()
208

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

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

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

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

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

    
299
        info( '*** Configuring hosts\n' )
300
        self._configHosts()
301

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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