diff --git a/cleanup b/cleanup index ba031eda6752ac7bd4a55ae8d38c4f674736f307..2375e848c6f504939fd819471543bbc9cf317159 100755 --- a/cleanup +++ b/cleanup @@ -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 + diff --git a/mininet.py b/mininet.py index f5a25181996bdf75eab4dd2880f4807484e37a4c..fe5091535ad1be2048214fde30142fa50bfdb3fc 100755 --- a/mininet.py +++ b/mininet.py @@ -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 @@ 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 @@ 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 @@ def __init__( self, name, inNamespace=True ): 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 @@ def __init__( self, name, inNamespace=True ): 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 @@ def cmdPrint( self, cmd ): 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 @@ def newIntf( self ): 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 ): @@ -213,13 +203,17 @@ def setDefaultRoute( self, intf ): return self.cmd( 'route add default ' + intf ) def IP( self ): "Return IP address of first interface" - return self.ips[ self.intfs[ 0 ] ] + 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 result += " waiting=", self.waiting return result - + # Maintain mapping between i/o pipes and nodes # This could be useful for monitoring multiple nodes # using select.poll() @@ -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' ) @@ -277,50 +344,20 @@ def createLink( node1, node2 ): return intf1, intf2 # Handy utilities - -def parsePing( pingOutput ): - "Parse ping output and return packets sent, received" - r = r'(\d+) packets transmitted, (\d+) received' - m = re.search( r, pingOutput ) - if m == None: - print "*** Error: could not parse ping output:", pingOutput - exit( 1 ) - sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) - return sent, received - -def pingTest( hosts, verbose=False ): - "Test that each host can reach every other host." - packets = 0 ; lost = 0 - for node in hosts: - if verbose: - print node.name, "->", ; flush() - for dest in hosts: - if node != dest: - result = node.cmd( 'ping -c1 ' + dest.IP() ) - sent, received = parsePing( result ) - packets += sent - if received > sent: - print "*** Error: received too many packets" - print result - node.cmdPrint( 'route' ) - exit( 1 ) - lost += sent - received - if verbose: - print ( dest.name if received else "X" ), ; flush() - if verbose: print - ploss = 100 * lost/packets - if verbose: - print "%d%% packet loss (%d/%d lost)" % ( ploss, lost, packets ) - flush() - return ploss - + +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: @@ -334,45 +371,43 @@ def ipGen( A, B, c, d ): def nameGen( prefix ): "Generate names starting with prefix." i = 0 - while True: - yield prefix + `i` - i += 1 - + 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 ] ) +# 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 ): - "Configure a routed control network on controller and switches." - cip = '10.0.0.1' - sips = ipGen( 10, 123, 0, 1) +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 = sips.next() + sip = ips.next() sintf = switch.intfs[ 0 ] node, cintf = switch.connection[ sintf ] - assert node == controller - controller.setIP( cintf, cip, '/24') + 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 checkUp( controller ): + while not controller.intfIsUp( controller.intfs[ 0 ] ): 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" + 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" - else: - return + if pingTest( [ switch, controller ] ) != 0: + print "*** Error: control network test failed" exit( 1 ) def configHosts( hosts, ( a, b, c, d ) ): @@ -386,236 +421,313 @@ def configHosts( hosts, ( a, b, c, d ) ): 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 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 parsePing( pingOutput ): + "Parse ping output and return packets sent, received." + r = r'(\d+) packets transmitted, (\d+) received' + m = re.search( r, pingOutput ) + if m == None: + print "*** Error: could not parse ping output:", pingOutput + exit( 1 ) + sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) + return sent, received -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 pingTest( controllers, switches, hosts, verbose=False ): + "Test that each host can reach every other host." + packets = 0 ; lost = 0 + for node in hosts: + if verbose: + print node.name, "->", ; flush() + for dest in hosts: + if node != dest: + result = node.cmd( 'ping -c1 ' + dest.IP() ) + sent, received = parsePing( result ) + packets += sent + if received > sent: + print "*** Error: received too many packets" + print result + node.cmdPrint( 'route' ) + exit( 1 ) + lost += sent - received + if verbose: + print ( dest.name if received else "X" ), ; flush() + if verbose: print + ploss = 100 * lost/packets + if verbose: + print "%d%% packet loss (%d/%d lost)" % ( ploss, lost, packets ) + flush() + return ploss -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 ) \ No newline at end of file + # 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 ) \ No newline at end of file