diff --git a/bin/mn b/bin/mn index b2323891b1205c4239d23ef5b9549fa11bd70459..7eab6c9125fd4fe472dc5e3308af78145d2b2ee5 100755 --- a/bin/mn +++ b/bin/mn @@ -20,11 +20,27 @@ from mininet.clean import cleanup from mininet.cli import CLI from mininet.log import lg, LEVELS, info from mininet.net import Mininet, init -from mininet.node import Host, Controller, ControllerParams, NOX +from mininet.node import Host, CPULimitedHost, Controller, NOX from mininet.node import RemoteController, UserSwitch, OVSKernelSwitch +from mininet.link import Intf, TCIntf from mininet.topo import SingleSwitchTopo, LinearTopo, SingleSwitchReversedTopo from mininet.topolib import TreeTopo -from mininet.util import makeNumeric +from mininet.util import makeNumeric, custom + +def customNode( constructors, argStr ): + "Return custom Node constructor based on argStr" + cname, noargs, kwargs = splitArgs( argStr ) + constructor = constructors.get( cname, None ) + if noargs: + raise Exception( "please specify keyword arguments for " + cname ) + if not constructor: + raise Exception( "error: %s is unknown - please specify one of %s" % + ( cname, constructors.keys() ) ) + def custom( *args, **params ): + params.update( kwargs ) + print 'CONSTRUCTOR', constructor, 'ARGS', args, 'PARAMS', params + return constructor( *args, **params ) + return custom # built in topologies, created only when run TOPODEF = 'minimal' @@ -38,17 +54,22 @@ SWITCHDEF = 'ovsk' SWITCHES = { 'user': UserSwitch, 'ovsk': OVSKernelSwitch } -HOSTDEF = 'process' -HOSTS = { 'process': Host } +HOSTDEF = 'proc' +HOSTS = { 'proc': Host, + 'rt': custom( CPULimitedHost, sched='rt' ), + 'cfs': custom( CPULimitedHost, sched='cfs' ) } CONTROLLERDEF = 'ref' -# a and b are the name and inNamespace params. CONTROLLERS = { 'ref': Controller, - 'nox_dump': lambda name: NOX( name, 'packetdump' ), - 'nox_pysw': lambda name: NOX( name, 'pyswitch' ), - 'remote': lambda name: None, + 'nox': NOX, + 'remote': RemoteController, 'none': lambda name: None } +INTFDEF = 'default' +INTFS = { 'default': Intf, + 'tc': TCIntf } + + # optional tests to run TESTS = [ 'cli', 'build', 'pingall', 'pingpair', 'iperf', 'all', 'iperfudp', 'none' ] @@ -56,24 +77,31 @@ TESTS = [ 'cli', 'build', 'pingall', 'pingpair', 'iperf', 'all', 'iperfudp', ALTSPELLING = { 'pingall': 'pingAll', 'pingpair': 'pingPair', 'iperfudp': 'iperfUdp', 'iperfUDP': 'iperfUdp', 'prefixlen': 'prefixLen' } -def buildTopo( topo ): - "Create topology from string with format (object, arg1, arg2,...)." - topo_split = topo.split( ',' ) - topo_name = topo_split[ 0 ] - topo_params = topo_split[ 1: ] - - # Convert int and float args; removes the need for every topology to - # be flexible with input arg formats. - topo_seq_params = [ s for s in topo_params if '=' not in s ] - topo_seq_params = [ makeNumeric( s ) for s in topo_seq_params ] - topo_kw_params = {} - for s in [ p for p in topo_params if '=' in p ]: + +def splitArgs( argstr ): + """Split argument string into usable python arguments + argstr: argument string with format fn,arg2,kw1=arg3... + returns: fn, args, kwargs""" + split = argstr.split( ',' ) + fn = split[ 0 ] + params = split[ 1: ] + # Convert int and float args; removes the need for function + # to be flexible with input arg formats. + args = [ s for s in params if '=' not in s ] + args = map( makeNumeric, args ) + kwargs = {} + for s in [ p for p in params if '=' in p ]: key, val = s.split( '=' ) - topo_kw_params[ key ] = makeNumeric( val ) + kwargs[ key ] = makeNumeric( val ) + return fn, args, kwargs - if topo_name not in TOPOS.keys(): - raise Exception( 'Invalid topo_name %s' % topo_name ) - return TOPOS[ topo_name ]( *topo_seq_params, **topo_kw_params ) + +def buildTopo( topoStr ): + "Create topology from string with format (object, arg1, arg2,...)." + topo, args, kwargs = splitArgs( topoStr ) + if topo not in TOPOS: + raise Exception( 'Invalid topo name %s' % topo ) + return TOPOS[ topo ]( *args, **kwargs ) def addDictOption( opts, choicesDict, default, name, helpStr=None ): @@ -87,10 +115,9 @@ def addDictOption( opts, choicesDict, default, name, helpStr=None ): raise Exception( 'Invalid default %s for choices dict: %s' % ( default, name ) ) if not helpStr: - helpStr = '[' + ' '.join( choicesDict.keys() ) + ']' + helpStr = '|'.join( sorted( choicesDict.keys() ) ) + '[,param=value...]' opts.add_option( '--' + name, - type='choice', - choices=choicesDict.keys(), + type='string', default = default, help = helpStr ) @@ -135,7 +162,6 @@ class MininetRunner( object ): """Parse command-line args and return options object. returns: opts parse options dict""" if '--custom' in sys.argv: - print "custom in sys.argv" index = sys.argv.index( '--custom' ) if len( sys.argv ) > index + 1: custom = sys.argv[ index + 1 ] @@ -147,46 +173,41 @@ class MininetRunner( object ): addDictOption( opts, SWITCHES, SWITCHDEF, 'switch' ) addDictOption( opts, HOSTS, HOSTDEF, 'host' ) addDictOption( opts, CONTROLLERS, CONTROLLERDEF, 'controller' ) + addDictOption( opts, INTFS, INTFDEF, 'intf' ) + addDictOption( opts, TOPOS, TOPODEF, 'topo' ) - opts.add_option( '--topo', type='string', default=TOPODEF, - help='[' + ' '.join( TOPOS.keys() ) + '],arg1,arg2,' - '...argN') 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 ) + ']' ) + help='|'.join( TESTS ) ) opts.add_option( '--xterms', '-x', action='store_true', default=False, help='spawn xterms for each node' ) opts.add_option( '--mac', action='store_true', - default=False, help='set MACs equal to DPIDs' ) + 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() ) + ']' ) + help = '|'.join( LEVELS.keys() ) ) opts.add_option( '--ip', type='string', default='127.0.0.1', - help='[ip address as a dotted decimal string for a' - 'remote controller]' ) - opts.add_option( '--port', type='int', default=6633, - help='[port integer for a listening remote' - ' controller]' ) + help='ip address as a dotted decimal string for a' + 'remote controller' ) 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' - ' controller]' ) + 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]' ) + help='CLI script to run before tests' ) opts.add_option( '--post', type='string', default=None, - help='[CLI script to run after tests]' ) + help='CLI script to run after tests' ) opts.add_option( '--prefixlen', type='int', default=8, - help='[prefix length (e.g. /8) for automatic ' - 'network configuration]' ) + help='prefix length (e.g. /8) for automatic ' + 'network configuration' ) self.options, self.args = opts.parse_args() @@ -214,23 +235,14 @@ class MininetRunner( object ): start = time.time() topo = buildTopo( self.options.topo ) - switch = SWITCHES[ self.options.switch ] - host = HOSTS[ self.options.host ] - controller = CONTROLLERS[ self.options.controller ] - if self.options.controller == 'remote': - controller = lambda a: RemoteController( a, - defaultIP=self.options.ip, - port=self.options.port ) + switch = customNode( SWITCHES, self.options.switch ) + host = customNode( HOSTS, self.options.host ) + controller = customNode( CONTROLLERS, self.options.controller ) + intf = customNode( INTFS, self.options.intf ) if self.validate: self.validate( self.options ) - # We should clarify what this is actually for... - # It seems like it should be default values for the - # *data* network, so it may be misnamed. - controllerParams = ControllerParams( '10.0.0.0', - self.options.prefixlen) - inNamespace = self.options.innamespace xterms = self.options.xterms mac = self.options.mac @@ -238,10 +250,12 @@ class MininetRunner( object ): listenPort = None if not self.options.nolistenport: listenPort = self.options.listenport - mn = Mininet( topo, switch, host, controller, controllerParams, - inNamespace=inNamespace, - xterms=xterms, autoSetMacs=mac, - autoStaticArp=arp, listenPort=listenPort ) + mn = Mininet( topo=topo, + switch=switch, host=host, controller=controller, + intf=intf, + inNamespace=inNamespace, + xterms=xterms, autoSetMacs=mac, + autoStaticArp=arp, listenPort=listenPort ) if self.options.pre: CLI( mn, script=self.options.pre ) diff --git a/examples/limit.py b/examples/limit.py index 58e2ff78babca4045e52be756ab06e2b5821fc5c..801159af6c0989f6149d4babd6d651f7cc7902f4 100755 --- a/examples/limit.py +++ b/examples/limit.py @@ -5,19 +5,20 @@ """ from mininet.net import Mininet -from mininet.link import TCIntf, Link +from mininet.link import TCIntf from mininet.node import CPULimitedHost from mininet.topolib import TreeTopo from mininet.util import custom, quietRun from mininet.log import setLogLevel from time import sleep -def testLinkLimit( net ): - print '*** Testing network bandwidth limit' - net.iperf() +def testLinkLimit( net, bw ): + print '*** Testing network %.2f Mbps bandwidth limit' % bw + net.iperf( ) -def testCpuLimit( net ): - print '*** Testing CPU bandwidth limit' +def testCpuLimit( net, cpu ): + pct = cpu * 100 + print '*** Testing CPU %.0f%% bandwidth limit' % pct h1, h2 = net.hosts h1.cmd( 'while true; do a=1; done &' ) h2.cmd( 'while true; do a=1; done &' ) @@ -26,26 +27,24 @@ def testCpuLimit( net ): cmd = 'ps -p %s,%s -o pid,%%cpu,args' % ( pid1, pid2 ) for i in range( 0, 5): sleep( 1 ) - print quietRun( cmd ) + print quietRun( cmd ).strip() h1.cmd( 'kill %1') h2.cmd( 'kill %1') -def limit(): - "Example/test of link and CPU bandwidth limits" - # 1 Mbps interfaces limited using tc - intf1Mbps = custom( TCIntf, bw=1 ) - # Links consisting of two 10 Mbps interfaces - link1Mbps = custom( Link, intf=intf1Mbps, cls2=TCIntf ) - # Hosts with 30% of system bandwidth - host30pct = custom( CPULimitedHost, cpu=.3 ) +def limit( bw=1, cpu=.3 ): + """Example/test of link and CPU bandwidth limits + bw: interface bandwidth limit in Mbps + cpu: cpu limit as fraction of overall CPU time""" + intf = custom( TCIntf, bw=1 ) myTopo = TreeTopo( depth=1, fanout=2 ) - net = Mininet( topo=myTopo, - link=link1Mbps, - host=host30pct ) - net.start() - testLinkLimit( net ) - testCpuLimit( net ) - net.stop() + for sched in 'rt', 'cfs': + print '*** Testing with', sched, 'bandwidth limiting' + host = custom( CPULimitedHost, sched=sched, cpu=cpu ) + net = Mininet( topo=myTopo, intf=intf, host=host ) + net.start() + testLinkLimit( net, bw=bw ) + testCpuLimit( net, cpu=cpu ) + net.stop() if __name__ == '__main__': setLogLevel( 'info' ) diff --git a/mininet/net.py b/mininet/net.py index 18d03633630508a491c4561bc8c5798eb8b2dd33..953a5bc65e727b2b41a1fb4bd8880e4c20300251 100755 --- a/mininet/net.py +++ b/mininet/net.py @@ -104,7 +104,7 @@ class Mininet( object ): "Network emulation with hosts spawned in network namespaces." def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, - controller=Controller, link=Link, + controller=Controller, link=Link, intf=None, build=True, xterms=False, cleanup=False, inNamespace=False, autoSetMacs=False, autoStaticArp=False, listenPort=None ): @@ -114,6 +114,7 @@ def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, 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? @@ -123,11 +124,12 @@ def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, autoStaticArp: set all-pairs static MAC addrs? listenPort: base listening port to open; will be incremented for each additional switch in the net if inNamespace=False""" + self.topo = topo self.switch = switch self.host = host self.controller = controller self.link = link - self.topo = topo + self.intf = intf self.inNamespace = inNamespace self.xterms = xterms self.cleanup = cleanup @@ -199,12 +201,12 @@ def addController( self, name='c0', controller=None, **params ): def configHosts( self ): "Configure a set of hosts." for host in self.hosts: + info( host.name + ' ' ) host.configDefault( defaultRoute=host.defaultIntf ) # You're low priority, dude! # BL: do we want to do this here or not? # May not make sense if we have CPU lmiting... # quietRun( 'renice +18 -p ' + repr( host.pid ) ) - info( host.name + ' ' ) info( '\n' ) def buildFromTopo( self, topo=None ): @@ -235,6 +237,8 @@ def addLink( srcId, dstId, link=None ): ei = topo.edgeInfo( srcId, dstId ) link = getattr( ei, 'cls', link ) params = ei.params + if self.intf and not 'intf' in params: + params[ 'intf' ] = self.intf if not link: link = self.link info( '(%s, %s) ' % ( src.name, dst.name ) ) @@ -447,6 +451,8 @@ def _parseIperf( iperfOutput ): error( 'could not parse iperf output: ' + iperfOutput ) return '' + # XXX This should be cleaned up + def iperf( self, hosts=None, l4Type='TCP', udpBw='10M' ): """Run iperf between two hosts. hosts: list of hosts; if None, uses opposite hosts diff --git a/mininet/node.py b/mininet/node.py index 68a843ed6c2c8c9dfd89379240fcce32731a8b79..f78ebe97d841091c46329009f244f3930650b13e 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -127,9 +127,12 @@ def startShell( self ): if self.shell: error( "%s: shell is already running" ) return + # mnexec: (c)lose descriptors, (d)etach from tty, + # (p)rint pid, and run in (n)amespace opts = '-cdp' if self.inNamespace: opts += 'n' + # bash -m: enable job control cmd = [ 'mnexec', opts, 'bash', '-m' ] self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True ) @@ -405,10 +408,14 @@ def intfIsUp( self, intf=None ): # annoying, but at least the information is there! def setParam( self, results, method, **param ): - """Internal method: configure single parameter""" + """Internal method: configure a *single* parameter + results: dict of results to update + method: config method name + param: arg=value (ignore if value=None) + value may also be list or dict""" name, value = param.items()[ 0 ] f = getattr( self, method, None ) - if not value or not f: + if not f or value is None: return if type( value ) is list: result = f( *value ) @@ -417,6 +424,7 @@ def setParam( self, results, method, **param ): else: result = f( value ) results[ name ] = result + return result def config( self, mac=None, ip=None, ifconfig=None, defaultRoute=None, **params): @@ -462,7 +470,12 @@ def __str__( self ): self.name, self.IP(), ','.join( self.intfNames() ), self.pid ) -class CPULimitedHost( Node ): +class Host( Node ): + "A host is simply a Node" + pass + + +class CPULimitedHost( Host ): "CPU limited host" @@ -472,9 +485,8 @@ def __init__( self, *args, **kwargs ): cgroup = 'cpu,cpuacct:/' + self.name errFail( 'cgcreate -g ' + cgroup ) errFail( 'cgclassify -g %s %s' % ( cgroup, self.pid ) ) - self.sched = 'rt' - self.period_us = 10000 - self.rtset = False + self.period_us = kwargs.get( 'period_us', 10000 ) + self.sched = kwargs.get( 'sched', 'rt' ) def cgroupSet( self, param, value, resource='cpu' ): "Set a cgroup parameter and return its value" @@ -495,40 +507,73 @@ def chrt( self, prio=20 ): lastword = firstline.split( ' ' )[ -1 ] return lastword - def setCPUFrac( self, f=-1 ): - "Set overall CPU fraction for this host" - if ( f < 0 or f is None): + # BL comment: + # This may not be the right API, + # since it doesn't specify CPU bandwidth in "absolute" + # units the way link bandwidth is specified. + # We should use MIPS or SPECINT or something instead. + # Alternatively, we should change from system fraction + # to CPU seconds per second, essentially assuming that + # all CPUs are the same. + + def setCPUFrac( self, f=-1, sched=None): + """Set overall CPU fraction for this host + f: CPU bandwidth limit (fraction) + sched: 'rt' or 'cfs' + Note 'cfs' requires CONFIG_CFS_BANDWIDTH""" + if not f: + return + if not sched: + sched = self.sched + period = self.period_us + if sched == 'rt': + pstr, qstr = 'rt_period_us', 'rt_runtime_us' + # RT uses system time for period and quota + quota = int( period * f * numCores() ) + elif sched == 'cfs': + pstr, qstr = 'cfs_period_us', 'cfs_quota_us' + # CFS uses wall clock time for period and CPU time for quota. + quota = int( self.period_us * f * numCores() ) + if f > 0 and quota < 1000: + info( '*** setCPUFrac: quota too small - adjusting period\n' ) + quota = 1000 + period = int( quota / f / numCores() ) + else: + return + if quota < 0: # Reset to unlimited - f = -1 - # Set new period and quota - pstr, qstr = 'rt_period_us', 'rt_runtime_us' - quota = int( self.period_us * f * numCores() ) - self.cgroupSet( pstr, self.period_us ) + quota = -1 + # Set cgroup's period and quota + self.cgroupSet( pstr, period ) nquota = int ( self.cgroupGet( qstr ) ) self.cgroupSet( qstr, quota ) nperiod = int( self.cgroupGet( pstr ) ) - # Set RT priority - nchrt = self.chrt( prio=20 ) - # Check to make sure it worked - if 'SCHED_RR' not in nchrt: - error( '*** error: could not assign SCHED_RR to %s\n' % self.name ) + # Make sure it worked if nperiod != self.period_us: error( '*** error: period is %s rather than %s\n' % ( nperiod, self.period_us ) ) if nquota != quota: error( '*** error: quota is %s rather than %s\n' % ( nquota, quota ) ) + if sched == 'rt': + # Set RT priority if necessary + nchrt = self.chrt( prio=20 ) + # Nake sure it worked + if sched == 'SCHED_RR' not in nchrt: + error( '*** error: could not assign SCHED_RR to %s\n' % self.name ) + info( '( period', nperiod, 'quota', nquota, nchrt, ') ' ) + else: + info( '( period', nperiod, 'quota', nquota, ') ' ) - def config( self, cpu=None, **params ): + def config( self, cpu=None, sched=None, **params ): """cpu: desired overall system CPU fraction params: parameters for Node.config()""" r = Node.config( self, **params ) + # Was considering cpu={'cpu': cpu , 'sched': sched}, but + # that seems redundant self.setParam( r, 'setCPUFrac', cpu=cpu ) return r -Host = CPULimitedHost - - # Some important things to note: # # The "IP" address which we assign to the switch is not @@ -805,7 +850,7 @@ def __init__( self, ip, prefixLen ): class NOX( Controller ): "Controller to run a NOX application." - def __init__( self, name, noxArgs=None, **kwargs ): + def __init__( self, name, noxArgs=[], **kwargs ): """Init. name: name to give controller noxArgs: list of args, or single arg, to pass to NOX"""