5 author: Brandon Heller (brandonh@stanford.edu)
10 Example to pull custom params (topo, switch, etc.) from a file:
11 sudo mn --custom ~/mininet/custom/custom_example.py
14 from optparse import OptionParser
19 # Fix setuptools' evil madness, and open up (more?) security holes
20 if 'PYTHONPATH' in os.environ:
21 sys.path = os.environ[ 'PYTHONPATH' ].split( ':' ) + sys.path
23 from mininet.clean import cleanup
25 from mininet.log import lg, LEVELS, info, debug, warn, error, output
26 from mininet.net import Mininet, MininetWithControlNet, VERSION
27 from mininet.node import ( Host, CPULimitedHost, Controller, OVSController,
28 Ryu, NOX, RemoteController, findController,
29 DefaultController, NullController,
30 UserSwitch, OVSSwitch, OVSBridge,
32 from mininet.nodelib import LinuxBridge
33 from mininet.link import Link, TCLink, TCULink, OVSLink
34 from mininet.topo import ( SingleSwitchTopo, LinearTopo,
35 SingleSwitchReversedTopo, MinimalTopo )
36 from mininet.topolib import TreeTopo, TorusTopo
37 from mininet.util import customClass, specialClass, splitArgs
38 from mininet.util import buildTopo
40 from functools import partial
42 # Experimental! cluster edition prototype
43 from mininet.examples.cluster import ( MininetCluster, RemoteHost,
44 RemoteOVSSwitch, RemoteLink,
45 SwitchBinPlacer, RandomPlacer,
47 from mininet.examples.clustercli import ClusterCLI
49 PLACEMENT = { 'block': SwitchBinPlacer, 'random': RandomPlacer }
51 # built in topologies, created only when run
53 TOPOS = { 'minimal': MinimalTopo,
55 'reversed': SingleSwitchReversedTopo,
56 'single': SingleSwitchTopo,
61 SWITCHES = { 'user': UserSwitch,
64 # Keep ovsk for compatibility with 2.0
68 'default': OVSSwitch }
71 HOSTS = { 'proc': Host,
72 'rt': specialClass( CPULimitedHost, defaults=dict( sched='rt' ) ),
73 'cfs': specialClass( CPULimitedHost, defaults=dict( sched='cfs' ) ) }
75 CONTROLLERDEF = 'default'
76 CONTROLLERS = { 'ref': Controller,
77 'ovsc': OVSController,
79 'remote': RemoteController,
81 'default': DefaultController, # Note: overridden below
82 'none': NullController }
85 LINKS = { 'default': Link, # Note: overridden below
90 # TESTS dict can contain functions and/or Mininet() method names
91 # XXX: it would be nice if we could specify a default test, but
94 for name in ( 'pingall', 'pingpair', 'iperf', 'iperfudp' ) }
96 CLI = None # Set below if needed
98 # Locally defined tests
100 "Run ping and iperf tests"
106 def nullTest( _net ):
107 "Null 'test' (does nothing)"
110 TESTS.update( all=allTest, none=nullTest, build=nullTest )
112 # Map to alternate spellings of Mininet() methods
113 ALTSPELLING = { 'pingall': 'pingAll', 'pingpair': 'pingPair',
114 'iperfudp': 'iperfUdp' }
116 def runTests( mn, options ):
119 option: list of test optinos """
120 # Split option into test name and parameters
121 for option in options:
122 # Multiple tests may be separated by '+' for now
123 for test in option.split( '+' ):
124 test, args, kwargs = splitArgs( test )
125 test = ALTSPELLING.get( test.lower(), test )
126 testfn = TESTS.get( test, test )
127 if callable( testfn ):
128 testfn( mn, *args, **kwargs )
129 elif hasattr( mn, test ):
131 getattr( mn, test )( *args, **kwargs )
133 raise Exception( 'Test %s is unknown - please specify one of '
134 '%s ' % ( test, TESTS.keys() ) )
137 def addDictOption( opts, choicesDict, default, name, **kwargs ):
138 """Convenience function to add choices dicts to OptionParser.
139 opts: OptionParser instance
140 choicesDict: dictionary of valid choices, must include default
141 default: default choice key
142 name: long option name
143 kwargs: additional arguments to add_option"""
144 helpStr = ( '|'.join( sorted( choicesDict.keys() ) ) +
145 '[,param=value...]' )
146 helpList = [ '%s=%s' % ( k, v.__name__ )
147 for k, v in choicesDict.items() ]
148 helpStr += ' ' + ( ' '.join( helpList ) )
149 params = dict( type='string', default=default, help=helpStr )
150 params.update( **kwargs )
151 opts.add_option( '--' + name, **params )
153 def version( *_args ):
154 "Print Mininet version and exit"
155 output( "%s\n" % VERSION )
159 class MininetRunner( object ):
160 "Build, setup, and run Mininet."
162 def __init__( self ):
165 self.args = None # May be used someday for more CLI scripts
172 def custom( self, _option, _opt_str, value, _parser ):
173 """Parse custom file and add params.
174 option: option e.g. --custom
175 opt_str: option string e.g. --custom
176 value: the value the follows the option
177 parser: option parser instance"""
179 if os.path.isfile( value ):
180 # Accept any single file (including those with commas)
181 files.append( value )
183 # Accept a comma-separated list of filenames
184 files += value.split(',')
186 for fileName in files:
188 if os.path.isfile( fileName ):
189 # pylint: disable=exec-used
190 exec( compile( open( fileName ).read(), fileName, 'exec' ),
192 for name, val in customs.items():
193 self.setCustom( name, val )
195 raise Exception( 'could not find custom file: %s' % fileName )
197 def setCustom( self, name, value ):
198 "Set custom parameters for MininetRunner."
199 if name in ( 'topos', 'switches', 'hosts', 'controllers', 'links'
200 'testnames', 'tests' ):
201 # Update dictionaries
203 globals()[ param ].update( value )
204 elif name == 'validate':
205 # Add custom validate function
206 self.validate = value
208 # Add or modify global variable or class
209 globals()[ name ] = value
211 def setNat( self, _option, opt_str, value, parser ):
213 assert self # satisfy pylint
214 parser.values.nat = True
215 # first arg, first char != '-'
216 if parser.rargs and parser.rargs[ 0 ][ 0 ] != '-':
217 value = parser.rargs.pop( 0 )
218 _, args, kwargs = splitArgs( opt_str + ',' + value )
219 parser.values.nat_args = args
220 parser.values.nat_kwargs = kwargs
222 parser.values.nat_args = []
223 parser.values.nat_kwargs = {}
225 def parseArgs( self ):
226 """Parse command-line args and return options object.
227 returns: opts parse options dict"""
229 desc = ( "The %prog utility creates Mininet network from the\n"
230 "command line. It can create parametrized topologies,\n"
231 "invoke the Mininet CLI, and run tests." )
233 usage = ( '%prog [options]\n'
234 '(type %prog -h for details)' )
236 opts = OptionParser( description=desc, usage=usage )
237 addDictOption( opts, SWITCHES, SWITCHDEF, 'switch' )
238 addDictOption( opts, HOSTS, HOSTDEF, 'host' )
239 addDictOption( opts, CONTROLLERS, [], 'controller', action='append' )
240 addDictOption( opts, LINKS, LINKDEF, 'link' )
241 addDictOption( opts, TOPOS, TOPODEF, 'topo' )
243 opts.add_option( '--clean', '-c', action='store_true',
244 default=False, help='clean and exit' )
245 opts.add_option( '--custom', action='callback',
246 callback=self.custom,
248 help='read custom classes or params from .py file(s)'
250 opts.add_option( '--test', default=[], action='append',
251 dest='test', help='|'.join( TESTS.keys() ) )
252 opts.add_option( '--xterms', '-x', action='store_true',
253 default=False, help='spawn xterms for each node' )
254 opts.add_option( '--ipbase', '-i', type='string', default='10.0.0.0/8',
255 help='base IP address for hosts' )
256 opts.add_option( '--mac', action='store_true',
257 default=False, help='automatically set host MACs' )
258 opts.add_option( '--arp', action='store_true',
259 default=False, help='set all-pairs ARP entries' )
260 opts.add_option( '--verbosity', '-v', type='choice',
261 choices=list( LEVELS.keys() ), default = 'info',
262 help = '|'.join( LEVELS.keys() ) )
263 opts.add_option( '--innamespace', action='store_true',
264 default=False, help='sw and ctrl in namespace?' )
265 opts.add_option( '--listenport', type='int', default=6654,
266 help='base port for passive switch listening' )
267 opts.add_option( '--nolistenport', action='store_true',
268 default=False, help="don't use passive listening " +
270 opts.add_option( '--pre', type='string', default=None,
271 help='CLI script to run before tests' )
272 opts.add_option( '--post', type='string', default=None,
273 help='CLI script to run after tests' )
274 opts.add_option( '--pin', action='store_true',
275 default=False, help="pin hosts to CPU cores "
276 "(requires --host cfs or --host rt)" )
277 opts.add_option( '--nat', action='callback', callback=self.setNat,
278 help="[option=val...] adds a NAT to the topology that"
279 " connects Mininet hosts to the physical network."
280 " Warning: This may route any traffic on the machine"
281 " that uses Mininet's"
282 " IP subnet into the Mininet network."
283 " If you need to change"
284 " Mininet's IP subnet, see the --ipbase option." )
285 opts.add_option( '--version', action='callback', callback=version,
286 help='prints the version and exits' )
287 opts.add_option( '--cluster', type='string', default=None,
288 metavar='server1,server2...',
289 help=( 'run on multiple servers (experimental!)' ) )
290 opts.add_option( '--placement', type='choice',
291 choices=list( PLACEMENT.keys() ), default='block',
292 metavar='block|random',
293 help=( 'node placement for --cluster '
294 '(experimental!) ' ) )
296 self.options, self.args = opts.parse_args()
298 # We don't accept extra arguments after the options
304 "Setup and validate environment."
306 # set logging verbosity
307 if LEVELS[self.options.verbosity] > LEVELS['output']:
308 warn( '*** WARNING: selected verbosity level (%s) will hide CLI '
310 'Please restart Mininet with -v [debug, info, output].\n'
311 % self.options.verbosity )
312 lg.setLogLevel( self.options.verbosity )
314 # Maybe we'll reorganize this someday...
315 # pylint: disable=too-many-branches,too-many-statements,global-statement
318 "Create and run mininet."
325 servers = opts.cluster.split( ',' )
326 for server in servers:
327 ClusterCleanup.add( server )
335 if not opts.controller:
336 # Update default based on available controllers
337 CONTROLLERS[ 'default' ] = findController()
338 opts.controller = [ 'default' ]
339 if not CONTROLLERS[ 'default' ]:
340 opts.controller = [ 'none' ]
341 if opts.switch == 'default':
342 info( '*** No default OpenFlow controller found '
343 'for default switch!\n' )
344 info( '*** Falling back to OVS Bridge\n' )
345 opts.switch = 'ovsbr'
346 elif opts.switch not in ( 'ovsbr', 'lxbr' ):
347 raise Exception( "Could not find a default controller "
351 topo = buildTopo( TOPOS, opts.topo )
352 switch = customClass( SWITCHES, opts.switch )
353 host = customClass( HOSTS, opts.host )
354 controller = [ customClass( CONTROLLERS, c )
355 for c in opts.controller ]
357 if opts.switch == 'user' and opts.link == 'default':
358 debug( '*** Using TCULink with UserSwitch\n' )
359 # Use link configured correctly for UserSwitch
362 link = customClass( LINKS, opts.link )
365 self.validate( opts )
367 if opts.nolistenport:
368 opts.listenport = None
370 # Handle innamespace, cluster options
371 if opts.innamespace and opts.cluster:
372 error( "Please specify --innamespace OR --cluster\n" )
374 Net = MininetWithControlNet if opts.innamespace else Mininet
376 warn( '*** WARNING: Experimental cluster mode!\n'
377 '*** Using RemoteHost, RemoteOVSSwitch, RemoteLink\n' )
378 host, switch, link = RemoteHost, RemoteOVSSwitch, RemoteLink
379 Net = partial( MininetCluster, servers=servers,
380 placement=PLACEMENT[ opts.placement ] )
381 mininet.cli.CLI = ClusterCLI
384 switch=switch, host=host, controller=controller, link=link,
385 ipBase=opts.ipbase, inNamespace=opts.innamespace,
386 xterms=opts.xterms, autoSetMacs=opts.mac,
387 autoStaticArp=opts.arp, autoPinCpus=opts.pin,
388 listenPort=opts.listenport )
390 if opts.ensure_value( 'nat', False ):
391 with open( '/etc/resolv.conf' ) as f:
392 if 'nameserver 127.' in f.read():
393 warn( '*** Warning: loopback address in /etc/resolv.conf '
394 'may break host DNS over NAT\n')
395 mn.addNAT( *opts.nat_args, **opts.nat_kwargs ).configDefault()
397 # --custom files can set CLI or change mininet.cli.CLI
398 CLI = mininet.cli.CLI if CLI is None else CLI
401 CLI( mn, script=opts.pre )
406 runTests( mn, opts.test )
411 CLI( mn, script=opts.post )
415 elapsed = float( time.time() - start )
416 info( 'completed in %0.3f seconds\n' % elapsed )
419 if __name__ == "__main__":
422 except KeyboardInterrupt:
423 info( "\n\nKeyboard Interrupt. Shutting down and cleaning up...\n\n")
427 type_, val_, trace_ = sys.exc_info()
428 errorMsg = ( "-"*80 + "\n" +
429 "Caught exception. Cleaning up...\n\n" +
430 "%s: %s\n" % ( type_.__name__, val_ ) +
433 # Print stack trace to debug log
435 stackTrace = traceback.format_exc()
436 debug( stackTrace + "\n" )