You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1024 lines
39 KiB
Python
1024 lines
39 KiB
Python
5 months ago
|
"""
|
||
|
|
||
|
Mininet: A simple networking testbed for OpenFlow/SDN!
|
||
|
|
||
|
author: Bob Lantz (rlantz@cs.stanford.edu)
|
||
|
author: Brandon Heller (brandonh@stanford.edu)
|
||
|
|
||
|
Mininet creates scalable OpenFlow test networks by using
|
||
|
process-based virtualization and network namespaces.
|
||
|
|
||
|
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 parent shell (and possibly some child processes) in a namespace
|
||
|
|
||
|
Hosts have a network interface which is configured via ifconfig/ip
|
||
|
link/etc.
|
||
|
|
||
|
This version supports both the kernel and user space datapaths
|
||
|
from the OpenFlow reference implementation (openflowswitch.org)
|
||
|
as well as OpenVSwitch (openvswitch.org.)
|
||
|
|
||
|
In kernel datapath mode, the controller and switches are simply
|
||
|
processes in the root namespace.
|
||
|
|
||
|
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 switches can be full-service
|
||
|
nodes that live in their own network namespaces and have management
|
||
|
interfaces and IP addresses on a control network (e.g. 192.168.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.
|
||
|
|
||
|
Consistent, straightforward naming is important in order to easily
|
||
|
identify hosts, switches and controllers, both from the CLI and
|
||
|
from program code. Interfaces are named to make it easy to identify
|
||
|
which interfaces belong to which node.
|
||
|
|
||
|
The basic naming scheme is as follows:
|
||
|
|
||
|
Host nodes are named h1-hN
|
||
|
Switch nodes are named s1-sN
|
||
|
Controller nodes are named c0-cN
|
||
|
Interfaces are named {nodename}-eth0 .. {nodename}-ethN
|
||
|
|
||
|
Note: If the network topology is created using mininet.topo, then
|
||
|
node numbers are unique among hosts and switches (e.g. we have
|
||
|
h1..hN and SN..SN+M) and also correspond to their default IP addresses
|
||
|
of 10.x.y.z/8 where x.y.z is the base-256 representation of N for
|
||
|
hN. This mapping allows easy determination of a node's IP
|
||
|
address from its name, e.g. h1 -> 10.0.0.1, h257 -> 10.0.1.1.
|
||
|
|
||
|
Note also that 10.0.0.1 can often be written as 10.1 for short, e.g.
|
||
|
"ping 10.1" is equivalent to "ping 10.0.0.1".
|
||
|
|
||
|
Currently we wrap the entire network in a 'mininet' object, which
|
||
|
constructs a simulated network based on a network topology created
|
||
|
using a topology object (e.g. LinearTopo) from mininet.topo or
|
||
|
mininet.topolib, and a Controller which the switches will connect
|
||
|
to. Several configuration options are provided for functions such as
|
||
|
automatically setting MAC addresses, populating the ARP table, or
|
||
|
even running a set of terminals to allow direct interaction with nodes.
|
||
|
|
||
|
After the network is created, it can be started using start(), and a
|
||
|
variety of useful tasks maybe performed, including basic connectivity
|
||
|
and bandwidth tests and running the mininet CLI.
|
||
|
|
||
|
Once the network is up and running, test code can easily get access
|
||
|
to host and switch objects which can then be used for arbitrary
|
||
|
experiments, typically involving running a series of commands on the
|
||
|
hosts.
|
||
|
|
||
|
After all desired tests or activities have been completed, the stop()
|
||
|
method may be called to shut down the network.
|
||
|
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import re
|
||
|
import select
|
||
|
import signal
|
||
|
import random
|
||
|
|
||
|
from sys import exit # pylint: disable=redefined-builtin
|
||
|
from time import sleep
|
||
|
from itertools import chain, groupby
|
||
|
from math import ceil
|
||
|
|
||
|
from mininet.cli import CLI
|
||
|
from mininet.log import info, error, output, warn, debug
|
||
|
from mininet.node import ( Node, Host, OVSKernelSwitch, DefaultController,
|
||
|
Controller )
|
||
|
from mininet.nodelib import NAT
|
||
|
from mininet.link import Link, Intf
|
||
|
from mininet.util import ( quietRun, fixLimits, numCores, ensureRoot,
|
||
|
macColonHex, ipStr, ipParse, netParse, ipAdd,
|
||
|
waitListening, BaseString, fmtBps )
|
||
|
from mininet.term import cleanUpScreens, makeTerms
|
||
|
|
||
|
# Mininet version: should be consistent with README and LICENSE
|
||
|
VERSION = "2.3.1b4"
|
||
|
|
||
|
class Mininet( object ):
|
||
|
"Network emulation with hosts spawned in network namespaces."
|
||
|
|
||
|
# pylint: disable=too-many-arguments
|
||
|
def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host,
|
||
|
controller=DefaultController, link=Link, intf=Intf,
|
||
|
build=True, xterms=False, cleanup=False, ipBase='10.0.0.0/8',
|
||
|
inNamespace=False,
|
||
|
autoSetMacs=False, autoStaticArp=False, autoPinCpus=False,
|
||
|
listenPort=None, waitConnected=False ):
|
||
|
"""Create Mininet object.
|
||
|
topo: Topo (topology) object or None
|
||
|
switch: default Switch class
|
||
|
host: default Host class/constructor
|
||
|
controller: default Controller class/constructor
|
||
|
link: default Link class/constructor
|
||
|
intf: default Intf class/constructor
|
||
|
ipBase: base IP address for hosts,
|
||
|
build: build now from topo?
|
||
|
xterms: if build now, spawn xterms?
|
||
|
cleanup: if build now, cleanup before creating?
|
||
|
inNamespace: spawn switches and controller in net namespaces?
|
||
|
autoSetMacs: set MAC addrs automatically like IP addresses?
|
||
|
autoStaticArp: set all-pairs static MAC addrs?
|
||
|
autoPinCpus: pin hosts to (real) cores (requires CPULimitedHost)?
|
||
|
listenPort: base listening port to open; will be incremented for
|
||
|
each additional switch in the net if inNamespace=False
|
||
|
waitConnected: wait for switches to Connect?
|
||
|
(False; True/None=wait indefinitely; time(s)=timed wait)"""
|
||
|
self.topo = topo
|
||
|
self.switch = switch
|
||
|
self.host = host
|
||
|
self.controller = controller
|
||
|
self.link = link
|
||
|
self.intf = intf
|
||
|
self.ipBase = ipBase
|
||
|
self.ipBaseNum, self.prefixLen = netParse( self.ipBase )
|
||
|
hostIP = ( 0xffffffff >> self.prefixLen ) & self.ipBaseNum
|
||
|
# Start for address allocation
|
||
|
self.nextIP = hostIP if hostIP > 0 else 1
|
||
|
self.inNamespace = inNamespace
|
||
|
self.xterms = xterms
|
||
|
self.cleanup = cleanup
|
||
|
self.autoSetMacs = autoSetMacs
|
||
|
self.autoStaticArp = autoStaticArp
|
||
|
self.autoPinCpus = autoPinCpus
|
||
|
self.numCores = numCores()
|
||
|
self.nextCore = 0 # next core for pinning hosts to CPUs
|
||
|
self.listenPort = listenPort
|
||
|
self.waitConn = waitConnected
|
||
|
|
||
|
self.hosts = []
|
||
|
self.switches = []
|
||
|
self.controllers = []
|
||
|
self.links = []
|
||
|
|
||
|
self.nameToNode = {} # name to Node (Host/Switch) objects
|
||
|
|
||
|
self.terms = [] # list of spawned xterm processes
|
||
|
|
||
|
Mininet.init() # Initialize Mininet if necessary
|
||
|
|
||
|
self.built = False
|
||
|
if topo and build:
|
||
|
self.build()
|
||
|
|
||
|
def waitConnected( self, timeout=None, delay=.5 ):
|
||
|
"""wait for each switch to connect to a controller
|
||
|
timeout: time to wait, or None or True to wait indefinitely
|
||
|
delay: seconds to sleep per iteration
|
||
|
returns: True if all switches are connected"""
|
||
|
info( '*** Waiting for switches to connect\n' )
|
||
|
time = 0.0
|
||
|
remaining = list( self.switches )
|
||
|
# False: 0s timeout; None: wait forever (preserve 2.2 behavior)
|
||
|
if isinstance( timeout, bool ):
|
||
|
timeout = None if timeout else 0
|
||
|
while True:
|
||
|
for switch in tuple( remaining ):
|
||
|
if switch.connected():
|
||
|
info( '%s ' % switch )
|
||
|
remaining.remove( switch )
|
||
|
if not remaining:
|
||
|
info( '\n' )
|
||
|
return True
|
||
|
if timeout is not None and time >= timeout:
|
||
|
break
|
||
|
sleep( delay )
|
||
|
time += delay
|
||
|
warn( 'Timed out after %d seconds\n' % time )
|
||
|
for switch in remaining.copy():
|
||
|
if not switch.connected():
|
||
|
warn( 'Warning: %s is not connected to a controller\n'
|
||
|
% switch.name )
|
||
|
else:
|
||
|
remaining.remove( switch )
|
||
|
return not remaining
|
||
|
|
||
|
def addHost( self, name, cls=None, **params ):
|
||
|
"""Add host.
|
||
|
name: name of host to add
|
||
|
cls: custom host class/constructor (optional)
|
||
|
params: parameters for host
|
||
|
returns: added host"""
|
||
|
# Default IP and MAC addresses
|
||
|
defaults = { 'ip': ipAdd( self.nextIP,
|
||
|
ipBaseNum=self.ipBaseNum,
|
||
|
prefixLen=self.prefixLen ) +
|
||
|
'/%s' % self.prefixLen }
|
||
|
if self.autoSetMacs:
|
||
|
defaults[ 'mac' ] = macColonHex( self.nextIP )
|
||
|
if self.autoPinCpus:
|
||
|
defaults[ 'cores' ] = self.nextCore
|
||
|
self.nextCore = ( self.nextCore + 1 ) % self.numCores
|
||
|
self.nextIP += 1
|
||
|
defaults.update( params )
|
||
|
if not cls:
|
||
|
cls = self.host
|
||
|
h = cls( name, **defaults )
|
||
|
self.hosts.append( h )
|
||
|
self.nameToNode[ name ] = h
|
||
|
return h
|
||
|
|
||
|
def delNode( self, node, nodes=None):
|
||
|
"""Delete node
|
||
|
node: node to delete
|
||
|
nodes: optional list to delete from (e.g. self.hosts)"""
|
||
|
if nodes is None:
|
||
|
nodes = ( self.hosts if node in self.hosts else
|
||
|
( self.switches if node in self.switches else
|
||
|
( self.controllers if node in self.controllers else
|
||
|
[] ) ) )
|
||
|
node.stop( deleteIntfs=True )
|
||
|
node.terminate()
|
||
|
nodes.remove( node )
|
||
|
del self.nameToNode[ node.name ]
|
||
|
|
||
|
def delHost( self, host ):
|
||
|
"Delete a host"
|
||
|
self.delNode( host, nodes=self.hosts )
|
||
|
|
||
|
def addSwitch( self, name, cls=None, **params ):
|
||
|
"""Add switch.
|
||
|
name: name of switch to add
|
||
|
cls: custom switch class/constructor (optional)
|
||
|
returns: added switch
|
||
|
side effect: increments listenPort ivar ."""
|
||
|
defaults = { 'listenPort': self.listenPort,
|
||
|
'inNamespace': self.inNamespace }
|
||
|
defaults.update( params )
|
||
|
if not cls:
|
||
|
cls = self.switch
|
||
|
sw = cls( name, **defaults )
|
||
|
if not self.inNamespace and self.listenPort:
|
||
|
self.listenPort += 1
|
||
|
self.switches.append( sw )
|
||
|
self.nameToNode[ name ] = sw
|
||
|
return sw
|
||
|
|
||
|
def delSwitch( self, switch ):
|
||
|
"Delete a switch"
|
||
|
self.delNode( switch, nodes=self.switches )
|
||
|
|
||
|
def addController( self, name='c0', controller=None, **params ):
|
||
|
"""Add controller.
|
||
|
controller: Controller class"""
|
||
|
# Get controller class
|
||
|
if not controller:
|
||
|
controller = self.controller
|
||
|
# Construct new controller if one is not given
|
||
|
if isinstance( name, Controller ):
|
||
|
controller_new = name
|
||
|
# Pylint thinks controller is a str()
|
||
|
# pylint: disable=maybe-no-member
|
||
|
name = controller_new.name
|
||
|
# pylint: enable=maybe-no-member
|
||
|
else:
|
||
|
controller_new = controller( name, **params )
|
||
|
# Add new controller to net
|
||
|
if controller_new: # allow controller-less setups
|
||
|
self.controllers.append( controller_new )
|
||
|
self.nameToNode[ name ] = controller_new
|
||
|
return controller_new
|
||
|
|
||
|
def delController( self, controller ):
|
||
|
"""Delete a controller
|
||
|
Warning - does not reconfigure switches, so they
|
||
|
may still attempt to connect to it!"""
|
||
|
self.delNode( controller )
|
||
|
|
||
|
def addNAT( self, name='nat0', connect=True, inNamespace=False,
|
||
|
**params):
|
||
|
"""Add a NAT to the Mininet network
|
||
|
name: name of NAT node
|
||
|
connect: switch to connect to | True (s1) | None
|
||
|
inNamespace: create in a network namespace
|
||
|
params: other NAT node params, notably:
|
||
|
ip: used as default gateway address"""
|
||
|
nat = self.addHost( name, cls=NAT, inNamespace=inNamespace,
|
||
|
subnet=self.ipBase, **params )
|
||
|
# find first switch and create link
|
||
|
if connect:
|
||
|
if not isinstance( connect, Node ):
|
||
|
# Use first switch if not specified
|
||
|
connect = self.switches[ 0 ]
|
||
|
# Connect the nat to the switch
|
||
|
self.addLink( nat, connect )
|
||
|
# Set the default route on hosts
|
||
|
natIP = nat.params[ 'ip' ].split('/')[ 0 ]
|
||
|
for host in self.hosts:
|
||
|
if host.inNamespace:
|
||
|
host.setDefaultRoute( 'via %s' % natIP )
|
||
|
return nat
|
||
|
|
||
|
# BL: We now have four ways to look up nodes
|
||
|
# This may (should?) be cleaned up in the future.
|
||
|
def getNodeByName( self, *args ):
|
||
|
"Return node(s) with given name(s)"
|
||
|
if len( args ) == 1:
|
||
|
return self.nameToNode[ args[ 0 ] ]
|
||
|
return [ self.nameToNode[ n ] for n in args ]
|
||
|
|
||
|
def get( self, *args ):
|
||
|
"Convenience alias for getNodeByName"
|
||
|
return self.getNodeByName( *args )
|
||
|
|
||
|
# Even more convenient syntax for node lookup and iteration
|
||
|
def __getitem__( self, key ):
|
||
|
"net[ name ] operator: Return node with given name"
|
||
|
return self.nameToNode[ key ]
|
||
|
|
||
|
def __delitem__( self, key ):
|
||
|
"del net[ name ] operator - delete node with given name"
|
||
|
self.delNode( self.nameToNode[ key ] )
|
||
|
|
||
|
def __iter__( self ):
|
||
|
"return iterator over node names"
|
||
|
for node in chain( self.hosts, self.switches, self.controllers ):
|
||
|
yield node.name
|
||
|
|
||
|
def __len__( self ):
|
||
|
"returns number of nodes in net"
|
||
|
return ( len( self.hosts ) + len( self.switches ) +
|
||
|
len( self.controllers ) )
|
||
|
|
||
|
def __contains__( self, item ):
|
||
|
"returns True if net contains named node"
|
||
|
return item in self.nameToNode
|
||
|
|
||
|
def keys( self ):
|
||
|
"return a list of all node names or net's keys"
|
||
|
return list( self )
|
||
|
|
||
|
def values( self ):
|
||
|
"return a list of all nodes or net's values"
|
||
|
return [ self[name] for name in self ]
|
||
|
|
||
|
def items( self ):
|
||
|
"return (key,value) tuple list for every node in net"
|
||
|
return zip( self.keys(), self.values() )
|
||
|
|
||
|
@staticmethod
|
||
|
def randMac():
|
||
|
"Return a random, non-multicast MAC address"
|
||
|
return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff |
|
||
|
0x020000000000 )
|
||
|
|
||
|
def addLink( self, node1, node2, port1=None, port2=None,
|
||
|
cls=None, **params ):
|
||
|
""""Add a link from node1 to node2
|
||
|
node1: source node (or name)
|
||
|
node2: dest node (or name)
|
||
|
port1: source port (optional)
|
||
|
port2: dest port (optional)
|
||
|
cls: link class (optional)
|
||
|
params: additional link params (optional)
|
||
|
returns: link object"""
|
||
|
# Accept node objects or names
|
||
|
node1 = node1 if not isinstance( node1, BaseString ) else self[ node1 ]
|
||
|
node2 = node2 if not isinstance( node2, BaseString ) else self[ node2 ]
|
||
|
options = dict( params )
|
||
|
# Port is optional
|
||
|
if port1 is not None:
|
||
|
options.setdefault( 'port1', port1 )
|
||
|
if port2 is not None:
|
||
|
options.setdefault( 'port2', port2 )
|
||
|
if self.intf is not None:
|
||
|
options.setdefault( 'intf', self.intf )
|
||
|
# Set default MAC - this should probably be in Link
|
||
|
options.setdefault( 'addr1', self.randMac() )
|
||
|
options.setdefault( 'addr2', self.randMac() )
|
||
|
cls = self.link if cls is None else cls
|
||
|
link = cls( node1, node2, **options )
|
||
|
self.links.append( link )
|
||
|
return link
|
||
|
|
||
|
def delLink( self, link ):
|
||
|
"Remove a link from this network"
|
||
|
link.delete()
|
||
|
self.links.remove( link )
|
||
|
|
||
|
def linksBetween( self, node1, node2 ):
|
||
|
"Return Links between node1 and node2"
|
||
|
return [ link for link in self.links
|
||
|
if ( node1, node2 ) in (
|
||
|
( link.intf1.node, link.intf2.node ),
|
||
|
( link.intf2.node, link.intf1.node ) ) ]
|
||
|
|
||
|
def delLinkBetween( self, node1, node2, index=0, allLinks=False ):
|
||
|
"""Delete link(s) between node1 and node2
|
||
|
index: index of link to delete if multiple links (0)
|
||
|
allLinks: ignore index and delete all such links (False)
|
||
|
returns: deleted link(s)"""
|
||
|
links = self.linksBetween( node1, node2 )
|
||
|
if not allLinks:
|
||
|
links = [ links[ index ] ]
|
||
|
for link in links:
|
||
|
self.delLink( link )
|
||
|
return links
|
||
|
|
||
|
def configHosts( self ):
|
||
|
"Configure a set of hosts."
|
||
|
for host in self.hosts:
|
||
|
info( host.name + ' ' )
|
||
|
intf = host.defaultIntf()
|
||
|
if intf:
|
||
|
host.configDefault()
|
||
|
else:
|
||
|
# Don't configure nonexistent intf
|
||
|
host.configDefault( ip=None, mac=None )
|
||
|
# You're low priority, dude!
|
||
|
# BL: do we want to do this here or not?
|
||
|
# May not make sense if we have CPU limiting...
|
||
|
# quietRun( 'renice +18 -p ' + repr( host.pid ) )
|
||
|
# This may not be the right place to do this, but
|
||
|
# it needs to be done somewhere.
|
||
|
info( '\n' )
|
||
|
|
||
|
def buildFromTopo( self, topo=None ):
|
||
|
"""Build mininet from a topology object
|
||
|
At the end of this function, everything should be connected
|
||
|
and up."""
|
||
|
|
||
|
# Possibly we should clean up here and/or validate
|
||
|
# the topo
|
||
|
if self.cleanup:
|
||
|
pass
|
||
|
|
||
|
info( '*** Creating network\n' )
|
||
|
|
||
|
if not self.controllers and self.controller:
|
||
|
# Add a default controller
|
||
|
info( '*** Adding controller\n' )
|
||
|
classes = self.controller
|
||
|
if not isinstance( classes, list ):
|
||
|
classes = [ classes ]
|
||
|
for i, cls in enumerate( classes ):
|
||
|
# Allow Controller objects because nobody understands partial()
|
||
|
if isinstance( cls, Controller ):
|
||
|
self.addController( cls )
|
||
|
else:
|
||
|
self.addController( 'c%d' % i, cls )
|
||
|
|
||
|
info( '*** Adding hosts:\n' )
|
||
|
for hostName in topo.hosts():
|
||
|
self.addHost( hostName, **topo.nodeInfo( hostName ) )
|
||
|
info( hostName + ' ' )
|
||
|
|
||
|
info( '\n*** Adding switches:\n' )
|
||
|
for switchName in topo.switches():
|
||
|
# A bit ugly: add batch parameter if appropriate
|
||
|
params = topo.nodeInfo( switchName)
|
||
|
cls = params.get( 'cls', self.switch )
|
||
|
if hasattr( cls, 'batchStartup' ):
|
||
|
params.setdefault( 'batch', True )
|
||
|
self.addSwitch( switchName, **params )
|
||
|
info( switchName + ' ' )
|
||
|
|
||
|
info( '\n*** Adding links:\n' )
|
||
|
for srcName, dstName, params in topo.links(
|
||
|
sort=True, withInfo=True ):
|
||
|
self.addLink( **params )
|
||
|
info( '(%s, %s) ' % ( srcName, dstName ) )
|
||
|
|
||
|
info( '\n' )
|
||
|
|
||
|
def configureControlNetwork( self ):
|
||
|
"Control net config hook: override in subclass"
|
||
|
raise Exception( 'configureControlNetwork: '
|
||
|
'should be overriden in subclass', self )
|
||
|
|
||
|
def build( self ):
|
||
|
"Build mininet."
|
||
|
if self.topo:
|
||
|
self.buildFromTopo( self.topo )
|
||
|
if self.inNamespace:
|
||
|
self.configureControlNetwork()
|
||
|
info( '*** Configuring hosts\n' )
|
||
|
self.configHosts()
|
||
|
if self.xterms:
|
||
|
self.startTerms()
|
||
|
if self.autoStaticArp:
|
||
|
self.staticArp()
|
||
|
self.built = True
|
||
|
|
||
|
def startTerms( self ):
|
||
|
"Start a terminal for each node."
|
||
|
if 'DISPLAY' not in os.environ:
|
||
|
error( "Error starting terms: Cannot connect to display\n" )
|
||
|
return
|
||
|
info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] )
|
||
|
cleanUpScreens()
|
||
|
self.terms += makeTerms( self.controllers, 'controller' )
|
||
|
self.terms += makeTerms( self.switches, 'switch' )
|
||
|
self.terms += makeTerms( self.hosts, 'host' )
|
||
|
|
||
|
def stopXterms( self ):
|
||
|
"Kill each xterm."
|
||
|
for term in self.terms:
|
||
|
os.kill( term.pid, signal.SIGKILL )
|
||
|
cleanUpScreens()
|
||
|
|
||
|
def staticArp( self ):
|
||
|
"Add all-pairs ARP entries to remove the need to handle broadcast."
|
||
|
for src in self.hosts:
|
||
|
for dst in self.hosts:
|
||
|
if src != dst:
|
||
|
src.setARP( ip=dst.IP(), mac=dst.MAC() )
|
||
|
|
||
|
def start( self ):
|
||
|
"Start controller and switches."
|
||
|
if not self.built:
|
||
|
self.build()
|
||
|
info( '*** Starting controller\n' )
|
||
|
for controller in self.controllers:
|
||
|
info( controller.name + ' ')
|
||
|
controller.start()
|
||
|
info( '\n' )
|
||
|
info( '*** Starting %s switches\n' % len( self.switches ) )
|
||
|
for switch in self.switches:
|
||
|
info( switch.name + ' ')
|
||
|
switch.start( self.controllers )
|
||
|
started = {}
|
||
|
for swclass, switches in groupby(
|
||
|
sorted( self.switches,
|
||
|
key=lambda s: str( type( s ) ) ), type ):
|
||
|
switches = tuple( switches )
|
||
|
if hasattr( swclass, 'batchStartup' ):
|
||
|
success = swclass.batchStartup( switches )
|
||
|
started.update( { s: s for s in success } )
|
||
|
info( '\n' )
|
||
|
if self.waitConn:
|
||
|
self.waitConnected( self.waitConn )
|
||
|
|
||
|
def stop( self ):
|
||
|
"Stop the controller(s), switches and hosts"
|
||
|
info( '*** Stopping %i controllers\n' % len( self.controllers ) )
|
||
|
for controller in self.controllers:
|
||
|
info( controller.name + ' ' )
|
||
|
controller.stop()
|
||
|
info( '\n' )
|
||
|
# Unlimit cfs hosts to speed up shutdown
|
||
|
for h in self.hosts:
|
||
|
if hasattr( h, 'unlimit' ):
|
||
|
h.unlimit()
|
||
|
if self.terms:
|
||
|
info( '*** Stopping %i terms\n' % len( self.terms ) )
|
||
|
self.stopXterms()
|
||
|
info( '*** Stopping %i links\n' % len( self.links ) )
|
||
|
for link in self.links:
|
||
|
info( '.' )
|
||
|
link.stop()
|
||
|
info( '\n' )
|
||
|
info( '*** Stopping %i switches\n' % len( self.switches ) )
|
||
|
stopped = {}
|
||
|
for swclass, switches in groupby(
|
||
|
sorted( self.switches,
|
||
|
key=lambda s: str( type( s ) ) ), type ):
|
||
|
switches = tuple( switches )
|
||
|
if hasattr( swclass, 'batchShutdown' ):
|
||
|
success = swclass.batchShutdown( switches )
|
||
|
stopped.update( { s: s for s in success } )
|
||
|
for switch in self.switches:
|
||
|
info( switch.name + ' ' )
|
||
|
if switch not in stopped:
|
||
|
switch.stop()
|
||
|
switch.terminate()
|
||
|
info( '\n' )
|
||
|
info( '*** Stopping %i hosts\n' % len( self.hosts ) )
|
||
|
for host in self.hosts:
|
||
|
info( host.name + ' ' )
|
||
|
host.terminate()
|
||
|
info( '\n*** Done\n' )
|
||
|
|
||
|
def run( self, test, *args, **kwargs ):
|
||
|
"Perform a complete start/test/stop cycle."
|
||
|
self.start()
|
||
|
info( '*** Running test\n' )
|
||
|
result = test( *args, **kwargs )
|
||
|
self.stop()
|
||
|
return result
|
||
|
|
||
|
def monitor( self, hosts=None, timeoutms=-1 ):
|
||
|
"""Monitor a set of hosts (or all hosts by default),
|
||
|
and return their output, a line at a time.
|
||
|
hosts: (optional) set of hosts to monitor
|
||
|
timeoutms: (optional) timeout value in ms
|
||
|
returns: iterator which returns host, line"""
|
||
|
if hosts is None:
|
||
|
hosts = self.hosts
|
||
|
poller = select.poll()
|
||
|
h1 = hosts[ 0 ] # so we can call class method fdToNode
|
||
|
for host in hosts:
|
||
|
poller.register( host.stdout )
|
||
|
while True:
|
||
|
ready = poller.poll( timeoutms )
|
||
|
for fd, event in ready:
|
||
|
host = h1.fdToNode( fd )
|
||
|
if event & select.POLLIN:
|
||
|
line = host.readline()
|
||
|
if line is not None:
|
||
|
yield host, line
|
||
|
# Return if non-blocking
|
||
|
if not ready and timeoutms >= 0:
|
||
|
yield None, None
|
||
|
|
||
|
# XXX These test methods should be moved out of this class.
|
||
|
# Probably we should create a tests.py for them
|
||
|
|
||
|
@staticmethod
|
||
|
def _parsePing( pingOutput ):
|
||
|
"Parse ping output and return packets sent, received."
|
||
|
# Check for downed link
|
||
|
if 'connect: Network is unreachable' in pingOutput:
|
||
|
return 1, 0
|
||
|
r = r'(\d+) packets transmitted, (\d+)( packets)? received'
|
||
|
m = re.search( r, pingOutput )
|
||
|
if m is None:
|
||
|
error( '*** Error: could not parse ping output: %s\n' %
|
||
|
pingOutput )
|
||
|
return 1, 0
|
||
|
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
|
||
|
return sent, received
|
||
|
|
||
|
def ping( self, hosts=None, timeout=None ):
|
||
|
"""Ping between all specified hosts.
|
||
|
hosts: list of hosts
|
||
|
timeout: time to wait for a response, as string
|
||
|
returns: ploss packet loss percentage"""
|
||
|
# should we check if running?
|
||
|
packets = 0
|
||
|
lost = 0
|
||
|
ploss = None
|
||
|
if not hosts:
|
||
|
hosts = self.hosts
|
||
|
output( '*** Ping: testing ping reachability\n' )
|
||
|
for node in hosts:
|
||
|
output( '%s -> ' % node.name )
|
||
|
for dest in hosts:
|
||
|
if node != dest:
|
||
|
opts = ''
|
||
|
if timeout:
|
||
|
opts = '-W %s' % timeout
|
||
|
if dest.intfs:
|
||
|
result = node.cmd( 'LANG=C ping -c1 %s %s' %
|
||
|
(opts, dest.IP()) )
|
||
|
sent, received = self._parsePing( result )
|
||
|
else:
|
||
|
sent, received = 0, 0
|
||
|
packets += sent
|
||
|
if received > sent:
|
||
|
error( '*** Error: received too many packets' )
|
||
|
error( '%s' % result )
|
||
|
node.cmdPrint( 'route' )
|
||
|
exit( 1 )
|
||
|
lost += sent - received
|
||
|
output( ( '%s ' % dest.name ) if received else 'X ' )
|
||
|
output( '\n' )
|
||
|
if packets > 0:
|
||
|
ploss = 100.0 * lost / packets
|
||
|
received = packets - lost
|
||
|
output( "*** Results: %i%% dropped (%d/%d received)\n" %
|
||
|
( ploss, received, packets ) )
|
||
|
else:
|
||
|
ploss = 0
|
||
|
output( "*** Warning: No packets sent\n" )
|
||
|
return ploss
|
||
|
|
||
|
@staticmethod
|
||
|
def _parsePingFull( pingOutput ):
|
||
|
"Parse ping output and return all data."
|
||
|
errorTuple = (1, 0, 0, 0, 0, 0)
|
||
|
# Check for downed link
|
||
|
r = r'[uU]nreachable'
|
||
|
m = re.search( r, pingOutput )
|
||
|
if m is not None:
|
||
|
return errorTuple
|
||
|
r = r'(\d+) packets transmitted, (\d+)( packets)? received'
|
||
|
m = re.search( r, pingOutput )
|
||
|
if m is None:
|
||
|
error( '*** Error: could not parse ping output: %s\n' %
|
||
|
pingOutput )
|
||
|
return errorTuple
|
||
|
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
|
||
|
r = r'rtt min/avg/max/mdev = '
|
||
|
r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms'
|
||
|
m = re.search( r, pingOutput )
|
||
|
if m is None:
|
||
|
if received == 0:
|
||
|
return errorTuple
|
||
|
error( '*** Error: could not parse ping output: %s\n' %
|
||
|
pingOutput )
|
||
|
return errorTuple
|
||
|
rttmin = float( m.group( 1 ) )
|
||
|
rttavg = float( m.group( 2 ) )
|
||
|
rttmax = float( m.group( 3 ) )
|
||
|
rttdev = float( m.group( 4 ) )
|
||
|
return sent, received, rttmin, rttavg, rttmax, rttdev
|
||
|
|
||
|
def pingFull( self, hosts=None, timeout=None ):
|
||
|
"""Ping between all specified hosts and return all data.
|
||
|
hosts: list of hosts
|
||
|
timeout: time to wait for a response, as string
|
||
|
returns: all ping data; see function body."""
|
||
|
# should we check if running?
|
||
|
# Each value is a tuple: (src, dsd, [all ping outputs])
|
||
|
all_outputs = []
|
||
|
if not hosts:
|
||
|
hosts = self.hosts
|
||
|
output( '*** Ping: testing ping reachability\n' )
|
||
|
for node in hosts:
|
||
|
output( '%s -> ' % node.name )
|
||
|
for dest in hosts:
|
||
|
if node != dest:
|
||
|
opts = ''
|
||
|
if timeout:
|
||
|
opts = '-W %s' % timeout
|
||
|
result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
|
||
|
outputs = self._parsePingFull( result )
|
||
|
sent, received, rttmin, rttavg, rttmax, rttdev = outputs
|
||
|
all_outputs.append( (node, dest, outputs) )
|
||
|
output( ( '%s ' % dest.name ) if received else 'X ' )
|
||
|
output( '\n' )
|
||
|
output( "*** Results: \n" )
|
||
|
for outputs in all_outputs:
|
||
|
src, dest, ping_outputs = outputs
|
||
|
sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs
|
||
|
output( " %s->%s: %s/%s, " % (src, dest, sent, received ) )
|
||
|
output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" %
|
||
|
(rttmin, rttavg, rttmax, rttdev) )
|
||
|
return all_outputs
|
||
|
|
||
|
def pingAll( self, timeout=None ):
|
||
|
"""Ping between all hosts.
|
||
|
returns: ploss packet loss percentage"""
|
||
|
return self.ping( timeout=timeout )
|
||
|
|
||
|
def pingPair( self ):
|
||
|
"""Ping between first two hosts, useful for testing.
|
||
|
returns: ploss packet loss percentage"""
|
||
|
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
|
||
|
return self.ping( hosts=hosts )
|
||
|
|
||
|
def pingAllFull( self ):
|
||
|
"""Ping between all hosts.
|
||
|
returns: ploss packet loss percentage"""
|
||
|
return self.pingFull()
|
||
|
|
||
|
def pingPairFull( self ):
|
||
|
"""Ping between first two hosts, useful for testing.
|
||
|
returns: ploss packet loss percentage"""
|
||
|
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
|
||
|
return self.pingFull( hosts=hosts )
|
||
|
|
||
|
@staticmethod
|
||
|
def _iperfVals( iperfcsv, serverip ):
|
||
|
"""Return iperf CSV as dict
|
||
|
iperfcsv: iperf -y C output
|
||
|
serverip: iperf server IP address
|
||
|
"""
|
||
|
fields = 'date cip cport sip sport ipver interval sent rate'
|
||
|
lines = iperfcsv.strip().split('\n')
|
||
|
svals = {}
|
||
|
for line in lines:
|
||
|
if ',' not in line:
|
||
|
continue
|
||
|
line = line.split( ',' )
|
||
|
svals = dict( zip( fields.split(), line ) )
|
||
|
# Return client in cip:cport, server in sip:sport
|
||
|
if svals[ 'cip' ] == serverip:
|
||
|
svals[ 'cip' ], svals[ 'sip' ] = (
|
||
|
svals[ 'sip' ], svals[ 'cip' ] )
|
||
|
svals[ 'cport' ], svals[ 'sport' ] = (
|
||
|
svals[ 'sport' ], svals[ 'cport' ] )
|
||
|
return svals
|
||
|
|
||
|
# XXX This should be cleaned up
|
||
|
|
||
|
def iperf( self, hosts=None, l4Type='TCP', udpBw='10M', fmt=None,
|
||
|
seconds=5, port=5001):
|
||
|
"""Run iperf between two hosts.
|
||
|
hosts: list of hosts; if None, uses first and last hosts
|
||
|
l4Type: string, one of [ TCP, UDP ]
|
||
|
udpBw: bandwidth target for UDP test
|
||
|
fmt: scale/format argument (e.g. m/M for Mbps)
|
||
|
seconds: iperf time to transmit
|
||
|
port: iperf port
|
||
|
returns: two-element array of [ server, client ] speeds
|
||
|
note: send() is buffered, so client rate can be much higher than
|
||
|
the actual transmission rate; on an unloaded system, server
|
||
|
rate should be much closer to the actual receive rate"""
|
||
|
hosts = hosts or [ self.hosts[ 0 ], self.hosts[ -1 ] ]
|
||
|
assert len( hosts ) == 2
|
||
|
client, server = hosts
|
||
|
output( '*** Iperf: testing', l4Type, 'bandwidth between',
|
||
|
client, 'and', server, '\n' )
|
||
|
server.cmd( 'killall -9 iperf' )
|
||
|
# Note: CSV mode
|
||
|
iperfArgs = 'iperf -y C -p %d ' % port
|
||
|
bwArgs = ''
|
||
|
if l4Type == 'UDP':
|
||
|
iperfArgs += '-u '
|
||
|
bwArgs = '-b ' + udpBw + ' '
|
||
|
server.sendCmd( iperfArgs + '-s' )
|
||
|
serverip = server.IP()
|
||
|
if l4Type == 'TCP':
|
||
|
if not waitListening( client, serverip, port ):
|
||
|
raise Exception( 'Could not connect to iperf on port %d'
|
||
|
% port )
|
||
|
cliout = client.cmd( iperfArgs + '-t %d -c ' % seconds +
|
||
|
server.IP() + ' ' + bwArgs )
|
||
|
cvals = self._iperfVals( cliout, serverip )
|
||
|
debug( 'iperf client output:', cliout, cvals )
|
||
|
serverout = ''
|
||
|
# Wait for output from the client session
|
||
|
while True:
|
||
|
serverout += server.monitor( timeoutms=5000 )
|
||
|
svals = self._iperfVals( serverout, serverip )
|
||
|
# Check for the client's source/output port
|
||
|
if ( svals and cvals[ 'sport' ] == svals[ 'sport' ]
|
||
|
and int( svals[ 'rate' ] ) > 0 ):
|
||
|
break
|
||
|
debug( 'iperf server output:', serverout, svals )
|
||
|
server.sendInt()
|
||
|
serverout += server.waitOutput()
|
||
|
result = [ fmtBps( svals[ 'rate'], fmt ),
|
||
|
fmtBps( cvals[ 'rate' ], fmt ) ]
|
||
|
if l4Type == 'UDP':
|
||
|
result.insert( 0, udpBw )
|
||
|
output( '*** Results: %s\n' % result )
|
||
|
return result
|
||
|
|
||
|
def runCpuLimitTest( self, cpu, duration=5 ):
|
||
|
"""run CPU limit test with 'while true' processes.
|
||
|
cpu: desired CPU fraction of each host
|
||
|
duration: test duration in seconds (integer)
|
||
|
returns a single list of measured CPU fractions as floats.
|
||
|
"""
|
||
|
pct = cpu * 100
|
||
|
info( '*** Testing CPU %.0f%% bandwidth limit\n' % pct )
|
||
|
hosts = self.hosts
|
||
|
cores = int( quietRun( 'nproc' ) )
|
||
|
# number of processes to run a while loop on per host
|
||
|
num_procs = int( ceil( cores * cpu ) )
|
||
|
pids = {}
|
||
|
for h in hosts:
|
||
|
pids[ h ] = []
|
||
|
for _core in range( num_procs ):
|
||
|
h.cmd( 'while true; do a=1; done &' )
|
||
|
pids[ h ].append( h.cmd( 'echo $!' ).strip() )
|
||
|
outputs = {}
|
||
|
time = {}
|
||
|
# get the initial cpu time for each host
|
||
|
for host in hosts:
|
||
|
outputs[ host ] = []
|
||
|
with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' %
|
||
|
host, 'r' ) as f:
|
||
|
time[ host ] = float( f.read() )
|
||
|
for _ in range( duration ):
|
||
|
sleep( 1 )
|
||
|
for host in hosts:
|
||
|
with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' %
|
||
|
host, 'r' ) as f:
|
||
|
readTime = float( f.read() )
|
||
|
outputs[ host ].append( ( ( readTime - time[ host ] )
|
||
|
/ 1000000000 ) / cores * 100 )
|
||
|
time[ host ] = readTime
|
||
|
for h, pids in pids.items():
|
||
|
for pid in pids:
|
||
|
h.cmd( 'kill -9 %s' % pid )
|
||
|
cpu_fractions = []
|
||
|
for _host, outputs in outputs.items():
|
||
|
for pct in outputs:
|
||
|
cpu_fractions.append( pct )
|
||
|
output( '*** Results: %s\n' % cpu_fractions )
|
||
|
return cpu_fractions
|
||
|
|
||
|
# BL: I think this can be rewritten now that we have
|
||
|
# a real link class.
|
||
|
def configLinkStatus( self, src, dst, status ):
|
||
|
"""Change status of src <-> dst links.
|
||
|
src: node name
|
||
|
dst: node name
|
||
|
status: string {up, down}"""
|
||
|
if src not in self.nameToNode:
|
||
|
error( 'src not in network: %s\n' % src )
|
||
|
elif dst not in self.nameToNode:
|
||
|
error( 'dst not in network: %s\n' % dst )
|
||
|
else:
|
||
|
src = self.nameToNode[ src ]
|
||
|
dst = self.nameToNode[ dst ]
|
||
|
connections = src.connectionsTo( dst )
|
||
|
if len( connections ) == 0:
|
||
|
error( 'src and dst not connected: %s %s\n' % ( src, dst) )
|
||
|
for srcIntf, dstIntf in connections:
|
||
|
result = srcIntf.ifconfig( status )
|
||
|
if result:
|
||
|
error( 'link src status change failed: %s\n' % result )
|
||
|
result = dstIntf.ifconfig( status )
|
||
|
if result:
|
||
|
error( 'link dst status change failed: %s\n' % result )
|
||
|
|
||
|
def interact( self ):
|
||
|
"Start network and run our simple CLI."
|
||
|
self.start()
|
||
|
result = CLI( self )
|
||
|
self.stop()
|
||
|
return result
|
||
|
|
||
|
inited = False
|
||
|
|
||
|
@classmethod
|
||
|
def init( cls ):
|
||
|
"Initialize Mininet"
|
||
|
if cls.inited:
|
||
|
return
|
||
|
ensureRoot()
|
||
|
fixLimits()
|
||
|
cls.inited = True
|
||
|
|
||
|
|
||
|
class MininetWithControlNet( Mininet ):
|
||
|
|
||
|
"""Control network support:
|
||
|
|
||
|
Create an explicit control network. Currently this is only
|
||
|
used/usable with the user datapath.
|
||
|
|
||
|
Notes:
|
||
|
|
||
|
1. If the controller and switches are in the same (e.g. root)
|
||
|
namespace, they can just use the loopback connection.
|
||
|
|
||
|
2. If we can get unix domain sockets to work, we can use them
|
||
|
instead of an explicit control network.
|
||
|
|
||
|
3. Instead of routing, we could bridge or use 'in-band' control.
|
||
|
|
||
|
4. Even if we dispense with this in general, it could still be
|
||
|
useful for people who wish to simulate a separate control
|
||
|
network (since real networks may need one!)
|
||
|
|
||
|
5. Basically nobody ever used this code, so it has been moved
|
||
|
into its own class.
|
||
|
|
||
|
6. Ultimately we may wish to extend this to allow us to create a
|
||
|
control network which every node's control interface is
|
||
|
attached to."""
|
||
|
|
||
|
def configureControlNetwork( self ):
|
||
|
"Configure control network."
|
||
|
self.configureRoutedControlNetwork()
|
||
|
|
||
|
# We still need to figure out the right way to pass
|
||
|
# in the control network location.
|
||
|
|
||
|
def configureRoutedControlNetwork( self, ip='192.168.123.1',
|
||
|
prefixLen=16 ):
|
||
|
"""Configure a routed control network on controller and switches.
|
||
|
For use with the user datapath only right now."""
|
||
|
controller = self.controllers[ 0 ]
|
||
|
info( controller.name + ' <->' )
|
||
|
cip = ip
|
||
|
snum = ipParse( ip )
|
||
|
for switch in self.switches:
|
||
|
info( ' ' + switch.name )
|
||
|
link = self.link( switch, controller, port1=0 )
|
||
|
sintf, cintf = link.intf1, link.intf2
|
||
|
switch.controlIntf = sintf
|
||
|
snum += 1
|
||
|
while snum & 0xff in [ 0, 255 ]:
|
||
|
snum += 1
|
||
|
sip = ipStr( snum )
|
||
|
cintf.setIP( cip, prefixLen )
|
||
|
sintf.setIP( sip, prefixLen )
|
||
|
controller.setHostRoute( sip, cintf )
|
||
|
switch.setHostRoute( cip, sintf )
|
||
|
info( '\n' )
|
||
|
info( '*** Testing control network\n' )
|
||
|
while not cintf.isUp():
|
||
|
info( '*** Waiting for', cintf, 'to come up\n' )
|
||
|
sleep( 1 )
|
||
|
for switch in self.switches:
|
||
|
while not sintf.isUp():
|
||
|
info( '*** Waiting for', sintf, 'to come up\n' )
|
||
|
sleep( 1 )
|
||
|
if self.ping( hosts=[ switch, controller ] ) != 0:
|
||
|
error( '*** Error: control network test failed\n' )
|
||
|
exit( 1 )
|
||
|
info( '\n' )
|