Organized routines into classes:
Node -> { Host, Switch, Controller }
Network -> { TreeNet, GridNet -> LinearNet }
Modified cleanup to clean up kernel datapaths.
This commit is contained in:
@@ -11,6 +11,10 @@ done
|
||||
echo "Removing excess controllers/ofprotocols/ofdatapaths/pings"
|
||||
killall -9 controller ofprotocol ofdatapath ping 2> /dev/null
|
||||
|
||||
echo "Removing excess kernel datapath processes"
|
||||
ps ax | grep 'dp[0-9]' | awk '{print $1;}' | xargs kill
|
||||
|
||||
echo "Removing vconn junk in /tmp"
|
||||
rm -f /tmp/vconn* /tmp/vlogs* /tmp/*.out /tmp/*.log
|
||||
|
||||
|
||||
|
||||
+443
-331
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
Mininet: A simple networking testbed for OpenFlow!
|
||||
|
||||
Mininet creates a simple test network for OpenFlow by using
|
||||
Mininet creates simple OpenFlow test networks by using
|
||||
process-based virtualization and network namespaces.
|
||||
|
||||
This file supports use of either the kernel or user space datapath
|
||||
@@ -12,35 +11,34 @@ from the OpenFlow reference implementation. Up to 32 switches are
|
||||
supported using the kernel datapath, and 512 (or more) switches are
|
||||
supported via the user datapath.
|
||||
|
||||
Simulated hosts are created as processes in
|
||||
separate network namespaces. This allows a complete OpenFlow
|
||||
network to be simulated on top of a single Linux kernel.
|
||||
Simulated hosts are created as processes in separate network
|
||||
namespaces. This allows a complete OpenFlow network to be simulated on
|
||||
top of a single Linux kernel.
|
||||
|
||||
Each host has:
|
||||
A virtual console (pipes to a shell)
|
||||
A virtual interfaces (half of a veth pair)
|
||||
A namespaced parent shell (and possibly some child processes)
|
||||
A parent shell (and possibly some child processes) in a namespace
|
||||
|
||||
Hosts have a network interface which is
|
||||
configured via ifconfig/ip link/etc. with data network IP
|
||||
addresses (e.g. 192.168.123.2 )
|
||||
Hosts have a network interface which is configured via ifconfig/ip
|
||||
link/etc. with data network IP addresses (e.g. 192.168.123.2 )
|
||||
|
||||
In kernel datapath mode, the controller and switches are simply
|
||||
processes in the root namespace.
|
||||
|
||||
Kernel OpenFlow datapaths are instantiated using dpctl, and are
|
||||
attached to the one side of a veth pair; the other side resides in
|
||||
the host namespace. In this mode, switch processes can simply
|
||||
connect to the controller via the loopback interface.
|
||||
Kernel OpenFlow datapaths are instantiated using dpctl(8), and are attached
|
||||
to the one side of a veth pair; the other side resides in the host
|
||||
namespace. In this mode, switch processes can simply connect to the
|
||||
controller via the loopback interface.
|
||||
|
||||
In user datapath mode, the controller and switch are full-service
|
||||
nodes that live in their own network namespace and have management
|
||||
In user datapath mode, the controller and switches are full-service
|
||||
nodes that live in their own network namespaces and have management
|
||||
interfaces and IP addresses on a control network (e.g. 10.0.123.1,
|
||||
currently routed although it could be bridged.)
|
||||
|
||||
In addition to a management interface, user mode switches also have
|
||||
several switch interfaces, halves of veth pairs whose other halves
|
||||
reside in the host nodes that the switches are connected to.
|
||||
several switch interfaces, halves of veth pairs whose other halves reside
|
||||
in the host nodes that the switches are connected to.
|
||||
|
||||
Naming:
|
||||
Host nodes are named h1-hN
|
||||
@@ -65,16 +63,13 @@ rlantz@cs.stanford.edu
|
||||
|
||||
History:
|
||||
11/19/09 Initial revision (user datapath only)
|
||||
12/8/08 Kernel datapath support complete
|
||||
|
||||
12/08/09 Kernel datapath support complete
|
||||
12/09/09 Moved controller and switch routines into classes
|
||||
"""
|
||||
|
||||
# Note: this script must be run as root
|
||||
# Perhaps we should do so automatically!
|
||||
|
||||
from subprocess import call, check_call, Popen, PIPE, STDOUT
|
||||
from time import sleep
|
||||
import re, os, signal, sys, select
|
||||
import os, re, signal, sys, select
|
||||
flush = sys.stdout.flush
|
||||
from resource import setrlimit, RLIMIT_NPROC, RLIMIT_NOFILE
|
||||
|
||||
@@ -106,11 +101,6 @@ def quietRun( cmd ):
|
||||
if popen.returncode != None: break
|
||||
return output
|
||||
|
||||
# Command paths
|
||||
netns = "/usr/local/bin/netns"
|
||||
bash = "/bin/bash"
|
||||
ifconfig = "/sbin/ifconfig"
|
||||
|
||||
class Node( object ):
|
||||
"""A virtual network node is simply a shell in a network namespace.
|
||||
We communicate with it using pipes."""
|
||||
@@ -118,9 +108,9 @@ class Node( object ):
|
||||
self.name = name
|
||||
closeFds = False # speed vs. memory use
|
||||
# xpg_echo is needed so we can echo our sentinel in sendCmd
|
||||
cmd = [ bash, '-O', 'xpg_echo' ]
|
||||
cmd = [ '/bin/bash', '-O', 'xpg_echo' ]
|
||||
self.inNamespace = inNamespace
|
||||
if self.inNamespace: cmd = [ netns ] + cmd
|
||||
if self.inNamespace: cmd = [ 'netns' ] + cmd
|
||||
self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
|
||||
close_fds=closeFds )
|
||||
self.stdin = self.shell.stdin
|
||||
@@ -135,8 +125,7 @@ class Node( object ):
|
||||
self.ips = {}
|
||||
self.connection = {}
|
||||
self.waiting = False
|
||||
# For sanity check, try bringing up loopback interface
|
||||
# self.cmd( "ifconfig lo 127.0.0.1 up" )
|
||||
# Subshell I/O, commands and control
|
||||
def read( self, max ): return os.read( self.stdout.fileno(), max )
|
||||
def write( self, data ): os.write( self.stdin.fileno(), data )
|
||||
def terminate( self ): os.kill( self.pid, signal.SIGKILL )
|
||||
@@ -190,6 +179,7 @@ class Node( object ):
|
||||
result = self.cmd( cmd )
|
||||
print result,
|
||||
return result
|
||||
# Interface management, configuration, and routing
|
||||
def intfName( self, n):
|
||||
"Construct a canonical interface name node-intf for interface N."
|
||||
return self.name + '-eth' + `n`
|
||||
@@ -201,7 +191,7 @@ class Node( object ):
|
||||
return intfName
|
||||
def setIP( self, intf, ip, bits ):
|
||||
"Set an interface's IP address."
|
||||
result = self.cmd( [ ifconfig, intf, ip + bits, 'up' ] )
|
||||
result = self.cmd( [ 'ifconfig', intf, ip + bits, 'up' ] )
|
||||
self.ips[ intf ] = ip
|
||||
return result
|
||||
def setHostRoute( self, ip, intf ):
|
||||
@@ -214,6 +204,10 @@ class Node( object ):
|
||||
def IP( self ):
|
||||
"Return IP address of first interface"
|
||||
return self.ips[ self.intfs[ 0 ] ]
|
||||
def intfIsUp( self, intf ):
|
||||
"Check if one of our interfaces is up."
|
||||
return 'UP' in self.cmd( 'ifconfig ' + self.intfs[ 0 ] )
|
||||
# Other methods
|
||||
def __str__( self ):
|
||||
result = self.name
|
||||
result += ": IP=" + self.IP() + " intfs=" + self.intfs
|
||||
@@ -233,20 +227,93 @@ def nodeFromFile( f ):
|
||||
node = outToNode.get( f )
|
||||
return node or inToNode.get( f )
|
||||
|
||||
def createNodes( name, count ):
|
||||
"Create and return a list of nodes."
|
||||
nodes = [ Node( name + `i` ) for i in range( 0, count ) ]
|
||||
# print "*** CreateNodes: created:", nodes
|
||||
return nodes
|
||||
class Host( Node ):
|
||||
"""A host is simply a Node."""
|
||||
pass
|
||||
|
||||
class Controller( Node ):
|
||||
"""A Controller is a Node that is running (or has execed) an
|
||||
OpenFlow controller."""
|
||||
def __init__( self, name, kernel=True ):
|
||||
Node.__init__( self, name, inNamespace=( not kernel ) )
|
||||
def start( self, cprog='controller', cargs='ptcp:' ):
|
||||
"Start <cprog cargs> on controller, logging to /tmp/cN.log"
|
||||
cout = '/tmp/' + self.name + '.log'
|
||||
self.cmdPrint( cprog + ' ' + cargs +
|
||||
' 1> ' + cout + ' 2> ' + cout + ' &' )
|
||||
def stop( self, cprog='controller' ):
|
||||
"Stop controller cprog on controller"
|
||||
self.cmd( "kill %" + cprog )
|
||||
|
||||
class Switch( Node ):
|
||||
"""A Switch is a Node that is running (or has execed)
|
||||
an OpenFlow switch."""
|
||||
def __init__( self, name, datapath=None ):
|
||||
self.dp = datapath
|
||||
self.execed = False
|
||||
Node.__init__( self, name, inNamespace=( datapath == None ) )
|
||||
def startUserDatapath( self, controller ):
|
||||
"""Start OpenFlow reference user datapath,
|
||||
logging to /tmp/sN-{ofd,ofp}.log"""
|
||||
ofdlog = '/tmp/' + self.name + '-ofd.log'
|
||||
ofplog = '/tmp/' + self.name + '-ofp.log'
|
||||
self.cmd( 'ifconfig lo up' )
|
||||
intfs = self.intfs[ 1 : ] # 0 is mgmt interface
|
||||
self.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) +
|
||||
' ptcp: 1> ' + ofdlog + ' 2> '+ ofdlog + ' &' )
|
||||
self.cmdPrint( 'ofprotocol tcp:' + controller.IP() +
|
||||
' tcp:localhost 1> ' + ofplog + ' 2>' + ofplog + ' &' )
|
||||
def stopUserDatapath( self ):
|
||||
"Stop OpenFlow reference user datapath."
|
||||
self.cmd( "kill %ofdatapath" )
|
||||
self.cmd( "kill %ofprotocol" )
|
||||
def startKernelDatapath( self, controller):
|
||||
"Start up switch using OpenFlow reference kernel datapath."
|
||||
ofplog = '/tmp/' + self.name + '-ofp.log'
|
||||
quietRun( 'ifconfig lo up' )
|
||||
# Delete local datapath if it exists;
|
||||
# then create a new one monitoring the given interfaces
|
||||
quietRun( 'dpctl deldp ' + self.dp )
|
||||
self.cmdPrint( 'dpctl adddp ' + self.dp )
|
||||
self.cmdPrint( 'dpctl addif ' + self.dp + ' ' + ' '.join( self.intfs ) )
|
||||
# Become protocol daemon
|
||||
self.cmdPrint( 'exec ofprotocol' +
|
||||
' ' + self.dp + ' tcp:127.0.0.1 1> ' + ofplog + ' 2>' + ofplog + ' &' )
|
||||
self.execed = True
|
||||
def stopKernelDatapath( self ):
|
||||
"Terminate a switch using OpenFlow reference kernel datapath."
|
||||
quietRun( 'dpctl deldp ' + self.dp )
|
||||
for intf in self.intfs: quietRun( 'ip link del ' + intf )
|
||||
self.terminate()
|
||||
def start( self, controller ):
|
||||
if self.dp is None: self.startUserDatapath( controller )
|
||||
else: self.startKernelDatapath( controller )
|
||||
def stop( self ):
|
||||
if self.dp is None: self.stopUserDatapath()
|
||||
else: self.stopKernelDatapath()
|
||||
# Handle non-interaction if we've execed
|
||||
def sendCmd( self, cmd ):
|
||||
if not self.execed: return Node.sendCmd( self, cmd )
|
||||
else: print "*** Error:", self.name, "has execed and cannot accept commands"
|
||||
def monitor( self ):
|
||||
if not self.execed: return Node.monitor( self )
|
||||
else: return True, ''
|
||||
|
||||
# Interface management
|
||||
#
|
||||
# We connect nodes by creating a pair of veth interfaces,
|
||||
# and then placing them in the pair of nodes that we want
|
||||
# to communicate. Interfaces are named nodeN-ethM
|
||||
# Interfaces are managed as strings which are simply the
|
||||
# interface names, of the form "nodeN-ethM".
|
||||
#
|
||||
# To connect nodes, we create a pair of veth interfaces, and then place them
|
||||
# in the pair of nodes that we want to communicate. We then update the node's
|
||||
# list of interfaces and connectivity map.
|
||||
#
|
||||
# For the kernel datapath, switch interfaces
|
||||
# live in the root namespace and thus do not have to be
|
||||
# explicitly moved.
|
||||
|
||||
def makeIntfPair( intf1, intf2 ):
|
||||
"Make a veth pair of intf1 and intf2"
|
||||
"Make a veth pair of intf1 and intf2."
|
||||
# Delete any old interfaces with the same names
|
||||
quietRun( 'ip link del ' + intf1 )
|
||||
quietRun( 'ip link del ' + intf2 )
|
||||
@@ -255,7 +322,7 @@ def makeIntfPair( intf1, intf2 ):
|
||||
return checkRun( cmd )
|
||||
|
||||
def moveIntf( intf, node ):
|
||||
"Move intf to node"
|
||||
"Move intf to node."
|
||||
cmd = 'ip link set ' + intf + ' netns ' + `node.pid`
|
||||
checkRun( cmd )
|
||||
links = node.cmd( 'ip link show' )
|
||||
@@ -278,8 +345,239 @@ def createLink( node1, node2 ):
|
||||
|
||||
# Handy utilities
|
||||
|
||||
def createNodes( name, count ):
|
||||
"Create and return a list of nodes."
|
||||
nodes = [ Node( name + `i` ) for i in range( 0, count ) ]
|
||||
# print "*** CreateNodes: created:", nodes
|
||||
return nodes
|
||||
|
||||
def dumpNodes( nodes ):
|
||||
"Dump ifconfig of each node."
|
||||
for node in nodes:
|
||||
print "*** Dumping node", node.name
|
||||
print node.cmd( 'ip link show' )
|
||||
print node.cmd( 'route' )
|
||||
|
||||
def ipGen( A, B, c, d ):
|
||||
"Generate next IP class B IP address, starting at A.B.c.d"
|
||||
while True:
|
||||
yield '%d.%d.%d.%d' % ( A, B, c, d )
|
||||
d += 1
|
||||
if d > 254:
|
||||
d = 1
|
||||
c += 1
|
||||
if c > 254: break
|
||||
|
||||
def nameGen( prefix ):
|
||||
"Generate names starting with prefix."
|
||||
i = 0
|
||||
while True: yield prefix + `i`; i += 1
|
||||
|
||||
# Control network support
|
||||
# For the user datapath, we create an explicit control network.
|
||||
# Note: Instead of routing, we could bridge or use "in-band" control
|
||||
|
||||
def configRoutedControlNetwork( controller, switches,
|
||||
startAddr=( 10, 123, 0, 1 ) ):
|
||||
"""Configure a routed control network on controller and switches,
|
||||
for use with the user datapath."""
|
||||
ips = apply( ipGen, startAddr )
|
||||
cip = ips.next()
|
||||
print controller.name, '<->',
|
||||
for switch in switches:
|
||||
print switch.name, ; flush()
|
||||
sip = ips.next()
|
||||
sintf = switch.intfs[ 0 ]
|
||||
node, cintf = switch.connection[ sintf ]
|
||||
if node != controller:
|
||||
print "*** Error: switch", switch.name,
|
||||
print "not connected to correct controller"
|
||||
exit( 1 )
|
||||
controller.setIP( cintf, cip, '/24' )
|
||||
switch.setIP( sintf, sip, '/24' )
|
||||
controller.setHostRoute( sip, cintf )
|
||||
switch.setHostRoute( cip, sintf )
|
||||
print
|
||||
print "*** Testing control network"
|
||||
while not controller.intfIsUp( controller.intfs[ 0 ] ):
|
||||
print "*** Waiting for ", controller.intfs[ 0 ], "to come up"
|
||||
sleep( 1 )
|
||||
for switch in switches:
|
||||
while not switch.intfIsUp( switch.intfs[ 0 ] ):
|
||||
print "*** Waiting for ", switch.intfs[ 0 ], "to come up"
|
||||
sleep( 1 )
|
||||
if pingTest( [ switch, controller ] ) != 0:
|
||||
print "*** Error: control network test failed"
|
||||
exit( 1 )
|
||||
|
||||
def configHosts( hosts, ( a, b, c, d ) ):
|
||||
"Configure a set of hosts, starting at IP address a.b.c.d"
|
||||
ips = ipGen( a, b, c, d )
|
||||
for host in hosts:
|
||||
hintf = host.intfs[ 0 ]
|
||||
host.setIP( hintf, ips.next(), '/24' )
|
||||
host.setDefaultRoute( hintf )
|
||||
# You're low priority, dude!
|
||||
quietRun( 'renice +18 -p ' + `host.pid` )
|
||||
print host.name, ; flush()
|
||||
print
|
||||
|
||||
# Test driver and topologies
|
||||
|
||||
class Network( object ):
|
||||
"Network topology (and test driver) base class."
|
||||
def __init__( self, kernel=True, startAddr=( 192, 168, 123, 1) ):
|
||||
self.kernel, self.startAddr = kernel, startAddr
|
||||
# In progress: we probably want to decouple creating/starting/stopping
|
||||
# the network and running tests, since we might wish to run
|
||||
# multiple tests on the same network. It's not clear if the network
|
||||
# should always be started/stopped for each test or not. Probably
|
||||
# not...
|
||||
def run( self, test ):
|
||||
"""Create a network by calling makeNet as follows:
|
||||
(switches, hosts ) = makeNet()
|
||||
and then run test( controller, switches, hosts ) on it."""
|
||||
kernel = self.kernel
|
||||
if kernel: print "*** Using kernel datapath"
|
||||
else: print "*** Using user datapath"
|
||||
print "*** Creating controller"
|
||||
controller = Controller( 'c0', kernel )
|
||||
print "*** Creating network"
|
||||
switches, hosts = self.makeNet()
|
||||
print
|
||||
if not kernel:
|
||||
print "*** Configuring control network"
|
||||
configRoutedControlNetwork( controller, switches )
|
||||
print "*** Configuring hosts"
|
||||
configHosts( hosts, self.startAddr )
|
||||
print "*** Starting reference controller"
|
||||
controller.start()
|
||||
print "*** Starting switches"
|
||||
for switch in switches:
|
||||
switch.start( controller )
|
||||
print "*** Running test"
|
||||
test( [ controller ], switches, hosts )
|
||||
print "*** Stopping controller"
|
||||
controller.stop()
|
||||
print "*** Stopping switches"
|
||||
for switch in switches:
|
||||
switch.stop()
|
||||
print "*** Test complete"
|
||||
def interact( self ):
|
||||
"Create a network and run our simple CLI."
|
||||
self.run( self, Cli )
|
||||
|
||||
def defaultNames( snames=None, hnames=None, dpnames=None ):
|
||||
"Reinitialize default names from generators, if necessary."
|
||||
if snames is None: snames = nameGen( 's' )
|
||||
if hnames is None: hnames = nameGen( 'h' )
|
||||
if dpnames is None: dpnames = nameGen( 'nl:' )
|
||||
return snames, hnames, dpnames
|
||||
|
||||
# Tree network
|
||||
|
||||
class TreeNet( Network ):
|
||||
"A tree-structured network of the given depth and fanout"
|
||||
def __init__( self, depth, fanout, kernel=True):
|
||||
self.depth, self.fanout = depth, fanout
|
||||
Network.__init__( self, kernel )
|
||||
def treeNet( self, depth, fanout, kernel=True, snames=None,
|
||||
hnames=None, dpnames=None ):
|
||||
"""Return a tree network of the given depth and fanout as a triple:
|
||||
( root, switches, hosts ), using the given switch, host and
|
||||
datapath name generators, with the switches connected to the given
|
||||
controller. If kernel=True, use the kernel datapath; otherwise the
|
||||
user datapath will be used."""
|
||||
# Ugly, but necessary (?) since defaults are only evaluated once
|
||||
snames, hnames, dpnames = defaultNames( snames, hnames, dpnames )
|
||||
if ( depth == 0 ):
|
||||
host = Host( hnames.next() )
|
||||
print host.name, ; flush()
|
||||
return host, [], [ host ]
|
||||
dp = dpnames.next() if kernel else None
|
||||
switch = Switch( snames.next(), dp )
|
||||
if not kernel: createLink( switch, controller )
|
||||
print switch.name, ; flush()
|
||||
switches, hosts = [ switch ], []
|
||||
for i in range( 0, fanout ):
|
||||
child, slist, hlist = self.treeNet(
|
||||
depth - 1, fanout, kernel, snames, hnames, dpnames )
|
||||
createLink( switch, child )
|
||||
switches += slist
|
||||
hosts += hlist
|
||||
return switch, switches, hosts
|
||||
def makeNet( self ):
|
||||
root, switches, hosts = self.treeNet(
|
||||
self.depth, self.fanout, self.kernel)
|
||||
return switches, hosts
|
||||
|
||||
# Grid network
|
||||
|
||||
class GridNet( Network ):
|
||||
"An n x m grid/mesh network of switches, with hosts at the edges."
|
||||
def __init__( self, n, m, kernel=True, linear=False ):
|
||||
self.n, self.m, self.linear = n, m, linear and m == 1
|
||||
print "m=",m
|
||||
Network.__init__( self, kernel )
|
||||
def makeNet( self ):
|
||||
snames, hnames, dpnames = defaultNames()
|
||||
n, m = self.n, self.m
|
||||
hosts = []
|
||||
switches = []
|
||||
kernel = self.kernel
|
||||
rows = []
|
||||
if not self.linear:
|
||||
print "*** gridNet: creating", n, "x", m, "grid of switches" ; flush()
|
||||
for y in range( 0, m ):
|
||||
row = []
|
||||
for x in range( 0, n ):
|
||||
dp = dpnames.next() if kernel else None
|
||||
switch = Switch( snames.next(), dp )
|
||||
if not kernel: createLink( switch, controller )
|
||||
row.append( switch )
|
||||
switches += [ switch ]
|
||||
print switch.name, ; flush()
|
||||
rows += [ row ]
|
||||
# Hook up rows
|
||||
for row in rows:
|
||||
previous = None
|
||||
for switch in row:
|
||||
if previous is not None:
|
||||
createLink( switch, previous )
|
||||
previous = switch
|
||||
h1, h2 = Host( hnames.next() ), Host( hnames.next() )
|
||||
createLink( h1, row[ 0 ] )
|
||||
createLink( h2, row[ -1 ] )
|
||||
hosts += [ h1, h2 ]
|
||||
print h1.name, h2.name, ; flush()
|
||||
# Return here if we're using this to make a linear network
|
||||
if self.linear:
|
||||
print "returning linear network"
|
||||
return switches, hosts
|
||||
# Hook up columns
|
||||
for x in range( 0, n ):
|
||||
previous = None
|
||||
for y in range( 0, m ):
|
||||
switch = rows[ y ][ x ]
|
||||
if previous is not None:
|
||||
createLink( switch, previous )
|
||||
previous = switch
|
||||
h1, h2 = Host( hnames.next() ), Host( hnames.next() )
|
||||
createLink( h1, rows[ 0 ][ x ] )
|
||||
createLink( h2, rows[ -1 ][ x ] )
|
||||
hosts += [ h1, h2 ]
|
||||
print h1.name, h2.name, ; flush()
|
||||
return switches, hosts
|
||||
|
||||
class LinearNet( GridNet ):
|
||||
def __init__( self, switchCount, kernel=True ):
|
||||
self.switchCount = switchCount
|
||||
GridNet.__init__( self, switchCount, 1, kernel, linear=True )
|
||||
|
||||
# Tests
|
||||
|
||||
def parsePing( pingOutput ):
|
||||
"Parse ping output and return packets sent, received"
|
||||
"Parse ping output and return packets sent, received."
|
||||
r = r'(\d+) packets transmitted, (\d+) received'
|
||||
m = re.search( r, pingOutput )
|
||||
if m == None:
|
||||
@@ -288,7 +586,7 @@ def parsePing( pingOutput ):
|
||||
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
|
||||
return sent, received
|
||||
|
||||
def pingTest( hosts, verbose=False ):
|
||||
def pingTest( controllers, switches, hosts, verbose=False ):
|
||||
"Test that each host can reach every other host."
|
||||
packets = 0 ; lost = 0
|
||||
for node in hosts:
|
||||
@@ -314,308 +612,122 @@ def pingTest( hosts, verbose=False ):
|
||||
flush()
|
||||
return ploss
|
||||
|
||||
def dumpNodes( nodes ):
|
||||
"Dump ifconfig of each node."
|
||||
for node in nodes:
|
||||
print "*** Dumping node", node.name
|
||||
print node.cmd( 'ip link show' )
|
||||
print node.cmd( 'route' )
|
||||
|
||||
def ipGen( A, B, c, d ):
|
||||
"Generate next IP class B IP address, starting at A.B.c.d"
|
||||
while True:
|
||||
yield '%d.%d.%d.%d' % ( A, B, c, d )
|
||||
d += 1
|
||||
if d > 254:
|
||||
d = 1
|
||||
c += 1
|
||||
if c > 254: break
|
||||
|
||||
def nameGen( prefix ):
|
||||
"Generate names starting with prefix."
|
||||
i = 0
|
||||
while True:
|
||||
yield prefix + `i`
|
||||
i += 1
|
||||
|
||||
# Control network support
|
||||
# Instead of routing, we could bridge or use "in-band" control
|
||||
|
||||
def checkUp( node ):
|
||||
"Make sure node's first interface is up."
|
||||
return 'UP' in node.cmd( 'ifconfig ' + node.intfs[ 0 ] )
|
||||
|
||||
def configRoutedControlNetwork( controller, switches ):
|
||||
"Configure a routed control network on controller and switches."
|
||||
cip = '10.0.0.1'
|
||||
sips = ipGen( 10, 123, 0, 1)
|
||||
print controller.name, '<->',
|
||||
for switch in switches:
|
||||
print switch.name, ; flush()
|
||||
sip = sips.next()
|
||||
sintf = switch.intfs[ 0 ]
|
||||
node, cintf = switch.connection[ sintf ]
|
||||
assert node == controller
|
||||
controller.setIP( cintf, cip, '/24')
|
||||
switch.setIP( sintf, sip, '/24' )
|
||||
controller.setHostRoute( sip, cintf )
|
||||
switch.setHostRoute( cip, sintf )
|
||||
print
|
||||
print "*** Testing control network"
|
||||
while not checkUp( controller ):
|
||||
print "*** Waiting for ", controller.intfs[ 0 ], "to come up"
|
||||
sleep( 1 )
|
||||
for switch in switches:
|
||||
while not checkUp( switch ):
|
||||
print "*** Waiting for ", controller.intfs[ 0 ], "to come up"
|
||||
sleep( 1 )
|
||||
if pingTest( [ switch, controller ] ) != 0:
|
||||
print "*** Error: control network test failed"
|
||||
else:
|
||||
return
|
||||
exit( 1 )
|
||||
|
||||
def configHosts( hosts, ( a, b, c, d ) ):
|
||||
"Configure a set of hosts, starting at IP address a.b.c.d"
|
||||
ips = ipGen( a, b, c, d )
|
||||
for host in hosts:
|
||||
hintf = host.intfs[ 0 ]
|
||||
host.setIP( hintf, ips.next(), '/24' )
|
||||
host.setDefaultRoute( hintf )
|
||||
# You're low priority, dude!
|
||||
quietRun( 'renice +18 -p ' + `host.pid` )
|
||||
print host.name, ; flush()
|
||||
print
|
||||
|
||||
def startController( controller, cprog='controller', cargs='ptcp:' ):
|
||||
"Start <cprog cargs> on controller, logging to /tmp/cN.log"
|
||||
cout = '/tmp/' + controller.name + '.log'
|
||||
controller.cmdPrint( cprog + ' ' + cargs +
|
||||
' 1> ' + cout + ' 2> ' + cout + ' &' )
|
||||
|
||||
def stopController( controller, cprog='controller' ):
|
||||
"Stop controller cprog on controller"
|
||||
controller.cmd( "kill %" + cprog )
|
||||
|
||||
def startOpenFlowU( switch, controller ):
|
||||
"""Start OpenFlow reference user datapath on a switch,
|
||||
logging to /tmp/sN-{ofd,ofp}.log"""
|
||||
ofdlog = '/tmp/' + switch.name + '-ofd.log'
|
||||
ofplog = '/tmp/' + switch.name + '-ofp.log'
|
||||
switch.cmd( 'ifconfig lo up' )
|
||||
intfs = switch.intfs[ 1 : ] # 0 is mgmt interface
|
||||
switch.cmdPrint( 'ofdatapath -i ' + ','.join( intfs ) +
|
||||
' ptcp: 1> ' + ofdlog + ' 2> '+ ofdlog + ' &' )
|
||||
switch.cmdPrint( 'ofprotocol tcp:' + controller.IP() +
|
||||
' tcp:localhost 1> ' + ofplog + ' 2>' + ofplog + '&' )
|
||||
|
||||
def stopOpenFlowU( switch ):
|
||||
"Stop OpenFlow reference user datapath on a switch."
|
||||
switch.cmd( "kill %ofdatapath" )
|
||||
switch.cmd( "kill %ofprotocol" )
|
||||
|
||||
def dpgen():
|
||||
"Generator for OpenFlow kernel datapath names."
|
||||
dpCount = 0
|
||||
while True:
|
||||
yield 'nl:' + `dpCount`
|
||||
dpCount += 1
|
||||
|
||||
def startOpenFlowK( switch, dp, controller):
|
||||
"Start up a switch connected to an OpenFlow reference kernel datapath."
|
||||
ofplog = '/tmp/' + switch.name + '-ofp.log'
|
||||
switch.cmd( 'ifconfig lo up' )
|
||||
# Delete local datapath if it exists;
|
||||
# then create a new one monitoring the given interfaces
|
||||
quietRun( 'dpctl deldp ' + dp )
|
||||
switch.cmdPrint( 'dpctl adddp ' + dp )
|
||||
switch.cmdPrint( 'dpctl addif ' + dp + ' ' + ' '.join( switch.intfs ) )
|
||||
switch.dp = dp
|
||||
# Become protocol daemon
|
||||
switch.cmdPrint( 'exec ofprotocol' +
|
||||
' ' + dp + ' tcp:127.0.0.1 1> ' + ofplog + ' 2>' + ofplog + '&' )
|
||||
|
||||
def stopOpenFlowK( switch ):
|
||||
"Terminate a switch using OpenFlow reference kernel datapath."
|
||||
quietRun( 'dpctl deldp ' + switch.dp )
|
||||
for intf in switch.intfs: quietRun( 'ip link del ' + intf )
|
||||
switch.terminate()
|
||||
|
||||
def stopOpenFlow( switch ):
|
||||
if hasattr(switch, 'dp' ): stopOpenFlowK( switch )
|
||||
else: stopOpenFlowU( switch )
|
||||
|
||||
# Test scenarios and topologies
|
||||
|
||||
def treeNet( controller, depth, fanout, snames=nameGen( 's' ),
|
||||
hnames=nameGen( 'h' ), kernel=True ):
|
||||
"""Return a tree network of the given depth and fanout as a triple:
|
||||
( root, switches, hosts ), using the given switch and host
|
||||
name generators, with the switches connected to the given
|
||||
controller."""
|
||||
if ( depth == 0 ):
|
||||
host = Node( hnames.next() )
|
||||
print host.name, ; flush()
|
||||
return host, [], [ host ]
|
||||
switch = Node( snames.next(), inNamespace=(not kernel) )
|
||||
if not kernel: createLink( switch, controller )
|
||||
print switch.name, ; flush()
|
||||
switches, hosts = [ switch ], []
|
||||
for i in range( 0, fanout ):
|
||||
child, slist, hlist = treeNet(
|
||||
controller, depth - 1, fanout, snames, hnames, kernel )
|
||||
createLink( switch, child )
|
||||
switches += slist
|
||||
hosts += hlist
|
||||
return switch, switches, hosts
|
||||
|
||||
def treeNetTest( depth, fanout, test, kernel=True ):
|
||||
"""Create a tree network of the given depth and fanout, and
|
||||
run test( controller, root, switches, hosts ) on it."""
|
||||
if kernel: print "*** Using kernel datapath"
|
||||
else: print "*** Using user datapath"
|
||||
print "*** Creating controller"
|
||||
controller = Node( 'c0', inNamespace=( not kernel ) )
|
||||
print "*** Creating tree network depth:", depth, "fanout:", fanout
|
||||
root, switches, hosts = treeNet( controller, depth, fanout, kernel=kernel )
|
||||
print
|
||||
if not kernel:
|
||||
print "*** Configuring control network"
|
||||
configRoutedControlNetwork( controller, switches )
|
||||
else: dp = dpgen()
|
||||
print "*** Configuring hosts"
|
||||
configHosts( hosts, ( 192, 168, 123, 1 ) )
|
||||
print "*** Starting reference controller"
|
||||
startController( controller )
|
||||
print "*** Starting switches"
|
||||
for switch in switches:
|
||||
if kernel: startOpenFlowK( switch, dp.next(), controller )
|
||||
else: startOpenFlowU( switch, controller )
|
||||
print "*** Running test"
|
||||
test( controller, root, switches, hosts )
|
||||
print "*** Stopping controller"
|
||||
stopController( controller )
|
||||
print "*** Stopping switches"
|
||||
for switch in switches:
|
||||
stopOpenFlow( switch )
|
||||
|
||||
def treePingTest( depth, fanout, kernel=True ):
|
||||
"Run a ping test on a tree network with the given depth and fanout."
|
||||
test = lambda c, r, s, hosts : pingTest( hosts, verbose=True )
|
||||
treeNetTest( depth, fanout, test, kernel)
|
||||
def pingTestVerbose( controllers, switches, hosts ):
|
||||
return pingTest( controllers, switches, hosts, verbose=True )
|
||||
|
||||
def iperf( hosts ):
|
||||
"Run iperf between two hosts."
|
||||
assert len( hosts ) == 2
|
||||
host1, host2 = hosts[ 0 ], hosts[ 1 ]
|
||||
dumpNodes( [ host1, host2 ] )
|
||||
host1.cmdPrint( 'killall -9 iperf')
|
||||
# dumpNodes( [ host1, host2 ] )
|
||||
host1.cmdPrint( 'killall -9 iperf') # XXX shouldn't be global killall
|
||||
host1.cmdPrint( 'iperf -s &' )
|
||||
host2.cmdPrint( 'iperf -t 5 -c ' + host1.IP() )
|
||||
host1.cmdPrint( 'kill -9 %iperf' )
|
||||
|
||||
def iperfTest( depth=1, fanout=2, kernel=True ):
|
||||
def iperfTest( controllers, switches, hosts ):
|
||||
"Simple iperf test between two hosts."
|
||||
def test( c, r, s, hosts ):
|
||||
h0, hN = hosts[ 0 ], hosts[ -1 ]
|
||||
print "*** iperfTest: Testing bandwidth between",
|
||||
print h0.name, "and", hN.name
|
||||
return iperf( [ h0, hN] )
|
||||
treeNetTest( depth, fanout, test, kernel )
|
||||
h0, hN = hosts[ 0 ], hosts[ -1 ]
|
||||
print "*** iperfTest: Testing bandwidth between",
|
||||
print h0.name, "and", hN.name
|
||||
return iperf( [ h0, hN] )
|
||||
|
||||
# Simple CLI
|
||||
|
||||
class Cli( object ):
|
||||
"Simple command-line interface to talk to nodes."
|
||||
cmds = [ '?', 'help', 'nodes', 'sh', 'pingtest', 'iperf', 'net', 'exit' ]
|
||||
def __init__( self, controllers, switches, hosts ):
|
||||
self.controllers = controllers
|
||||
self.switches = switches
|
||||
self.hosts = hosts
|
||||
self.nodemap = {}
|
||||
self.nodelist = controllers + switches + hosts
|
||||
for node in self.nodelist:
|
||||
self.nodemap[ node.name ] = node
|
||||
self.run()
|
||||
# Commands
|
||||
def help( self, args ):
|
||||
"Semi-useful help for CLI"
|
||||
print "available commands are:", self.cmds
|
||||
def nodes( self, args ):
|
||||
"List available nodes"
|
||||
print "available nodes are:", [ node.name for node in self.nodelist]
|
||||
def sh( self, args ):
|
||||
"Run an external shell command"
|
||||
call( [ 'sh', '-c' ] + args )
|
||||
def pingtest( self, args ):
|
||||
pingTest( self.controllers, self.switches, self.hosts, verbose=True )
|
||||
def net( self, args ):
|
||||
for switch in self.switches:
|
||||
print switch.name, "<->",
|
||||
for intf in switch.intfs:
|
||||
node, rintf = switch.connection[ intf ]
|
||||
print node.name,
|
||||
print
|
||||
def iperf( self, args ):
|
||||
print "iperf: got args", args
|
||||
if len( args ) != 2:
|
||||
print "usage: iperf <h1> <h2>"
|
||||
return
|
||||
for host in args:
|
||||
if host not in self.nodemap:
|
||||
print "iperf: cannot find host:", host
|
||||
return
|
||||
iperf( [ self.nodemap[ h ] for h in args ] )
|
||||
# Interpreter
|
||||
def run( self ):
|
||||
"Read and execute commands."
|
||||
print "*** cli: starting"
|
||||
while True:
|
||||
print "mininet> ", ; flush()
|
||||
input = sys.stdin.readline()
|
||||
if input == '': break
|
||||
if input[ -1 ] == '\n': input = input[ : -1 ]
|
||||
cmd = input.split( ' ' )
|
||||
first = cmd[ 0 ]
|
||||
rest = cmd[ 1: ]
|
||||
if first in self.cmds and hasattr( self, first ):
|
||||
getattr( self, first )( rest )
|
||||
elif first in self.nodemap and rest != []:
|
||||
node = self.nodemap[ first ]
|
||||
# Substitute IP addresses for node names in command
|
||||
rest = [ self.nodemap[ arg ].IP() if arg in self.nodemap else arg
|
||||
for arg in rest ]
|
||||
rest = ' '.join( rest )
|
||||
# Interactive commands don't work yet, and
|
||||
# there are still issues with control-c
|
||||
print "***", node.name, ": running", rest
|
||||
node.sendCmd( rest )
|
||||
while True:
|
||||
try:
|
||||
done, data = node.monitor()
|
||||
print data,
|
||||
if done: break
|
||||
except KeyboardInterrupt: node.sendInt()
|
||||
print
|
||||
elif first == '': pass
|
||||
elif first in [ 'exit', 'quit' ]: break
|
||||
elif first == '?': self.help( rest )
|
||||
else: print "cli: unknown node or command: <", first, ">"
|
||||
print "*** cli: exiting"
|
||||
|
||||
def fixLimits():
|
||||
"Fix ridiculously small resource limits."
|
||||
setrlimit( RLIMIT_NPROC, ( 4096, 8192 ) )
|
||||
setrlimit( RLIMIT_NOFILE, ( 16384, 32768 ) )
|
||||
|
||||
# Simple CLI
|
||||
|
||||
def cliHelp( nodemap, c, s, h, args ):
|
||||
"Semi-useful help for CLI"
|
||||
print "available commands are:", cliCmds.keys()
|
||||
|
||||
def cliNodes( nodemap, c, s, h, args ):
|
||||
"List available nodes"
|
||||
print "available nodes are:", nodemap.keys()
|
||||
|
||||
def cliSh( nodemap, c, s, h, args ):
|
||||
"Run an external shell command"
|
||||
call( [ bash, '-c', args ] )
|
||||
|
||||
def cliPingTest( map, c, s, hosts, args ):
|
||||
pingTest( hosts, verbose=True )
|
||||
|
||||
def cliNet( map, c, switches, h, args ):
|
||||
for switch in switches:
|
||||
print switch.name, "<->",
|
||||
for intf in switch.intfs:
|
||||
node, rintf = switch.connection[ intf ]
|
||||
print node.name,
|
||||
print
|
||||
|
||||
def cliIperf( map, c, switches, h, args ):
|
||||
print "iperf: got args", args
|
||||
if len( args ) != 2:
|
||||
print "usage: iperf <h1> <h2>"
|
||||
return
|
||||
for host in args:
|
||||
if host not in map:
|
||||
print "iperf: cannot find host:", host
|
||||
return
|
||||
iperf( [ map[ h ] for h in args ] )
|
||||
|
||||
cliCmds = { '?': cliHelp, 'help': cliHelp, 'net': cliNet, 'nodes': cliNodes,
|
||||
'pingtest': cliPingTest, 'iperf': cliIperf, 'sh': cliSh,
|
||||
'exit': None }
|
||||
|
||||
def cli( controllers, switches, hosts ):
|
||||
"Simple command-line interface to talk to nodes."
|
||||
print "*** cli: starting"
|
||||
nodemap = {}
|
||||
nodes = controllers + switches + hosts
|
||||
for node in nodes:
|
||||
nodemap[ node.name ] = node
|
||||
while True:
|
||||
print "mininet> ", ; flush()
|
||||
input = sys.stdin.readline()
|
||||
if input == '': break
|
||||
if input[ -1 ] == '\n': input = input[ : -1 ]
|
||||
cmd = input.split( ' ' )
|
||||
first = cmd[ 0 ]
|
||||
rest = cmd[ 1: ]
|
||||
if first in cliCmds: cliCmds[ first ](
|
||||
nodemap, controllers, switches, hosts, rest )
|
||||
elif first in nodemap and rest != []:
|
||||
node = nodemap[ first ]
|
||||
# Substitute IP addresses for node names in command
|
||||
rest = [ nodemap[ arg ].IP() if arg in nodemap else arg
|
||||
for arg in rest ]
|
||||
rest = ' '.join( rest )
|
||||
# Interactive commands don't work yet, and
|
||||
# there are still issues with control-c
|
||||
print "***", node.name, ": running", rest
|
||||
node.sendCmd( rest )
|
||||
while True:
|
||||
try:
|
||||
done, data = node.monitor()
|
||||
print data,
|
||||
if done: break
|
||||
except KeyboardInterrupt: node.sendInt()
|
||||
print
|
||||
elif first == '': pass
|
||||
elif first in [ 'exit', 'quit' ]: break
|
||||
else: print "cli: unknown node or command: <", first, ">"
|
||||
print "*** cli: exiting"
|
||||
|
||||
def treeInteract( depth, fanout, kernel=True ):
|
||||
"Create a tree network and start the CLI."
|
||||
interact = lambda c, r, s, h : cli( [ c ], s, h )
|
||||
treeNetTest( depth, fanout, interact, kernel )
|
||||
def init():
|
||||
# Note: this script must be run as root
|
||||
# Perhaps we should do so automatically!
|
||||
if os.getuid() != 0:
|
||||
print "*** Mininet must run as root."; exit( 1 )
|
||||
fixLimits()
|
||||
|
||||
if __name__ == '__main__':
|
||||
fixLimits()
|
||||
init()
|
||||
# for kernel in [ False, True ]:
|
||||
# treePingTest( depth=3, fanout=4, kernel=kernel )
|
||||
# treeInteract( depth=1, fanout=2, kernel=False )
|
||||
# iperfTest( depth=1, fanout=2, kernel=kernel )
|
||||
treeInteract( depth=1, fanout=2, kernel=False )
|
||||
# TreeNet( depth=3, fanout=4, kernel=kernel).run( pingTest )
|
||||
TreeNet( depth=2, fanout=32).run( Cli )
|
||||
# LinearNet( switchCount=100 ).run( iperfTest)
|
||||
# GridNet( 2, 2 ).run( Cli )
|
||||
Reference in New Issue
Block a user