Statistics
| Branch: | Tag: | Revision:

mininet / mininet / net.py @ 89bf3103

History | View | Annotate | Download (20.5 KB)

1
#!/usr/bin/python
2

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

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

9
Simulated hosts are created as processes in separate network
10
namespaces. This allows a complete OpenFlow network to be simulated on
11
top of a single Linux kernel.
12

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

21
This version supports both the kernel and user space datapaths
22
from the OpenFlow reference implementation.
23

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

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

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

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

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

46
Thoughts/TBD:
47

48
   It should be straightforward to add a function to read
49
   OpenFlowVMS spec files, but I haven't done so yet.
50
   For the moment, specifying configurations and tests in Python
51
   is straightforward and relatively concise.
52
   Soon, we may want to split the various subsystems (core,
53
   topology/network, cli, tests, etc.) into multiple modules.
54
   Currently nox support is in nox.py.
55
   We'd like to support OpenVSwitch as well as the reference
56
   implementation.
57
   
58
Bob Lantz
59
rlantz@cs.stanford.edu
60

61
History:
62
11/19/09 Initial revision (user datapath only)
63
11/19/09 Mininet demo at OpenFlow SWAI meeting
64
12/08/09 Kernel datapath support complete
65
12/09/09 Moved controller and switch routines into classes
66
12/12/09 Added subdivided network driver workflow
67
12/13/09 Added support for custom controller and switch classes
68
"""
69

    
70
from subprocess import 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
from mininet.logging_mod import lg, set_loglevel
77
from mininet.node import Node, Host, Controller, Switch
78
from mininet.util import run, checkRun, quietRun, makeIntfPair, moveIntf
79
from mininet.util import createLink
80

    
81
DATAPATHS = ['user', 'kernel']
82

    
83
# Handy utilities
84
     
85
def dumpNodes( nodes ):
86
   "Dump ifconfig of each node."
87
   for node in nodes:
88
      lg.info("*** Dumping node %s\n" % node.name)
89
      lg.info("%s\n" % node.cmd( 'ip link show' ))
90
      lg.info("%s\n" % node.cmd( 'route' ))
91

    
92
def ipGen( A, B, c, d ):
93
   "Generate next IP class B IP address, starting at A.B.c.d"
94
   while True:
95
      yield '%d.%d.%d.%d' % ( A, B, c, d )
96
      d += 1
97
      if d > 254:
98
         d = 1
99
         c += 1
100
         if c > 254: break
101

    
102
def nameGen( prefix ):
103
   "Generate names starting with prefix."
104
   i = 0
105
   while True: yield prefix + `i`; i += 1
106
      
107
# Control network support:
108
#
109
# Create an explicit control network. Currently this is only
110
# used by the user datapath configuration.
111
#
112
# Notes:
113
#
114
# 1. If the controller and switches are in the same (e.g. root)
115
#    namespace, they can just use the loopback connection.
116
#    We may wish to do this for the user datapath as well as the
117
#    kernel datapath.
118
#
119
# 2. If we can get unix domain sockets to work, we can use them
120
#    instead of an explicit control network.
121
#
122
# 3. Instead of routing, we could bridge or use "in-band" control.
123
#
124
# 4. Even if we dispense with this in general, it could still be
125
#    useful for people who wish to simulate a separate control
126
#    network (since real networks may need one!)
127

    
128
def configureRoutedControlNetwork( controller, switches, ips):
129
   """Configure a routed control network on controller and switches,
130
      for use with the user datapath."""
131
   cip = ips.next()
132
   lg.info("%s <-> " % controller.name)
133
   for switch in switches:
134
      lg.info("%s " % switch.name)
135
      sip = ips.next()
136
      sintf = switch.intfs[ 0 ]
137
      node, cintf = switch.connection[ sintf ]
138
      if node != controller:
139
         lg.error("*** Error: switch %s not connected to correct controller" %
140
                  switch.name)
141
         exit( 1 )
142
      controller.setIP( cintf, cip,  '/24' )
143
      switch.setIP( sintf, sip, '/24' )
144
      controller.setHostRoute( sip, cintf )
145
      switch.setHostRoute( cip, sintf )
146
   lg.info("\n")
147
   lg.info("*** Testing control network\n")
148
   while not controller.intfIsUp():
149
      lg.info("*** Waiting for %s to come up\n", controller.intfs[ 0 ])
150
      sleep( 1 )
151
   for switch in switches:
152
      while not switch.intfIsUp():
153
         lg.info("*** Waiting for %s to come up\n" % switch.intfs[ 0 ])
154
         sleep( 1 )
155
      if pingTest( hosts=[ switch, controller ] ) != 0:
156
         lg.error("*** Error: control network test failed\n")
157
         exit( 1 )
158

    
159
def configHosts( hosts, ips ):
160
   """Configure a set of hosts, starting at IP address a.b.c.d"""
161
   for host in hosts:
162
      hintf = host.intfs[ 0 ]
163
      host.setIP( hintf, ips.next(), '/24' )
164
      host.setDefaultRoute( hintf )
165
      # You're low priority, dude!
166
      quietRun( 'renice +18 -p ' + `host.pid` )
167
      lg.info("%s ", host.name)
168
   lg.info("\n")
169

    
170
# Test driver and topologies
171

    
172
class Network( object ):
173
   """Network topology (and test driver) base class."""
174
   def __init__( self,
175
      kernel=True, 
176
      Controller=Controller, Switch=Switch, 
177
      hostIpGen=ipGen, hostIpStart=( 192, 168, 123, 1 ) ):
178
      self.kernel = kernel
179
      self.Controller = Controller
180
      self.Switch = Switch
181
      self.hostIps = apply( hostIpGen, hostIpStart )
182
      # Check for kernel modules
183
      modules = quietRun( 'lsmod' )
184
      if not kernel and 'tun' not in modules:
185
         lg.error("*** Error: kernel module tun not loaded:\n")
186
         lg.error(" user datapath not supported\n")
187
         exit( 1 )
188
      if kernel and 'ofdatapath' not in modules:
189
         lg.error("*** Error: kernel module ofdatapath not loaded:\n")
190
         lg.error(" kernel datapath not supported\n")
191
         exit( 1 )
192
      # Create network, but don't start things up yet!
193
      self.prepareNet()
194
   def configureControlNetwork( self,
195
      ipGen=ipGen, ipStart = (10, 0, 123, 1 ) ):
196
      ips = apply( ipGen, ipStart )
197
      configureRoutedControlNetwork( self.controllers[ 0 ],
198
         self.switches, ips = ips)
199
   def configHosts( self ):
200
      configHosts( self.hosts, self.hostIps )
201
   def prepareNet( self ):
202
      """Create a network by calling makeNet as follows: 
203
         (switches, hosts ) = makeNet()
204
         Create a controller here as well."""
205
      kernel = self.kernel
206
      if kernel:
207
          lg.info("*** Using kernel datapath\n")
208
      else:
209
          lg.info("*** Using user datapath\n")
210
      lg.info("*** Creating controller\n")
211
      self.controller = self.Controller( 'c0', kernel=kernel )
212
      self.controllers = [ self.controller ]
213
      lg.info("*** Creating network\n")
214
      self.switches, self.hosts = self.makeNet( self.controller )
215
      lg.info("\n")
216
      if not kernel:
217
         lg.info("*** Configuring control network\n")
218
         self.configureControlNetwork()
219
      lg.info("*** Configuring hosts\n")
220
      self.configHosts()
221
   def start( self ):
222
      """Start controller and switches\n"""
223
      lg.info("*** Starting controller\n")
224
      for controller in self.controllers:
225
         controller.start()
226
      lg.info("*** Starting %s switches" % len(self.switches))
227
      for switch in self.switches:
228
         switch.start( self.controllers[ 0 ] )
229
      lg.info("\n")
230
   def stop( self ):
231
      """Stop the controller(s), switches and hosts\n"""
232
      lg.info("*** Stopping hosts\n")
233
      for host in self.hosts: 
234
         host.terminate()
235
      lg.info("*** Stopping switches\n")
236
      for switch in self.switches:
237
         lg.info("%s" % switch.name)
238
         switch.stop()
239
      lg.info("\n")
240
      lg.info("*** Stopping controller\n")
241
      for controller in self.controllers:
242
         controller.stop();
243
      lg.info("*** Test complete\n")
244
   def runTest( self, test ):
245
      """Run a given test, called as test( controllers, switches, hosts)"""
246
      return test( self.controllers, self.switches, self.hosts )
247
   def run( self, test ):
248
      """Perform a complete start/test/stop cycle; test is of the form
249
         test( controllers, switches, hosts )"""
250
      self.start()
251
      lg.info("*** Running test\n")
252
      result = self.runTest( test )
253
      self.stop()
254
      return result
255
   def interact( self ):
256
      "Create a network and run our simple CLI."
257
      self.run( self, Cli )
258
   
259
def defaultNames( snames=None, hnames=None, dpnames=None ):
260
   "Reinitialize default names from generators, if necessary."
261
   if snames is None: snames = nameGen( 's' )
262
   if hnames is None: hnames = nameGen( 'h' )
263
   if dpnames is None: dpnames = nameGen( 'nl:' )
264
   return snames, hnames, dpnames
265

    
266
# Tree network
267

    
268
class TreeNet( Network ):
269
   "A tree-structured network with the specified depth and fanout"
270
   def __init__( self, depth, fanout, **kwargs):
271
      self.depth, self.fanout = depth, fanout
272
      Network.__init__( self, **kwargs )
273
   def treeNet( self, controller, depth, fanout, snames=None,
274
      hnames=None, dpnames=None ):
275
      """Return a tree network of the given depth and fanout as a triple:
276
         ( root, switches, hosts ), using the given switch, host and
277
         datapath name generators, with the switches connected to the given
278
         controller. If kernel=True, use the kernel datapath; otherwise the
279
         user datapath will be used."""
280
      # Ugly, but necessary (?) since defaults are only evaluated once
281
      snames, hnames, dpnames = defaultNames( snames, hnames, dpnames )
282
      if ( depth == 0 ):
283
         host = Host( hnames.next() )
284
         lg.info("%s " % host.name)
285
         return host, [], [ host ]
286
      dp = dpnames.next() if self.kernel else None
287
      switch = Switch( snames.next(), dp )
288
      if not self.kernel: createLink( switch, controller )
289
      lg.info("%s " % switch.name)
290
      switches, hosts = [ switch ], []
291
      for i in range( 0, fanout ):
292
         child, slist, hlist = self.treeNet( controller, 
293
            depth - 1, fanout, snames, hnames, dpnames )
294
         createLink( switch, child )
295
         switches += slist
296
         hosts += hlist
297
      return switch, switches, hosts
298
   def makeNet( self, controller ):
299
      root, switches, hosts = self.treeNet( controller,
300
         self.depth, self.fanout )
301
      return switches, hosts
302
   
303
# Grid network
304

    
305
class GridNet( Network ):
306
   """An N x M grid/mesh network of switches, with hosts at the edges.
307
      This class also demonstrates creating a somewhat complicated
308
      topology."""
309
   def __init__( self, n, m, linear=False, **kwargs ):
310
      self.n, self.m, self.linear = n, m, linear and m == 1
311
      Network.__init__( self, **kwargs )
312
   def makeNet( self, controller ):
313
      snames, hnames, dpnames = defaultNames()
314
      n, m = self.n, self.m
315
      hosts = []
316
      switches = []
317
      kernel = self.kernel
318
      rows = []
319
      if not self.linear:
320
         lg.info("*** gridNet: creating", n, "x", m, "grid of switches")
321
      for y in range( 0, m ):
322
         row = []
323
         for x in range( 0, n ):
324
            dp = dpnames.next() if kernel else None
325
            switch = Switch( snames.next(), dp )
326
            if not kernel: createLink( switch, controller )
327
            row.append( switch )
328
            switches += [ switch ]
329
            lg.info("%s " % switch.name)
330
         rows += [ row ]
331
      # Hook up rows
332
      for row in rows:
333
         previous = None
334
         for switch in row:
335
            if previous is not None:
336
               createLink( switch, previous )
337
            previous = switch
338
         h1, h2 = Host( hnames.next() ), Host( hnames.next() )
339
         createLink( h1, row[ 0 ] )
340
         createLink( h2, row[ -1 ] )
341
         hosts += [ h1, h2 ]
342
         lg.info("%s %s" % (h1.name, h2.name))
343
      # Return here if we're using this to make a linear network
344
      if self.linear: return switches, hosts
345
      # Hook up columns
346
      for x in range( 0, n ):
347
         previous = None
348
         for y in range( 0, m ):
349
            switch = rows[ y ][ x ]
350
            if previous is not None:
351
               createLink( switch, previous )
352
            previous = switch
353
         h1, h2 = Host( hnames.next() ), Host( hnames.next() )
354
         createLink( h1, rows[ 0 ][ x ] )
355
         createLink( h2, rows[ -1 ][ x ] )
356
         hosts += [ h1, h2 ]
357
         lg.info("%s %s" % (h1.name, h2.name))
358
      return switches, hosts
359

    
360
class LinearNet( GridNet ):
361
   "A network consisting of two hosts connected by a string of switches."
362
   def __init__( self, switchCount, **kwargs ):
363
      self.switchCount = switchCount
364
      GridNet.__init__( self, switchCount, 1, linear=True, **kwargs )
365
      
366
# Tests
367

    
368
def parsePing( pingOutput ):
369
   "Parse ping output and return packets sent, received."
370
   r = r'(\d+) packets transmitted, (\d+) received'
371
   m = re.search( r, pingOutput )
372
   if m == None:
373
      lg.error("*** Error: could not parse ping output: %s\n" % pingOutput)
374
      exit( 1 )
375
   sent, received  = int( m.group( 1 ) ), int( m.group( 2 ) )
376
   return sent, received
377
   
378
def pingTest( controllers=[], switches=[], hosts=[], verbose=False ):
379
   "Test that each host can reach every other host."
380
   packets = 0 ; lost = 0
381
   for node in hosts:
382
      if verbose:
383
         lg.info("%s -> " % node.name)
384
      for dest in hosts: 
385
         if node != dest:
386
            result = node.cmd( 'ping -c1 ' + dest.IP() )
387
            sent, received = parsePing( result )
388
            packets += sent
389
            if received > sent:
390
               lg.error("*** Error: received too many packets")
391
               lg.error("%s" % result)
392
               node.cmdPrint( 'route' )
393
               exit( 1 )
394
            lost += sent - received
395
            if verbose: 
396
               lg.info(("%s " % dest.name) if received else "X ")
397
      if verbose:
398
          lg.info("\n")
399
   ploss = 100 * lost/packets
400
   if verbose:
401
      lg.info("%d%% packet loss (%d/%d lost)\n" % ( ploss, lost, packets ))
402
      flush()
403
   return ploss
404

    
405
def pingTestVerbose( controllers, switches, hosts ):
406
   return "%d %% packet loss" % \
407
      pingTest( controllers, switches, hosts, verbose=True )
408
 
409
def parseIperf( iperfOutput ):
410
   "Parse iperf output and return bandwidth."
411
   r = r'([\d\.]+ \w+/sec)'
412
   m = re.search( r, iperfOutput )
413
   return m.group( 1 ) if m is not None else "could not parse iperf output"
414
    
415
def iperf( hosts, verbose=False ):
416
   "Run iperf between two hosts."
417
   assert len( hosts ) == 2
418
   host1, host2 = hosts[ 0 ], hosts[ 1 ]
419
   host1.cmd( 'killall -9 iperf') # XXX shouldn't be global killall
420
   server = host1.cmd( 'iperf -s &' )
421
   if verbose:
422
       lg.info("%s" % server)
423
   client = host2.cmd( 'iperf -t 5 -c ' + host1.IP() )
424
   if verbose:
425
       lg.info("%s" % client)
426
   server = host1.cmd( 'kill -9 %iperf' )
427
   if verbose:
428
       lg.info("%s" % server)
429
   return [ parseIperf( server ), parseIperf( client ) ]
430
   
431
def iperfTest( controllers, switches, hosts, verbose=False ):
432
   "Simple iperf test between two hosts."
433
   if verbose: 
434
       lg.info("*** Starting ping test\n")
435
   h0, hN = hosts[ 0 ], hosts[ -1 ]
436
   lg.info("*** iperfTest: Testing bandwidth between")
437
   lg.info("%s and %s\n" % (h0.name, hN.name))
438
   result = iperf( [ h0, hN], verbose )
439
   lg.info("*** result: %s\n" % result)
440
   return result
441

    
442
# Simple CLI
443

    
444
class Cli( object ):
445
   "Simple command-line interface to talk to nodes."
446
   cmds = [ '?', 'help', 'nodes', 'sh', 'pingtest', 'iperf', 'net', 'exit' ]
447
   def __init__( self, controllers, switches, hosts ):
448
      self.controllers = controllers
449
      self.switches = switches
450
      self.hosts = hosts
451
      self.nodemap = {}
452
      self.nodelist = controllers + switches + hosts
453
      for node in self.nodelist:
454
         self.nodemap[ node.name ] = node
455
      self.run()
456
   # Commands
457
   def help( self, args ):
458
      "Semi-useful help for CLI"
459
      help_str = "Available commands are:" + str(self.cmds) + "\n" + \
460
                 "You may also send a command to a node using:" + \
461
                 "  <node> command {args}" + \
462
                 "For example:" + \
463
                 "  mininet> h0 ifconfig" + \
464
                 "\n" + \
465
                 "The interpreter automatically substitutes IP addresses" + \
466
                 "for node names, so commands like" + \
467
                 "  mininet> h0 ping -c1 h1" + \
468
                 "should work." + \
469
                 "\n" + \
470
                 "Interactive commands are not really supported yet," + \
471
                 "so please limit commands to ones that do not" + \
472
                 "require user interaction and will terminate" + \
473
                 "after a reasonable amount of time."
474
      print(help_str)
475

    
476
   def nodes( self, args ):
477
      "List available nodes"
478
      lg.info("available nodes are:\n", [ node.name for node in self.nodelist])
479
   def sh( self, args ):
480
      "Run an external shell command"
481
      call( [ 'sh', '-c' ] + args )
482
   def pingtest( self, args ):
483
      pingTest( self.controllers, self.switches, self.hosts, verbose=True )
484
   def net( self, args ):
485
      for switch in self.switches:
486
         lg.info("%s <-> ", switch.name)
487
         for intf in switch.intfs:
488
            node, remoteIntf = switch.connection[ intf ]
489
            lg.info("%s" % node.name)
490
   def iperf( self, args ):
491
      if len( args ) != 2:
492
         lg.error("usage: iperf <h1> <h2>")
493
         return
494
      for host in args:
495
         if host not in self.nodemap:
496
            lg.error("iperf: cannot find host: %s" % host)
497
            return
498
      iperf( [ self.nodemap[ h ] for h in args ], verbose=True )
499
   # Interpreter
500
   def run( self ):
501
      "Read and execute commands."
502
      lg.info("*** cli: starting\n")
503
      while True:
504
         lg.info("mininet> ")
505
         input = sys.stdin.readline()
506
         if input == '': break
507
         if input[ -1 ] == '\n': input = input[ : -1 ]
508
         cmd = input.split( ' ' )
509
         first = cmd[ 0 ]
510
         rest = cmd[ 1: ]
511
         if first in self.cmds and hasattr( self, first ):
512
            getattr( self, first )( rest )
513
         elif first in self.nodemap and rest != []:
514
            node = self.nodemap[ first ]
515
            # Substitute IP addresses for node names in command
516
            rest = [ self.nodemap[ arg ].IP() if arg in self.nodemap else arg
517
               for arg in rest ]
518
            rest = ' '.join( rest )
519
            # Interactive commands don't work yet, and
520
            # there are still issues with control-c
521
            lg.error("*** %s: running %s\n" % (node.name, rest))
522
            node.sendCmd( rest )
523
            while True:
524
               try:
525
                  done, data = node.monitor()
526
                  lg.info("%s\n" % data)
527
                  if done: break
528
               except KeyboardInterrupt: node.sendInt()
529
         elif first == '': pass
530
         elif first in [ 'exit', 'quit' ]: break
531
         elif first == '?': self.help( rest )
532
         else:
533
            lg.error("cli: unknown node or command: < %s >\n" % first)
534
      lg.info("*** cli: exiting\n")
535
   
536
def fixLimits():
537
   "Fix ridiculously small resource limits."
538
   setrlimit( RLIMIT_NPROC, ( 4096, 8192 ) )
539
   setrlimit( RLIMIT_NOFILE, ( 16384, 32768 ) )
540

    
541
def init():
542
   "Initialize Mininet."
543
   if os.getuid() != 0: 
544
      # Note: this script must be run as root 
545
      # Perhaps we should do so automatically!
546
      print "*** Mininet must run as root."; exit( 1 )
547
   # If which produces no output, then netns is not in the path.
548
   # May want to loosen this to handle netns in the current dir.
549
   if not quietRun( [ 'which', 'netns' ] ):
550
       raise Exception( "Could not find netns; see INSTALL" )
551
   fixLimits()
552

    
553
if __name__ == '__main__':
554
   if len(sys.argv) > 1:
555
      set_loglevel(sys.argv[1])
556
   else:
557
      set_loglevel('info')
558

    
559
   init()
560
   results = {}
561
   lg.info("*** Welcome to Mininet!\n")
562
   lg.info("*** Look in examples/ for more examples\n\n")
563
   lg.info("*** Testing Mininet with kernel and user datapath\n")
564
   for datapath in [ 'kernel', 'user' ]:
565
      k = datapath == 'kernel'
566
      network = TreeNet( depth=2, fanout=4, kernel=k)
567
      result = network.run( pingTestVerbose )
568
      results[ datapath ] = result
569
   lg.info("*** Test results: %s\n", results)