We add a new experimental feature to allow Mininet to run across a cluster of machines. This is currently implemented via a set mix-in classes that provide remote nodes that are implemented via a connection to a remote shell, and remote links which are tunnels across servers. In this preliminary implementation, both control and data connections are made via ssh, but this could change in the future. A MininetCluster class is provided which allows existing code to be used with minimal modification - all that is required is to provide a list of servers to use. A customizable placement algorithm may also be specified. An experimental CLI subclass is also provided to make it easier to examine node placement; status and links commands can also check whether nodes and tunnels are still running. Although this is an experimental feature, it does include a --cluster option to make it convenient to start up a Mininet simulation over a cluster, and a script to assist with setting up the prerequisite authentication via ssh key pairs. The cluster feature is preliminary and missing some obvious important features, such as parallel startup and multiple tunnel types, which we hope to add in the future.
349 lines
13 KiB
Python
Executable File
349 lines
13 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
Mininet runner
|
|
author: Brandon Heller (brandonh@stanford.edu)
|
|
|
|
To see options:
|
|
sudo mn -h
|
|
|
|
Example to pull custom params (topo, switch, etc.) from a file:
|
|
sudo mn --custom ~/mininet/custom/custom_example.py
|
|
"""
|
|
|
|
from optparse import OptionParser
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
# Fix setuptools' evil madness, and open up (more?) security holes
|
|
if 'PYTHONPATH' in os.environ:
|
|
sys.path = os.environ[ 'PYTHONPATH' ].split( ':' ) + sys.path
|
|
|
|
from mininet.clean import cleanup
|
|
from mininet.cli import CLI
|
|
from mininet.log import lg, LEVELS, info, debug, warn, error
|
|
from mininet.net import Mininet, MininetWithControlNet, VERSION
|
|
from mininet.node import ( Host, CPULimitedHost, Controller, OVSController,
|
|
NOX, RemoteController, DefaultController,
|
|
UserSwitch, OVSSwitch,
|
|
OVSLegacyKernelSwitch, IVSSwitch )
|
|
from mininet.nodelib import LinuxBridge
|
|
from mininet.link import Link, TCLink
|
|
from mininet.topo import SingleSwitchTopo, LinearTopo, SingleSwitchReversedTopo
|
|
from mininet.topolib import TreeTopo, TorusTopo
|
|
from mininet.util import custom, customConstructor
|
|
from mininet.util import buildTopo
|
|
|
|
from functools import partial
|
|
|
|
# Experimental! cluster edition prototype
|
|
from mininet.examples.cluster import ( MininetCluster, RemoteHost,
|
|
RemoteOVSSwitch, RemoteLink,
|
|
SwitchBinPlacer, RandomPlacer )
|
|
from mininet.examples.clustercli import DemoCLI as ClusterCLI
|
|
|
|
PLACEMENT = { 'block': SwitchBinPlacer, 'random': RandomPlacer }
|
|
|
|
# built in topologies, created only when run
|
|
TOPODEF = 'minimal'
|
|
TOPOS = { 'minimal': lambda: SingleSwitchTopo( k=2 ),
|
|
'linear': LinearTopo,
|
|
'reversed': SingleSwitchReversedTopo,
|
|
'single': SingleSwitchTopo,
|
|
'tree': TreeTopo,
|
|
'torus': TorusTopo }
|
|
|
|
SWITCHDEF = 'ovsk'
|
|
SWITCHES = { 'user': UserSwitch,
|
|
'ovs': OVSSwitch,
|
|
# Keep ovsk for compatibility with 2.0
|
|
'ovsk': OVSSwitch,
|
|
'ovsl': OVSLegacyKernelSwitch,
|
|
'ivs': IVSSwitch,
|
|
'lxbr': LinuxBridge }
|
|
|
|
HOSTDEF = 'proc'
|
|
HOSTS = { 'proc': Host,
|
|
'rt': custom( CPULimitedHost, sched='rt' ),
|
|
'cfs': custom( CPULimitedHost, sched='cfs' ) }
|
|
|
|
CONTROLLERDEF = 'default'
|
|
CONTROLLERS = { 'ref': Controller,
|
|
'ovsc': OVSController,
|
|
'nox': NOX,
|
|
'remote': RemoteController,
|
|
'default': DefaultController,
|
|
'none': lambda name: None }
|
|
|
|
LINKDEF = 'default'
|
|
LINKS = { 'default': Link,
|
|
'tc': TCLink }
|
|
|
|
|
|
# optional tests to run
|
|
TESTS = [ 'cli', 'build', 'pingall', 'pingpair', 'iperf', 'all', 'iperfudp',
|
|
'none' ]
|
|
|
|
ALTSPELLING = { 'pingall': 'pingAll',
|
|
'pingpair': 'pingPair',
|
|
'iperfudp': 'iperfUdp',
|
|
'iperfUDP': 'iperfUdp' }
|
|
|
|
|
|
def addDictOption( opts, choicesDict, default, name, helpStr=None ):
|
|
"""Convenience function to add choices dicts to OptionParser.
|
|
opts: OptionParser instance
|
|
choicesDict: dictionary of valid choices, must include default
|
|
default: default choice key
|
|
name: long option name
|
|
help: string"""
|
|
if default not in choicesDict:
|
|
raise Exception( 'Invalid default %s for choices dict: %s' %
|
|
( default, name ) )
|
|
if not helpStr:
|
|
helpStr = ( '|'.join( sorted( choicesDict.keys() ) ) +
|
|
'[,param=value...]' )
|
|
opts.add_option( '--' + name,
|
|
type='string',
|
|
default = default,
|
|
help = helpStr )
|
|
|
|
|
|
def version( *_args ):
|
|
"Print Mininet version and exit"
|
|
print "%s" % VERSION
|
|
exit()
|
|
|
|
class MininetRunner( object ):
|
|
"Build, setup, and run Mininet."
|
|
|
|
def __init__( self ):
|
|
"Init."
|
|
self.options = None
|
|
self.args = None # May be used someday for more CLI scripts
|
|
self.validate = None
|
|
|
|
self.parseArgs()
|
|
self.setup()
|
|
self.begin()
|
|
|
|
def setCustom( self, name, value ):
|
|
"Set custom parameters for MininetRunner."
|
|
if name in ( 'topos', 'switches', 'hosts', 'controllers' ):
|
|
# Update dictionaries
|
|
param = name.upper()
|
|
globals()[ param ].update( value )
|
|
elif name == 'validate':
|
|
# Add custom validate function
|
|
self.validate = value
|
|
else:
|
|
# Add or modify global variable or class
|
|
globals()[ name ] = value
|
|
|
|
def parseCustomFile( self, fileName ):
|
|
"Parse custom file and add params before parsing cmd-line options."
|
|
customs = {}
|
|
if os.path.isfile( fileName ):
|
|
execfile( fileName, customs, customs )
|
|
for name, val in customs.iteritems():
|
|
self.setCustom( name, val )
|
|
else:
|
|
raise Exception( 'could not find custom file: %s' % fileName )
|
|
|
|
def parseArgs( self ):
|
|
"""Parse command-line args and return options object.
|
|
returns: opts parse options dict"""
|
|
if '--custom' in sys.argv:
|
|
index = sys.argv.index( '--custom' )
|
|
if len( sys.argv ) > index + 1:
|
|
filename = sys.argv[ index + 1 ]
|
|
self.parseCustomFile( filename )
|
|
else:
|
|
raise Exception( 'Custom file name not found' )
|
|
|
|
desc = ( "The %prog utility creates Mininet network from the\n"
|
|
"command line. It can create parametrized topologies,\n"
|
|
"invoke the Mininet CLI, and run tests." )
|
|
|
|
usage = ( '%prog [options]\n'
|
|
'(type %prog -h for details)' )
|
|
|
|
opts = OptionParser( description=desc, usage=usage )
|
|
addDictOption( opts, SWITCHES, SWITCHDEF, 'switch' )
|
|
addDictOption( opts, HOSTS, HOSTDEF, 'host' )
|
|
addDictOption( opts, CONTROLLERS, CONTROLLERDEF, 'controller' )
|
|
addDictOption( opts, LINKS, LINKDEF, 'link' )
|
|
addDictOption( opts, TOPOS, TOPODEF, 'topo' )
|
|
|
|
opts.add_option( '--clean', '-c', action='store_true',
|
|
default=False, help='clean and exit' )
|
|
opts.add_option( '--custom', type='string', default=None,
|
|
help='read custom topo and node params from .py' +
|
|
'file' )
|
|
opts.add_option( '--test', type='choice', choices=TESTS,
|
|
default=TESTS[ 0 ],
|
|
help='|'.join( TESTS ) )
|
|
opts.add_option( '--xterms', '-x', action='store_true',
|
|
default=False, help='spawn xterms for each node' )
|
|
opts.add_option( '--ipbase', '-i', type='string', default='10.0.0.0/8',
|
|
help='base IP address for hosts' )
|
|
opts.add_option( '--mac', action='store_true',
|
|
default=False, help='automatically set host MACs' )
|
|
opts.add_option( '--arp', action='store_true',
|
|
default=False, help='set all-pairs ARP entries' )
|
|
opts.add_option( '--verbosity', '-v', type='choice',
|
|
choices=LEVELS.keys(), default = 'info',
|
|
help = '|'.join( LEVELS.keys() ) )
|
|
opts.add_option( '--innamespace', action='store_true',
|
|
default=False, help='sw and ctrl in namespace?' )
|
|
opts.add_option( '--listenport', type='int', default=6634,
|
|
help='base port for passive switch listening' )
|
|
opts.add_option( '--nolistenport', action='store_true',
|
|
default=False, help="don't use passive listening " +
|
|
"port")
|
|
opts.add_option( '--pre', type='string', default=None,
|
|
help='CLI script to run before tests' )
|
|
opts.add_option( '--post', type='string', default=None,
|
|
help='CLI script to run after tests' )
|
|
opts.add_option( '--pin', action='store_true',
|
|
default=False, help="pin hosts to CPU cores "
|
|
"(requires --host cfs or --host rt)" )
|
|
opts.add_option( '--nat', action='store_true',
|
|
default=False, help="adds a NAT to the topology "
|
|
"that connects Mininet to the physical network" )
|
|
opts.add_option( '--version', action='callback', callback=version )
|
|
opts.add_option( '--cluster', type='string', default=None,
|
|
metavar='server1,server2...',
|
|
help=( 'run on multiple servers (experimental!)' ) )
|
|
opts.add_option( '--placement', type='choice',
|
|
choices=PLACEMENT.keys(), default='block',
|
|
metavar='block|random',
|
|
help=( 'node placement for --cluster '
|
|
'(experimental!) ' ) )
|
|
|
|
self.options, self.args = opts.parse_args()
|
|
|
|
# We don't accept extra arguments after the options
|
|
if self.args:
|
|
opts.print_help()
|
|
exit()
|
|
|
|
def setup( self ):
|
|
"Setup and validate environment."
|
|
|
|
# set logging verbosity
|
|
if LEVELS[self.options.verbosity] > LEVELS['output']:
|
|
print ( '*** WARNING: selected verbosity level (%s) will hide CLI '
|
|
'output!\n'
|
|
'Please restart Mininet with -v [debug, info, output].'
|
|
% self.options.verbosity )
|
|
lg.setLogLevel( self.options.verbosity )
|
|
|
|
def begin( self ):
|
|
"Create and run mininet."
|
|
|
|
global CLI
|
|
|
|
if self.options.clean:
|
|
cleanup()
|
|
exit()
|
|
|
|
start = time.time()
|
|
|
|
topo = buildTopo( TOPOS, self.options.topo )
|
|
switch = customConstructor( SWITCHES, self.options.switch )
|
|
host = customConstructor( HOSTS, self.options.host )
|
|
controller = customConstructor( CONTROLLERS, self.options.controller )
|
|
link = customConstructor( LINKS, self.options.link )
|
|
|
|
if self.validate:
|
|
self.validate( self.options )
|
|
|
|
ipBase = self.options.ipbase
|
|
xterms = self.options.xterms
|
|
mac = self.options.mac
|
|
arp = self.options.arp
|
|
pin = self.options.pin
|
|
listenPort = None
|
|
if not self.options.nolistenport:
|
|
listenPort = self.options.listenport
|
|
|
|
# Handle inNamespace, cluster options
|
|
inNamespace = self.options.innamespace
|
|
cluster = self.options.cluster
|
|
if inNamespace and cluster:
|
|
print "Please specify --innamespace OR --cluster"
|
|
exit()
|
|
Net = MininetWithControlNet if inNamespace else Mininet
|
|
if cluster:
|
|
warn( '*** WARNING: Experimental cluster mode!\n'
|
|
'*** Using RemoteHost, RemoteOVSSwitch, RemoteLink\n' )
|
|
host, switch, link = RemoteHost, RemoteOVSSwitch, RemoteLink
|
|
CLI = ClusterCLI
|
|
Net = partial( MininetCluster, servers=cluster.split( ',' ),
|
|
placement=PLACEMENT[ self.options.placement ] )
|
|
|
|
mn = Net( topo=topo,
|
|
switch=switch, host=host, controller=controller,
|
|
link=link,
|
|
ipBase=ipBase,
|
|
inNamespace=inNamespace,
|
|
xterms=xterms, autoSetMacs=mac,
|
|
autoStaticArp=arp, autoPinCpus=pin,
|
|
listenPort=listenPort )
|
|
|
|
if self.options.nat:
|
|
nat = mn.addNAT()
|
|
nat.configDefault()
|
|
|
|
if self.options.pre:
|
|
CLI( mn, script=self.options.pre )
|
|
|
|
test = self.options.test
|
|
test = ALTSPELLING.get( test, test )
|
|
|
|
mn.start()
|
|
|
|
if test == 'none':
|
|
pass
|
|
elif test == 'all':
|
|
mn.waitConnected()
|
|
mn.start()
|
|
mn.ping()
|
|
mn.iperf()
|
|
elif test == 'cli':
|
|
CLI( mn )
|
|
elif test != 'build':
|
|
mn.waitConnected()
|
|
getattr( mn, test )()
|
|
|
|
if self.options.post:
|
|
CLI( mn, script=self.options.post )
|
|
|
|
mn.stop()
|
|
|
|
elapsed = float( time.time() - start )
|
|
info( 'completed in %0.3f seconds\n' % elapsed )
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
MininetRunner()
|
|
except KeyboardInterrupt:
|
|
info( "\n\nKeyboard Interrupt. Shutting down and cleaning up...\n\n")
|
|
cleanup()
|
|
except Exception:
|
|
# Print exception
|
|
type_, val_, trace_ = sys.exc_info()
|
|
errorMsg = ( "-"*80 + "\n" +
|
|
"Caught exception. Cleaning up...\n\n" +
|
|
"%s: %s\n" % ( type_.__name__, val_ ) +
|
|
"-"*80 + "\n" )
|
|
error( errorMsg )
|
|
# Print stack trace to debug log
|
|
import traceback
|
|
stackTrace = traceback.format_exc()
|
|
debug( stackTrace + "\n" )
|
|
cleanup()
|