Statistics
| Branch: | Tag: | Revision:

mininet / mininet.py @ 748e35d5

History | View | Annotate | Download (28 KB)

1
#!/usr/bin/python
2

    
3
"""
4
Mininet: A simple networking testbed for OpenFlow!
5

6
Mininet creates simple OpenFlow test networks by using
7
process-based virtualization and network namespaces. 
8

9
This file supports use of either the kernel or user space datapath
10
from the OpenFlow reference implementation. Up to 32 switches are
11
supported using the kernel datapath, and 512 (or more) switches are
12
supported via the user datapath.
13

14
Simulated hosts are created as processes in separate network
15
namespaces. This allows a complete OpenFlow network to be simulated on
16
top of a single Linux kernel.
17

18
Each host has:
19
   A virtual console (pipes to a shell)
20
   A virtual interfaces (half of a veth pair)
21
   A parent shell (and possibly some child processes) in a namespace
22
   
23
Hosts have a network interface which is configured via ifconfig/ip
24
link/etc. with data network IP addresses (e.g. 192.168.123.2 )
25

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

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

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

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

43
Naming:
44
   Host nodes are named h1-hN
45
   Switch nodes are named s0-sN
46
   Interfaces are named {nodename}-eth0 .. {nodename}-ethN,
47

48
Thoughts/TBD:
49

50
   It should be straightforward to add a function to read
51
   OpenFlowVMS  spec files, but I haven't done so yet.
52
   For the moment, specifying configurations and tests in Python
53
   is straightforward and concise.
54
   Soon, we'll want to split the various subsystems (core,
55
   cli, tests, etc.) into multiple modules.
56
   We may be able to get better performance by using the kernel
57
   datapath (using its multiple datapath feature on multiple 
58
   interfaces.) This would eliminate the ofdatapath user processes.
59
   OpenVSwitch would still run at user level.
60
   
61
Bob Lantz
62
rlantz@cs.stanford.edu
63

64
History:
65
11/19/09 Initial revision (user datapath only)
66
12/08/09 Kernel datapath support complete
67
12/09/09 Moved controller and switch routines into classes
68
"""
69

    
70
from subprocess import call, check_call, Popen, PIPE, STDOUT
71
from time import sleep
72
import os, re, signal, sys, select
73
flush = sys.stdout.flush
74
from resource import setrlimit, RLIMIT_NPROC, RLIMIT_NOFILE
75

    
76
# Utility routines to make it easier to run commands
77

    
78
def run( cmd ):
79
   "Simple interface to subprocess.call()"
80
   return call( cmd.split( ' ' ) )
81

    
82
def checkRun( cmd ):
83
   "Simple interface to subprocess.check_call()"
84
   check_call( cmd.split( ' ' ) )
85
   
86
def quietRun( cmd ):
87
   "Run a command, routing stderr to stdout, and return the output."
88
   if isinstance( cmd, str ): cmd = cmd.split( ' ' )
89
   popen = Popen( cmd, stdout=PIPE, stderr=STDOUT)
90
   # We can't use Popen.communicate() because it uses 
91
   # select(), which can't handle
92
   # high file descriptor numbers! poll() can, however.
93
   output = ''
94
   readable = select.poll()
95
   readable.register( popen.stdout )
96
   while True:
97
      while readable.poll(): 
98
         data = popen.stdout.read( 1024 )
99
         if len( data ) == 0: break
100
         output += data
101
      popen.poll()
102
      if popen.returncode != None: break
103
   return output
104
   
105
class Node( object ):
106
   """A virtual network node is simply a shell in a network namespace.
107
      We communicate with it using pipes."""
108
   def __init__( self, name, inNamespace=True ):
109
      self.name = name
110
      closeFds = False # speed vs. memory use
111
      # xpg_echo is needed so we can echo our sentinel in sendCmd
112
      cmd = [ '/bin/bash', '-O', 'xpg_echo' ]
113
      self.inNamespace = inNamespace
114
      if self.inNamespace: cmd = [ 'netns' ] + cmd
115
      self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
116
         close_fds=closeFds )
117
      self.stdin = self.shell.stdin
118
      self.stdout = self.shell.stdout
119
      self.pollOut = select.poll() 
120
      self.pollOut.register( self.stdout )
121
      outToNode[ self.stdout ] = self
122
      inToNode[ self.stdin ] = self
123
      self.pid = self.shell.pid
124
      self.intfCount = 0
125
      self.intfs = []
126
      self.ips = {}
127
      self.connection = {}
128
      self.waiting = False
129
      self.execed = False
130
   def cleanup( self ):
131
      # Help python collect its garbage
132
      self.shell = None
133
   # Subshell I/O, commands and control
134
   def read( self, max ): return os.read( self.stdout.fileno(), max )
135
   def write( self, data ): os.write( self.stdin.fileno(), data )
136
   def terminate( self ):
137
      self.cleanup()
138
      os.kill( self.pid, signal.SIGKILL )
139
   def waitReadable( self ): self.pollOut.poll()
140
   def sendCmd( self, cmd ):
141
      """Send a command, followed by a command to echo a sentinel,
142
         and return without waiting for the command to complete."""
143
      assert not self.waiting
144
      if cmd[ -1 ] == '&':
145
         separator = '&'
146
         cmd = cmd[ : -1 ]
147
      else: separator = ';'
148
      if isinstance( cmd, list): cmd = ' '.join( cmd )
149
      self.write( cmd + separator + " echo -n '\\0177' \n")
150
      self.waiting = True
151
   def monitor( self ):
152
      "Monitor a command's output, returning (done, data)."
153
      assert self.waiting
154
      self.waitReadable()
155
      data = self.read( 1024 )
156
      if len( data ) > 0 and data[ -1 ] == chr( 0177 ):
157
         self.waiting = False
158
         return True, data[ : -1 ]
159
      else:
160
         return False, data
161
   def sendInt( self ):
162
      "Send ^C, hopefully interrupting a running subprocess."
163
      self.write( chr( 3 ) )
164
   def waitOutput( self ):
165
      """Wait for a command to complete (signaled by a sentinel
166
      character, ASCII(127) appearing in the output stream) and return
167
      the output, including trailing newline."""
168
      assert self.waiting
169
      output = ""
170
      while True:
171
         self.waitReadable()
172
         data = self.read( 1024 )
173
         if len(data) > 0  and data[ -1 ] == chr( 0177 ): 
174
            output += data[ : -1 ]
175
            break
176
         else: output += data
177
      self.waiting = False
178
      return output
179
   def cmd( self, cmd ):
180
      "Send a command, wait for output, and return it."
181
      self.sendCmd( cmd )
182
      return self.waitOutput()
183
   def cmdPrint( self, cmd ):
184
      "Call cmd, printing the command and output"
185
      print "***", self.name, ":", cmd
186
      result = self.cmd( cmd )
187
      print result,
188
      return result
189
   # Interface management, configuration, and routing
190
   def intfName( self, n):
191
      "Construct a canonical interface name node-intf for interface N."
192
      return self.name + '-eth' + `n`
193
   def newIntf( self ):
194
      "Reserve and return a new interface name for this node."
195
      intfName = self.intfName( self.intfCount)
196
      self.intfCount += 1
197
      self.intfs += [ intfName ]
198
      return intfName
199
   def setIP( self, intf, ip, bits ):
200
      "Set an interface's IP address."
201
      result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
202
      self.ips[ intf ] = ip
203
      return result
204
   def setHostRoute( self, ip, intf ):
205
      "Add a route to the given IP address via intf."
206
      return self.cmd( 'route add -host ' + ip + ' dev ' + intf )
207
   def setDefaultRoute( self, intf ):
208
      "Set the default route to go through intf."
209
      self.cmd( 'ip route flush' )
210
      return self.cmd( 'route add default ' + intf )
211
   def IP( self ):
212
      "Return IP address of first interface"
213
      return self.ips[ self.intfs[ 0 ] ]
214
   def intfIsUp( self, intf ):
215
      "Check if one of our interfaces is up."
216
      return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
217
   # Other methods  
218
   def __str__( self ): 
219
      result = self.name
220
      result += ": IP=" + self.IP() + " intfs=" + self.intfs
221
      result += " waiting=", self.waiting
222
      return result
223

    
224
# Maintain mapping between i/o pipes and nodes
225
# This could be useful for monitoring multiple nodes
226
# using select.poll()
227

    
228
inToNode = {}
229
outToNode = {}
230
def outputs(): return outToNode.keys()
231
def nodes(): return outToNode.values()
232
def inputs(): return [ node.stdin for node in nodes() ]
233
def nodeFromFile( f ):
234
   node = outToNode.get( f )
235
   return node or inToNode.get( f )
236

    
237
class Host( Node ):
238
   """A host is simply a Node."""
239
   pass
240
      
241
class Controller( Node ):
242
   """A Controller is a Node that is running (or has execed) an 
243
      OpenFlow controller."""
244
   def __init__( self, name, kernel=True ):
245
      Node.__init__( self, name, inNamespace=( not kernel ) )
246
   def start( self, controller='controller', args='ptcp:' ):
247
      "Start <controller> <args> on controller, logging to /tmp/cN.log"
248
      cout = '/tmp/' + self.name + '.log'
249
      self.cmdPrint( controller + ' ' + args + 
250
         ' 1> ' + cout + ' 2> ' + cout + ' &' )
251
   def stop( self, controller='controller' ):
252
      "Stop controller cprog on controller"
253
      self.cmd( "kill %" + controller )  
254
         
255
class Switch( Node ):
256
   """A Switch is a Node that is running (or has execed)
257
      an OpenFlow switch."""
258
   def __init__( self, name, datapath=None ):
259
      self.dp = datapath
260
      Node.__init__( self, name, inNamespace=( datapath == None ) )
261
   def startUserDatapath( self, controller ):
262
      """Start OpenFlow reference user datapath, 
263
         logging to /tmp/sN-{ofd,ofp}.log"""
264
      ofdlog = '/tmp/' + self.name + '-ofd.log'
265
      ofplog = '/tmp/' + self.name + '-ofp.log'
266
      self.cmd( 'ifconfig lo up' )
267
      intfs = self.intfs[ 1 : ] # 0 is mgmt interface
268
      self.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) +
269
       ' ptcp: 1> ' + ofdlog + ' 2> '+ ofdlog + ' &' )
270
      self.cmdPrint( 'ofprotocol tcp:' + controller.IP() +
271
         ' tcp:localhost 1> ' + ofplog + ' 2>' + ofplog + ' &' )
272
   def stopUserDatapath( self ):
273
      "Stop OpenFlow reference user datapath."
274
      self.cmd( "kill %ofdatapath" )
275
      self.cmd( "kill %ofprotocol" )
276
   def startKernelDatapath( self, controller):
277
      "Start up switch using OpenFlow reference kernel datapath."
278
      ofplog = '/tmp/' + self.name + '-ofp.log'
279
      quietRun( 'ifconfig lo up' )
280
      # Delete local datapath if it exists;
281
      # then create a new one monitoring the given interfaces
282
      quietRun( 'dpctl deldp ' + self.dp )
283
      self.cmdPrint( 'dpctl adddp ' + self.dp )
284
      self.cmdPrint( 'dpctl addif ' + self.dp + ' ' + ' '.join( self.intfs ) )
285
      # Become protocol daemon
286
      self.cmdPrint( 'exec ofprotocol' +
287
         ' ' + self.dp + ' tcp:127.0.0.1 1> ' + ofplog + ' 2>' + ofplog + ' &' )
288
      self.execed = True
289
   def stopKernelDatapath( self ):
290
      "Terminate a switch using OpenFlow reference kernel datapath."
291
      quietRun( 'dpctl deldp ' + self.dp )
292
      for intf in self.intfs: quietRun( 'ip link del ' + intf )
293
      self.terminate()
294
   def start( self, controller ): 
295
      if self.dp is None: self.startUserDatapath( controller )
296
      else: self.startKernelDatapath( controller )
297
   def stop( self ):
298
      if self.dp is None: self.stopUserDatapath()
299
      else: self.stopKernelDatapath()
300
   # Handle non-interaction if we've execed
301
   def sendCmd( self, cmd ):
302
      if not self.execed: return Node.sendCmd( self, cmd )
303
      else: print "*** Error:", self.name, "has execed and cannot accept commands"
304
   def monitor( self ):
305
      if not self.execed: return Node.monitor( self )
306
      else: return True, ''
307
         
308
# Interface management
309
# 
310
# Interfaces are managed as strings which are simply the
311
# interface names, of the form "nodeN-ethM".
312
#
313
# To connect nodes, we create a pair of veth interfaces, and then place them
314
# in the pair of nodes that we want to communicate. We then update the node's
315
# list of interfaces and connectivity map.
316
#
317
# For the kernel datapath, switch interfaces
318
# live in the root namespace and thus do not have to be
319
# explicitly moved.
320

    
321
def makeIntfPair( intf1, intf2 ):
322
   "Make a veth pair of intf1 and intf2."
323
   # Delete any old interfaces with the same names
324
   quietRun( 'ip link del ' + intf1 )
325
   quietRun( 'ip link del ' + intf2 )
326
   # Create new pair
327
   cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2
328
   return checkRun( cmd )
329
   
330
def moveIntf( intf, node ):
331
   "Move intf to node."
332
   cmd = 'ip link set ' + intf + ' netns ' + `node.pid`
333
   checkRun( cmd )
334
   links = node.cmd( 'ip link show' )
335
   if not intf in links:
336
      print "*** Error: moveIntf:", intf, "not successfully moved to",
337
      print node.name,":"
338
      exit( 1 )
339
   return
340
   
341
def createLink( node1, node2 ):
342
   "Create a link node1-intf1 <---> node2-intf2."
343
   intf1 = node1.newIntf()
344
   intf2 = node2.newIntf()
345
   makeIntfPair( intf1, intf2 )
346
   if node1.inNamespace: moveIntf( intf1, node1 )
347
   if node2.inNamespace: moveIntf( intf2, node2 )
348
   node1.connection[ intf1 ] = ( node2, intf2 )
349
   node2.connection[ intf2 ] = ( node1, intf1 )
350
   return intf1, intf2
351

    
352
# Handy utilities
353
 
354
def createNodes( name, count ):
355
   "Create and return a list of nodes."
356
   nodes = [ Node( name + `i` ) for i in range( 0, count ) ]
357
   # print "*** CreateNodes: created:", nodes
358
   return nodes
359
     
360
def dumpNodes( nodes ):
361
   "Dump ifconfig of each node."
362
   for node in nodes:
363
      print "*** Dumping node", node.name
364
      print node.cmd( 'ip link show' )
365
      print node.cmd( 'route' )
366
   
367
def ipGen( A, B, c, d ):
368
   "Generate next IP class B IP address, starting at A.B.c.d"
369
   while True:
370
      yield '%d.%d.%d.%d' % ( A, B, c, d )
371
      d += 1
372
      if d > 254:
373
         d = 1
374
         c += 1
375
         if c > 254: break
376

    
377
def nameGen( prefix ):
378
   "Generate names starting with prefix."
379
   i = 0
380
   while True: yield prefix + `i`; i += 1
381
      
382
# Control network support
383
# For the user datapath, we create an explicit control network.
384
# Note: Instead of routing, we could bridge or use "in-band" control
385
   
386
def configRoutedControlNetwork( controller, switches, 
387
   startAddr=( 10, 123, 0, 1 ) ):
388
   """Configure a routed control network on controller and switches,
389
      for use with the user datapath."""
390
   ips = apply( ipGen, startAddr )
391
   cip = ips.next()
392
   print controller.name, '<->',
393
   for switch in switches:
394
      print switch.name, ; flush()
395
      sip = ips.next()
396
      sintf = switch.intfs[ 0 ]
397
      node, cintf = switch.connection[ sintf ]
398
      if node != controller:
399
         print "*** Error: switch", switch.name, 
400
         print "not connected to correct controller"
401
         exit( 1 )
402
      controller.setIP( cintf, cip,  '/24' )
403
      switch.setIP( sintf, sip, '/24' )
404
      controller.setHostRoute( sip, cintf )
405
      switch.setHostRoute( cip, sintf )
406
   print
407
   print "*** Testing control network"
408
   while not controller.intfIsUp( controller.intfs[ 0 ] ):
409
      print "*** Waiting for ", controller.intfs[ 0 ], "to come up"
410
      sleep( 1 )
411
   for switch in switches:
412
      while not switch.intfIsUp( switch.intfs[ 0 ] ):
413
         print "*** Waiting for ", switch.intfs[ 0 ], "to come up"
414
         sleep( 1 )
415
   if pingTest( hosts=[ switch, controller ] ) != 0:
416
      print "*** Error: control network test failed"
417
      exit( 1 )
418

    
419
def configHosts( hosts, ( a, b, c, d ) ):
420
   "Configure a set of hosts, starting at IP address a.b.c.d"
421
   ips = ipGen( a, b, c, d )
422
   for host in hosts:
423
      hintf = host.intfs[ 0 ]
424
      host.setIP( hintf, ips.next(), '/24' )
425
      host.setDefaultRoute( hintf )
426
      # You're low priority, dude!
427
      quietRun( 'renice +18 -p ' + `host.pid` )
428
      print host.name, ; flush()
429
   print
430
 
431
# Test driver and topologies
432

    
433
class Network( object ):
434
   "Network topology (and test driver) base class."
435
   def __init__( self, kernel=True, startAddr=( 192, 168, 123, 1) ):
436
      self.kernel, self.startAddr = kernel, startAddr
437
      # Check for kernel modules
438
      tun = quietRun( [ 'sh', '-c', 'lsmod | grep tun' ] )
439
      ofdatapath = quietRun( [ 'sh', '-c', 'lsmod | grep ofdatapath' ] )
440
      if tun == '' and not kernel: 
441
         print "*** Error: kernel module tun not loaded:",
442
         print " user datapath not supported"
443
         exit( 1 )
444
      if ofdatapath == '' and kernel:
445
         print "*** Error: ofdatapath not loaded:",
446
         print " kernel datapath not supported"
447
         exit( 1 )
448
   # In progress: we probably want to decouple creating/starting/stopping
449
   # the network and running tests, since we might wish to run
450
   # multiple tests on the same network. It's not clear if the network
451
   # should always be started/stopped for each test or not. Probably
452
   # not...
453
   def run( self, test ):
454
      """Create a network by calling makeNet as follows: 
455
         (switches, hosts ) = makeNet()
456
         and then run test( controller, switches, hosts ) on it."""
457
      kernel = self.kernel
458
      if kernel: print "*** Using kernel datapath"
459
      else: print "*** Using user datapath"
460
      print "*** Creating controller"
461
      controller = Controller( 'c0', kernel )
462
      print "*** Creating network"
463
      switches, hosts = self.makeNet( controller )
464
      print
465
      if not kernel:
466
         print "*** Configuring control network"
467
         configRoutedControlNetwork( controller, switches )
468
      print "*** Configuring hosts"
469
      configHosts( hosts, self.startAddr )
470
      print "*** Starting reference controller"
471
      controller.start()
472
      print "*** Starting", len( switches ), "switches"
473
      for switch in switches:
474
         switch.start( controller )
475
      print "*** Running test"
476
      result = test( [ controller ], switches, hosts )
477
      print "*** Stopping controller"
478
      controller.stop(); controller.terminate()
479
      print "*** Stopping switches"
480
      for switch in switches:
481
         switch.stop() ; switch.terminate()
482
      print "*** Stopping hosts"
483
      for host in hosts: host.terminate()
484
      print "*** Test complete"
485
      return result
486
   def interact( self ):
487
      "Create a network and run our simple CLI."
488
      self.run( self, Cli )
489
   
490
def defaultNames( snames=None, hnames=None, dpnames=None ):
491
   "Reinitialize default names from generators, if necessary."
492
   if snames is None: snames = nameGen( 's' )
493
   if hnames is None: hnames = nameGen( 'h' )
494
   if dpnames is None: dpnames = nameGen( 'nl:' )
495
   return snames, hnames, dpnames
496

    
497
# Tree network
498

    
499
class TreeNet( Network ):
500
   "A tree-structured network with the specified depth and fanout"
501
   def __init__( self, depth, fanout, kernel=True):
502
      self.depth, self.fanout = depth, fanout
503
      Network.__init__( self, kernel )
504
   def treeNet( self, controller, depth, fanout, kernel=True, snames=None,
505
      hnames=None, dpnames=None ):
506
      """Return a tree network of the given depth and fanout as a triple:
507
         ( root, switches, hosts ), using the given switch, host and
508
         datapath name generators, with the switches connected to the given
509
         controller. If kernel=True, use the kernel datapath; otherwise the
510
         user datapath will be used."""
511
      # Ugly, but necessary (?) since defaults are only evaluated once
512
      snames, hnames, dpnames = defaultNames( snames, hnames, dpnames )
513
      if ( depth == 0 ):
514
         host = Host( hnames.next() )
515
         print host.name, ; flush()
516
         return host, [], [ host ]
517
      dp = dpnames.next() if kernel else None
518
      switch = Switch( snames.next(), dp )
519
      if not kernel: createLink( switch, controller )
520
      print switch.name, ; flush()
521
      switches, hosts = [ switch ], []
522
      for i in range( 0, fanout ):
523
         child, slist, hlist = self.treeNet( controller, 
524
            depth - 1, fanout, kernel, snames, hnames, dpnames )
525
         createLink( switch, child )
526
         switches += slist
527
         hosts += hlist
528
      return switch, switches, hosts
529
   def makeNet( self, controller ):
530
      root, switches, hosts = self.treeNet( controller,
531
         self.depth, self.fanout, self.kernel)
532
      return switches, hosts
533
   
534
# Grid network
535

    
536
class GridNet( Network ):
537
   "An N x M grid/mesh network of switches, with hosts at the edges."
538
   def __init__( self, n, m, kernel=True, linear=False ):
539
      self.n, self.m, self.linear = n, m, linear and m == 1
540
      Network.__init__( self, kernel )
541
   def makeNet( self, controller ):
542
      snames, hnames, dpnames = defaultNames()
543
      n, m = self.n, self.m
544
      hosts = []
545
      switches = []
546
      kernel = self.kernel
547
      rows = []
548
      if not self.linear:
549
         print "*** gridNet: creating", n, "x", m, "grid of switches" ; flush()
550
      for y in range( 0, m ):
551
         row = []
552
         for x in range( 0, n ):
553
            dp = dpnames.next() if kernel else None
554
            switch = Switch( snames.next(), dp )
555
            if not kernel: createLink( switch, controller )
556
            row.append( switch )
557
            switches += [ switch ]
558
            print switch.name, ; flush()
559
         rows += [ row ]
560
      # Hook up rows
561
      for row in rows:
562
         previous = None
563
         for switch in row:
564
            if previous is not None:
565
               createLink( switch, previous )
566
            previous = switch
567
         h1, h2 = Host( hnames.next() ), Host( hnames.next() )
568
         createLink( h1, row[ 0 ] )
569
         createLink( h2, row[ -1 ] )
570
         hosts += [ h1, h2 ]
571
         print h1.name, h2.name, ; flush()
572
      # Return here if we're using this to make a linear network
573
      if self.linear: return switches, hosts
574
      # Hook up columns
575
      for x in range( 0, n ):
576
         previous = None
577
         for y in range( 0, m ):
578
            switch = rows[ y ][ x ]
579
            if previous is not None:
580
               createLink( switch, previous )
581
            previous = switch
582
         h1, h2 = Host( hnames.next() ), Host( hnames.next() )
583
         createLink( h1, rows[ 0 ][ x ] )
584
         createLink( h2, rows[ -1 ][ x ] )
585
         hosts += [ h1, h2 ]
586
         print h1.name, h2.name, ; flush()
587
      return switches, hosts
588

    
589
class LinearNet( GridNet ):
590
   def __init__( self, switchCount, kernel=True ):
591
      self.switchCount = switchCount
592
      GridNet.__init__( self, switchCount, 1, kernel, linear=True )
593
      
594
# Tests
595

    
596
def parsePing( pingOutput ):
597
   "Parse ping output and return packets sent, received."
598
   r = r'(\d+) packets transmitted, (\d+) received'
599
   m = re.search( r, pingOutput )
600
   if m == None:
601
      print "*** Error: could not parse ping output:", pingOutput
602
      exit( 1 )
603
   sent, received  = int( m.group( 1 ) ), int( m.group( 2 ) )
604
   return sent, received
605
   
606
def pingTest( controllers=[], switches=[], hosts=[], verbose=False ):
607
   "Test that each host can reach every other host."
608
   packets = 0 ; lost = 0
609
   for node in hosts:
610
      if verbose: 
611
         print node.name, "->", ; flush()
612
      for dest in hosts: 
613
         if node != dest:
614
            result = node.cmd( 'ping -c1 ' + dest.IP() )
615
            sent, received = parsePing( result )
616
            packets += sent
617
            if received > sent:
618
               print "*** Error: received too many packets"
619
               print result
620
               node.cmdPrint( 'route' )
621
               exit( 1 )
622
            lost += sent - received
623
            if verbose: 
624
               print ( dest.name if received else "X" ), ; flush()
625
      if verbose: print
626
   ploss = 100 * lost/packets
627
   if verbose:
628
      print "%d%% packet loss (%d/%d lost)" % ( ploss, lost, packets )
629
      flush()
630
   return ploss
631

    
632
def pingTestVerbose( controllers, switches, hosts ):
633
   return "%d %% packet loss" % \
634
      pingTest( controllers, switches, hosts, verbose=True )
635
 
636
def parseIperf( iperfOutput ):
637
   "Parse iperf output and return bandwidth."
638
   r = r'([\d\.]+ \w+/sec)'
639
   m = re.search( r, iperfOutput )
640
   return m.group( 1 ) if m is not None else "could not parse iperf output"
641
    
642
def iperf( hosts, verbose=False ):
643
   "Run iperf between two hosts."
644
   assert len( hosts ) == 2
645
   host1, host2 = hosts[ 0 ], hosts[ 1 ]
646
   # dumpNodes( [ host1, host2 ] )
647
   host1.cmd( 'killall -9 iperf') # XXX shouldn't be global killall
648
   server = host1.cmd( 'iperf -s &' )
649
   if verbose: print server ; flush()
650
   client = host2.cmd( 'iperf -t 5 -c ' + host1.IP() )
651
   if verbose: print client ; flush()
652
   server = host1.cmd( 'kill -9 %iperf' )
653
   if verbose: print server; flush()
654
   return [ parseIperf( server ), parseIperf( client ) ]
655
   
656
def iperfTest( controllers, switches, hosts, verbose=False ):
657
   "Simple iperf test between two hosts."
658
   if verbose: print "*** Starting ping test"   
659
   h0, hN = hosts[ 0 ], hosts[ -1 ]
660
   print "*** iperfTest: Testing bandwidth between", 
661
   print h0.name, "and", hN.name
662
   result = iperf( [ h0, hN], verbose )
663
   print "*** result:", result
664
   return result
665

    
666
# Simple CLI
667

    
668
class Cli( object ):
669
   "Simple command-line interface to talk to nodes."
670
   cmds = [ '?', 'help', 'nodes', 'sh', 'pingtest', 'iperf', 'net', 'exit' ]
671
   def __init__( self, controllers, switches, hosts ):
672
      self.controllers = controllers
673
      self.switches = switches
674
      self.hosts = hosts
675
      self.nodemap = {}
676
      self.nodelist = controllers + switches + hosts
677
      for node in self.nodelist:
678
         self.nodemap[ node.name ] = node
679
      self.run()
680
   # Commands
681
   def help( self, args ):
682
      "Semi-useful help for CLI"
683
      print "available commands are:", self.cmds
684
   def nodes( self, args ):
685
      "List available nodes"
686
      print "available nodes are:", [ node.name for node in self.nodelist]
687
   def sh( self, args ):
688
      "Run an external shell command"
689
      call( [ 'sh', '-c' ] + args )
690
   def pingtest( self, args ):
691
      pingTest( self.controllers, self.switches, self.hosts, verbose=True )
692
   def net( self, args ):
693
      for switch in self.switches:
694
         print switch.name, "<->",
695
         for intf in switch.intfs:
696
            node, rintf = switch.connection[ intf ]
697
            print node.name,
698
         print
699
   def iperf( self, args ):
700
      print "iperf: got args", args
701
      if len( args ) != 2:
702
         print "usage: iperf <h1> <h2>"
703
         return
704
      for host in args:
705
         if host not in self.nodemap:
706
            print "iperf: cannot find host:", host
707
            return
708
      iperf( [ self.nodemap[ h ] for h in args ] )
709
   # Interpreter
710
   def run( self ):
711
      "Read and execute commands."
712
      print "*** cli: starting"
713
      while True:
714
         print "mininet> ", ; flush()
715
         input = sys.stdin.readline()
716
         if input == '': break
717
         if input[ -1 ] == '\n': input = input[ : -1 ]
718
         cmd = input.split( ' ' )
719
         first = cmd[ 0 ]
720
         rest = cmd[ 1: ]
721
         if first in self.cmds and hasattr( self, first ):
722
            getattr( self, first )( rest )
723
         elif first in self.nodemap and rest != []:
724
            node = self.nodemap[ first ]
725
            # Substitute IP addresses for node names in command
726
            rest = [ self.nodemap[ arg ].IP() if arg in self.nodemap else arg
727
               for arg in rest ]
728
            rest = ' '.join( rest )
729
            # Interactive commands don't work yet, and
730
            # there are still issues with control-c
731
            print "***", node.name, ": running", rest
732
            node.sendCmd( rest )
733
            while True:
734
               try:
735
                  done, data = node.monitor()
736
                  print data,
737
                  if done: break
738
               except KeyboardInterrupt: node.sendInt()
739
            print
740
         elif first == '': pass
741
         elif first in [ 'exit', 'quit' ]: break
742
         elif first == '?': self.help( rest )
743
         else: print "cli: unknown node or command: <", first, ">"
744
      print "*** cli: exiting"
745
   
746
def fixLimits():
747
   "Fix ridiculously small resource limits."
748
   setrlimit( RLIMIT_NPROC, ( 4096, 8192 ) )
749
   setrlimit( RLIMIT_NOFILE, ( 16384, 32768 ) )
750

    
751
def init():
752
   "Initialize Mininet."
753
   # Note: this script must be run as root 
754
   # Perhaps we should do so automatically!
755
   if os.getuid() != 0: 
756
      print "*** Mininet must run as root."; exit( 1 )
757
   fixLimits()
758

    
759
if __name__ == '__main__':
760
   init()
761
   results = {}
762
   exit( 1 )
763
   print "*** Testing Mininet with kernel and user datapath"
764
   for datapath in [ 'kernel', 'user' ]:
765
      k = datapath == 'kernel'
766
      # results += [ TreeNet( depth=2, fanout=2, kernel=k ).
767
      #   run( pingTestVerbose ) ]
768
      results[ datapath ] = []
769
      for switchCount in range( 1, 4 ):
770
         results[ datapath ]  += [ ( switchCount,
771
            LinearNet( switchCount, k).run( iperfTest ) ) ]
772
      # GridNet( 2, 2 ).run( Cli )
773
   print "*** Test results:", results