From 84a91a14a46873b088793ebf136637a3486726a3 Mon Sep 17 00:00:00 2001 From: Bob Lantz Date: Tue, 6 Mar 2012 23:52:00 -0800 Subject: [PATCH] New configuration scheme and support for CPU limits (RT). --- mininet/link.py | 94 ++++++++----- mininet/net.py | 249 ++++++++++++++++---------------- mininet/node.py | 366 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 434 insertions(+), 275 deletions(-) diff --git a/mininet/link.py b/mininet/link.py index c7c7aa2..d3193ca 100644 --- a/mininet/link.py +++ b/mininet/link.py @@ -39,10 +39,12 @@ class BasicIntf( object ): self.name = name self.link = link self.mac, self.ip = None, None + # Add to node (and move ourselves if necessary ) + node.addIntf( self ) self.config( **kwargs ) def cmd( self, *args, **kwargs ): - self.node.cmd( *args, **kwargs ) + return self.node.cmd( *args, **kwargs ) def ifconfig( self, *args ): "Configure ourselves using ifconfig" @@ -93,22 +95,43 @@ class BasicIntf( object ): return "UP" in self.ifconfig() - # Map of config params to config methods - # Perhaps this could be more graceful, but it - # is flexible - configMap = { 'mac': 'setMAC', - 'ip': 'setIP', - 'ifconfig': 'ifconfig' } + # The reason why we configure things in this way is so + # That the parameters can be listed and documented in + # the config method. + # Dealing with subclasses and superclasses is slightly + # annoying, but at least the information is there! - def config( self, **params ): - "Configure interface based on parameters" - self.__dict__.update(**params) - for name, value in params.iteritems(): - method = self.configMap.get( name, None ) - if method: - if type( value ) is str: - value = value.split( ',' ) - method( value ) + def setParam( self, result, method, **param ): + """Internal method: configure single parameter + result: dict of results to update + method: config method + param: foo=bar (ignore if bar=None)""" + name, value = param.items()[ 0 ] + if value is None: + return + if type( value ) is list: + result[ name ] = getattr( self, method )( *value ) + elif type( value ) is dict: + result[ name ] = getattr( self, method )( **value ) + else: + result[ name ] = getattr( self, method )( value ) + + def config( self, mac=None, ip=None, ifconfig=None, + defaultRoute=None, **params): + """Configure Node according to (optional) parameters: + mac: MAC address + ip: IP address + ifconfig: arbitrary interface configuration + Subclasses should override this method and call + the parent class's config(**params)""" + # If we were overriding this method, we would call + # the superclass config method here as follows: + # r = Parent.config( **params ) + r = {} + self.setParam( r, 'setMAC', mac=mac ) + self.setParam( r, 'setIP', ip=ip ) + self.setParam( r, 'ifconfig', ifconfig=ifconfig ) + return r def delete( self ): "Delete interface" @@ -125,10 +148,10 @@ class TCIntf( BasicIntf ): def config( self, bw=None, delay=None, loss=0, disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False, enable_ecn=False, - enable_red=False, max_queue_size=1000, **kwargs ): + enable_red=False, max_queue_size=1000, **params ): "Configure the port and set its properties." - BasicIntf.config( self, **kwargs) + result = BasicIntf.config( self, **params) # disable GRO if disable_gro: @@ -153,7 +176,7 @@ class TCIntf( BasicIntf ): delay = '0ms' if bw is not None and delay is not None: - info( self, '(bw %.2fMbit, delay %s, loss %d%%)\n' % + info( self, '(bw %.2fMbit, delay %s, loss %d%%) ' % ( bw, delay, loss ) ) # BL: hmm... what exactly is this??? @@ -209,8 +232,11 @@ class TCIntf( BasicIntf ): debug(" *** executing command: %s\n" % c) return self.cmd(c) - outputs = [ doConfigPort(cmd) for cmd in cmds ] - debug( "outputs: %s\n" % outputs ) + tcoutputs = [ doConfigPort(cmd) for cmd in cmds ] + debug( "cmds:", cmds, '\n' ) + debug( "outputs:", tcoutputs, '\n' ) + result[ 'tcoutputs'] = tcoutputs + return result Intf = TCIntf @@ -220,14 +246,18 @@ class Link( object ): Other types of links could be tunnels, link emulators, etc..""" def __init__( self, node1, node2, port1=None, port2=None, intfName1=None, intfName2=None, - intf=Intf, params1={}, params2={} ): + intf=Intf, cls1=None, cls2=None, params1={}, params2={} ): """Create veth link to another node, making two new interfaces. node1: first node node2: second node port1: node1 port number (optional) port2: node2 port number (optional) + intf: default interface class/constructor + cls1, cls2: optional interface-specific constructors intfName1: node1 interface name (optional) - intfName2: node2 interface name (optional)""" + intfName2: node2 interface name (optional) + params1: parameters for interface 1 + params2: parameters for interface 2""" # This is a bit awkward; it seems that having everything in # params would be more orthogonal, but being able to specify # in-line arguments is more convenient! @@ -240,11 +270,13 @@ class Link( object ): if not intfName2: intfName2 = self.intfName( node2, port2 ) self.makeIntfPair( intfName1, intfName2 ) - intf1 = intf( name=intfName1, node=node1, link=self, **params1 ) - intf2 = intf( name=intfName2, node=node2, link=self, **params2 ) - # Add to nodes - node1.addIntf( intf1 ) - node2.addIntf( intf2 ) + if not cls1: + cls1 = intf + if not cls2: + cls2 = intf + intf1 = cls1( name=intfName1, node=node1, link=self, **params1 ) + intf2 = cls2( name=intfName2, node=node2, link=self, **params2 ) + # All we are is dust in the wind, and our two interfaces self.intf1, self.intf2 = intf1, intf2 @classmethod @@ -267,9 +299,3 @@ class Link( object ): def __str__( self ): return '%s<->%s' % ( self.intf1, self.intf2 ) - - - - - - diff --git a/mininet/net.py b/mininet/net.py index c5c2931..18d0363 100755 --- a/mininet/net.py +++ b/mininet/net.py @@ -94,8 +94,7 @@ from time import sleep from mininet.cli import CLI from mininet.log import info, error, debug, output -from mininet.node import Host, UserSwitch, OVSKernelSwitch, Controller -from mininet.node import ControllerParams +from mininet.node import Host, OVSKernelSwitch, Controller from mininet.link import Link from mininet.util import quietRun, fixLimits from mininet.util import createLink, macColonHex, ipStr, ipParse @@ -106,7 +105,6 @@ class Mininet( object ): def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, controller=Controller, link=Link, - cparams=ControllerParams( '10.0.0.0', 8 ), build=True, xterms=False, cleanup=False, inNamespace=False, autoSetMacs=False, autoStaticArp=False, listenPort=None ): @@ -116,12 +114,12 @@ class Mininet( object ): host: default Host class/constructor controller: default Controller class/constructor link: default Link class/constructor - cparams: ControllerParams object + 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 from topo? + autoSetMacs: set MAC addrs from topo dpid? 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""" @@ -129,7 +127,6 @@ class Mininet( object ): self.host = host self.controller = controller self.link = link - self.cparams = cparams self.topo = topo self.inNamespace = inNamespace self.xterms = xterms @@ -141,13 +138,13 @@ class Mininet( object ): self.hosts = [] self.switches = [] self.controllers = [] + self.nameToNode = {} # name to Node (Host/Switch) objects self.idToNode = {} # dpid to Node (Host/Switch) objects - self.dps = 0 # number of created kernel datapaths + self.terms = [] # list of spawned xterm processes - init() - switch.setup() + init() # Initialize Mininet if necessary self.built = False if topo and build: @@ -157,17 +154,15 @@ class Mininet( object ): # The specific items for host/switch/etc. should probably be # handled in the node classes rather than here!! - def addHost( self, name, mac=None, ip=None, host=None, **params ): + def addHost( self, name, host=None, **params ): """Add host. name: name of host to add - mac: default MAC address for intf 0 - ip: default IP address for intf 0 + host: custom host constructor (optional) + params: parameters for host returns: added host""" if not host: host = self.host - defaults = { 'defaultMAC': mac, 'defaultIP': ip } - defaults.update( params ) - h = host( name, **defaults) + h = host( name, **params) self.hosts.append( h ) self.nameToNode[ name ] = h return h @@ -175,19 +170,17 @@ class Mininet( object ): def addSwitch( self, name, switch=None, **params ): """Add switch. name: name of switch to add + switch: custom switch constructor (optional) returns: added switch - side effect: increments listenPort and dps ivars.""" + side effect: increments listenPort ivar .""" defaults = { 'listenPort': self.listenPort, 'inNamespace': self.inNamespace } + defaults.update( params ) if not switch: switch = self.switch - if switch != UserSwitch: - defaults[ 'dps' ] = self.dps - defaults.update( params ) sw = self.switch( name, **defaults ) if not self.inNamespace and self.listenPort: self.listenPort += 1 - self.dps += 1 self.switches.append( sw ) self.nameToNode[ name ] = sw return sw @@ -203,122 +196,81 @@ class Mininet( object ): self.nameToNode[ name ] = controller_new return controller_new - # Control network support: - # - # Create an explicit control network. Currently this is only - # used by the user datapath configuration. - # - # 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 uses this method, so perhaps it should be moved - # out of this core class. - - 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 ) - sintf, cintf = createLink( switch, controller ) - snum += 1 - while snum & 0xff in [ 0, 255 ]: - snum += 1 - sip = ipStr( snum ) - controller.setIP( cintf, cip, prefixLen ) - switch.setIP( sintf, sip, prefixLen ) - controller.setHostRoute( sip, cintf ) - switch.setHostRoute( cip, sintf ) - info( '\n' ) - info( '*** Testing control network\n' ) - while not controller.intfIsUp( cintf ): - info( '*** Waiting for', cintf, 'to come up\n' ) - sleep( 1 ) - for switch in self.switches: - while not switch.intfIsUp( sintf ): - 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' ) - def configHosts( self ): "Configure a set of hosts." - # params were: hosts, ips for host in self.hosts: - hintf = host.defaultIntf() - host.setIP( host.defaultIP, self.cparams.prefixLen, hintf ) - host.setDefaultRoute( hintf ) + host.configDefault( defaultRoute=host.defaultIntf ) # You're low priority, dude! - quietRun( 'renice +18 -p ' + repr( host.pid ) ) + # 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 ): + def buildFromTopo( self, topo=None ): """Build mininet from a topology object At the end of this function, everything should be connected and up.""" + if not topo: + topo = self.topo() + def addNode( prefix, addMethod, nodeId ): - "Add a host or a switch." + "Add a host or a switch from topo" name = prefix + topo.name( nodeId ) - # MAC and IP should probably be from nodeInfo... - mac = macColonHex( nodeId ) if self.setMacs else None - ip = topo.ip( nodeId ) ni = topo.nodeInfo( nodeId ) - node = addMethod( name, cls=ni.cls, mac=mac, ip=ip, **ni.params ) + # Default IP and MAC addresses + defaults = { 'ip': topo.ip( nodeId ) } + if self.autoSetMacs: + defaults[ 'mac'] = macColonHex( nodeId ) + defaults.update( ni.params ) + node = addMethod( name, cls=ni.cls, **defaults ) self.idToNode[ nodeId ] = node info( name + ' ' ) + def addLink( srcId, dstId, link=None ): + "Add a link from topo" + src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ] + srcPort, dstPort = topo.port( srcId, dstId ) + ei = topo.edgeInfo( srcId, dstId ) + link = getattr( ei, 'cls', link ) + params = ei.params + if not link: + link = self.link + info( '(%s, %s) ' % ( src.name, dst.name ) ) + link( src, dst, srcPort, dstPort, **params ) + # Possibly we should clean up here and/or validate # the topo if self.cleanup: pass - info( '*** Adding controller\n' ) - self.addController( 'c0' ) info( '*** Creating network\n' ) + + if not self.controllers: + # Add a default controller + info( '*** Adding controller\n' ) + self.addController( 'c0' ) + info( '*** Adding hosts:\n' ) for hostId in sorted( topo.hosts() ): addNode( 'h', self.addHost, hostId ) + info( '\n*** Adding switches:\n' ) for switchId in sorted( topo.switches() ): - addNode( 's', self.addSwitch, switchId) + addNode( 's', self.addSwitch, switchId ) + info( '\n*** Adding links:\n' ) for srcId, dstId in sorted( topo.edges() ): - src, dst = self.idToNode[ srcId ], self.idToNode[ dstId ] - srcPort, dstPort = topo.port( srcId, dstId ) - ei = topo.edgeInfo( srcId, dstId ) - link, params = ei.cls, ei.params - if not link: - link = self.link - link( src, dst, srcPort, dstPort, **params ) - info( '(%s, %s) ' % ( src.name, dst.name ) ) + addLink( srcId, dstId ) + info( '\n' ) + + def configureControlNetwork( self ): + error( "configureControlNetwork: override in subclass, or use" + "MininetWithControlNet class" ) + def build( self ): "Build mininet." if self.topo: @@ -330,8 +282,6 @@ class Mininet( object ): self.configHosts() if self.xterms: self.startTerms() - if self.autoSetMacs: - self.setMacs() if self.autoStaticArp: self.staticArp() self.built = True @@ -346,17 +296,10 @@ class Mininet( object ): def stopXterms( self ): "Kill each xterm." - # Kill xterms for term in self.terms: os.kill( term.pid, signal.SIGKILL ) cleanUpScreens() - def setMacs( self ): - """Set MAC addrs to correspond to default MACs on hosts. - Assume that the host only has one interface.""" - for host in self.hosts: - host.setMAC( host.intfs[ 0 ], host.defaultMAC ) - def staticArp( self ): "Add all-pairs ARP entries to remove the need to handle broadcast." for src in self.hosts: @@ -384,18 +327,19 @@ class Mininet( object ): self.stopXterms() info( '*** Stopping %i hosts\n' % len( self.hosts ) ) for host in self.hosts: - info( '%s ' % host.name ) + info( host.name + ' ' ) host.terminate() info( '\n' ) info( '*** Stopping %i switches\n' % len( self.switches ) ) for switch in self.switches: - info( switch.name ) + info( switch.name + ' ' ) switch.stop() info( '\n' ) info( '*** Stopping %i controllers\n' % len( self.controllers ) ) for controller in self.controllers: + info( controller.name + ' ' ) controller.stop() - info( '*** Done\n' ) + info( '\n*** Done\n' ) def run( self, test, *args, **kwargs ): "Perform a complete start/test/stop cycle." @@ -429,6 +373,9 @@ class Mininet( object ): 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." @@ -543,6 +490,8 @@ class Mininet( object ): output( '*** Results: %s\n' % result ) return result + # 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 @@ -573,6 +522,70 @@ class Mininet( object ): return result +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.""" + + 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 ) + sintf, cintf = createLink( switch, controller ) + snum += 1 + while snum & 0xff in [ 0, 255 ]: + snum += 1 + sip = ipStr( snum ) + controller.setIP( cintf, cip, prefixLen ) + switch.setIP( sintf, sip, prefixLen ) + controller.setHostRoute( sip, cintf ) + switch.setHostRoute( cip, sintf ) + info( '\n' ) + info( '*** Testing control network\n' ) + while not controller.intfIsUp( cintf ): + info( '*** Waiting for', cintf, 'to come up\n' ) + sleep( 1 ) + for switch in self.switches: + while not switch.intfIsUp( sintf ): + 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' ) + # pylint thinks inited is unused # pylint: disable-msg=W0612 @@ -585,10 +598,6 @@ def init(): # Perhaps we should do so automatically! print "*** Mininet must run as root." exit( 1 ) - # If which produces no output, then mnexec is not in the path. - # May want to loosen this to handle mnexec in the current dir. - if not quietRun( 'which mnexec' ): - raise Exception( "Could not find mnexec - check $PATH" ) fixLimits() init.inited = True diff --git a/mininet/node.py b/mininet/node.py index cfeb02f..68a843e 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -48,7 +48,8 @@ import select from subprocess import Popen, PIPE, STDOUT from mininet.log import info, error, debug -from mininet.util import quietRun, errRun, moveIntf, isShellBuiltin +from mininet.util import quietRun, errRun, errFail, moveIntf, isShellBuiltin +from mininet.util import numCores from mininet.moduledeps import moduleDeps, pathCheck, OVS_KMOD, OF_KMOD, TUN from mininet.link import Link @@ -58,21 +59,74 @@ class Node( object ): """A virtual network node is simply a shell in a network namespace. We communicate with it using pipes.""" + portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0 + + def __init__( self, name, inNamespace=True, **params ): + """name: name of node + inNamespace: in network namespace? + params: Node parameters (see config() for details)""" + + # Make sure class actually works + self.checkSetup() + + self.name = name + self.inNamespace = inNamespace + + # Stash configuration parameters for future reference + self.params = params + + self.intfs = {} # dict of port numbers to interfaces + self.ports = {} # dict of interfaces to port numbers + # replace with Port objects, eventually ? + self.nameToIntf = {} # dict of interface names to Intfs + + # Start command interpreter shell + self.shell = None + self.startShell() + + # File descriptor to node mapping support + # Class variables and methods + inToNode = {} # mapping of input fds to nodes outToNode = {} # mapping of output fds to nodes - portBase = 0 # Nodes always start with eth0/port0, even in OF 1.0 + @classmethod + def fdToNode( cls, fd ): + """Return node corresponding to given file descriptor. + fd: file descriptor + returns: node""" + node = cls.outToNode.get( fd ) + return node or cls.inToNode.get( fd ) - def __init__( self, name, inNamespace=True, - defaultMAC=None, defaultIP=None, **kwargs ): - """name: name of node - inNamespace: in network namespace? - defaultMAC: default MAC address for intf 0 - defaultIP: default IP address for intf 0""" - self.name = name - self.inNamespace = inNamespace - self.defaultIP = defaultIP - self.defaultMAC = defaultMAC + # Automatic class setup support + + isSetup = False; + + @classmethod + def checkSetup( cls ): + "Make sure our class and superclasses are set up" + while cls and not getattr( cls, 'isSetup', True ): + cls.setup() + cls.isSetup = True + # Make pylint happy + cls = getattr( type( cls ), '__base__', None ) + + @classmethod + def setup( cls ): + "Make sure our class dependencies are available" + pathCheck( 'mnexec', 'ifconfig', moduleName='Mininet') + + def cleanup( self ): + "Help python collect its garbage." + self.shell = None + + # Command support via shell process in namespace + + def startShell( self ): + "Start a shell process for running commands" + if self.shell: + error( "%s: shell is already running" ) + return opts = '-cdp' if self.inNamespace: opts += 'n' @@ -89,31 +143,12 @@ class Node( object ): # using select.poll() self.outToNode[ self.stdout.fileno() ] = self self.inToNode[ self.stdin.fileno() ] = self - self.intfs = {} # dict of port numbers to interfaces - self.ports = {} # dict of interfaces to port numbers - # replace with Port objects, eventually ? - self.nameToIntf = {} # dict of interface names to Intfs self.execed = False self.lastCmd = None self.lastPid = None self.readbuf = '' self.waiting = False - # Stash additional information as desired - self.args = kwargs - @classmethod - def fdToNode( cls, fd ): - """Return node corresponding to given file descriptor. - fd: file descriptor - returns: node""" - node = Node.outToNode.get( fd ) - return node or Node.inToNode.get( fd ) - - def cleanup( self ): - "Help python collect its garbage." - self.shell = None - - # Subshell I/O, commands and control def read( self, bytes=1024 ): """Buffered read from node, non-blocking. bytes: maximum number of bytes to return""" @@ -267,10 +302,10 @@ class Node( object ): self.intfs[ port ] = intf self.ports[ intf ] = port self.nameToIntf[ intf.name ] = intf - info( '\n' ) - info( 'added intf %s:%d to node %s\n' % ( intf,port, self.name ) ) + debug( '\n' ) + debug( 'added intf %s:%d to node %s\n' % ( intf,port, self.name ) ) if self.inNamespace: - info( 'moving', intf, 'into namespace for', self.name, '\n' ) + debug( 'moving', intf, 'into namespace for', self.name, '\n' ) moveIntf( intf.name, self ) def defaultIntf( self ): @@ -326,13 +361,15 @@ class Node( object ): intf: string, interface name""" return self.cmd( 'route add -host ' + ip + ' dev ' + intf ) - def setDefaultRoute( self, intf ): + def setDefaultRoute( self, intf=None ): """Set the default route to go through intf. intf: string, interface name""" + if not intf: + intf = self.defaultIntf() self.cmd( 'ip route flush root 0/0' ) return self.cmd( 'route add default %s' % intf ) - # Convenience methods + # Convenience and configuration methods def setMAC( self, mac, intf=''): """Set the MAC address for an interface. @@ -361,6 +398,49 @@ class Node( object ): "Check if an interface is up." return self.intf( intf ).isUp() + # The reason why we configure things in this way is so + # That the parameters can be listed and documented in + # the config method. + # Dealing with subclasses and superclasses is slightly + # annoying, but at least the information is there! + + def setParam( self, results, method, **param ): + """Internal method: configure single parameter""" + name, value = param.items()[ 0 ] + f = getattr( self, method, None ) + if not value or not f: + return + if type( value ) is list: + result = f( *value ) + elif type( value ) is dict: + result = f( **value ) + else: + result = f( value ) + results[ name ] = result + + def config( self, mac=None, ip=None, ifconfig=None, + defaultRoute=None, **params): + """Configure Node according to (optional) parameters: + mac: MAC address for default interface + ip: IP address for default interface + ifconfig: arbitrary interface configuration + Subclasses should override this method and call + the parent class's config(**params)""" + # If we were overriding this method, we would call + # the superclass config method here as follows: + # r = Parent.config( **params ) + r = {} + self.setParam( r, 'setMAC', mac=mac ) + self.setParam( r, 'setIP', ip=ip ) + self.setParam( r, 'ifconfig', ifconfig=ifconfig ) + self.setParam( r, 'defaultRoute', defaultRoute=defaultRoute ) + return r + + def configDefault( self, **moreParams ): + "Configure with default parameters" + self.params.update( moreParams ) + self.config( **self.params ) + # This is here for backward compatibility def linkTo( self, node, link=Link ): """(Deprecated) Link to another node @@ -382,9 +462,94 @@ class Node( object ): self.name, self.IP(), ','.join( self.intfNames() ), self.pid ) -class Host( Node ): - "A host is simply a Node." +class CPULimitedHost( Node ): + "CPU limited host" + + def __init__( self, *args, **kwargs ): + Node.__init__( self, *args, **kwargs ) + # Create a cgroup and move shell into it + 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 + + def cgroupSet( self, param, value, resource='cpu' ): + "Set a cgroup parameter and return its value" + cmd = 'cgset -r %s.%s=%s /%s' % ( + resource, param, value, self.name ) + return quietRun( cmd ) + + def cgroupGet( self, param, resource='cpu' ): + cmd = 'cgget -r %s.%s /%s' % ( + resource, param, self.name ) + return quietRun( cmd ).split()[ -1 ] + + def chrt( self, prio=20 ): + "Set RT scheduling priority" + quietRun( 'chrt -p %s %s' % ( prio, self.pid ) ) + result = quietRun( 'chrt -p %s' % self.pid ) + firstline = result.split( '\n' )[ 0 ] + 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): + # 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 ) + 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 ) + 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 ) ) + + def config( self, cpu=None, **params ): + """cpu: desired overall system CPU fraction + params: parameters for Node.config()""" + r = Node.config( self, **params ) + 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 +# an "IP address for the switch" in the sense of IP routing. +# Rather, it is the IP address for a control interface if +# (and only if) you happen to be running the switch in a +# namespace, which is something we currently don't support +# for OVS! +# +# In general, you NEVER want to attempt to use Linux's +# network stack (i.e. ifconfig) to "assign" an IP address or +# MAC address to a switch data port. Instead, you "assign" +# the IP and MAC addresses in the controller by specifying +# packets that you want to receive or send. The "MAC" address +# reported by ifconfig for a switch data port is essentially +# meaningless. +# +# So, I'm tyring changing the API to make it +# impossible to try this, since it will not work, since nobody +# ever makes separate control networks in Mininet, and indeed +# we don't even support running OVS in a namespace. class Switch( Node ): """A Switch is a Node that is running (or has execed?) @@ -392,19 +557,31 @@ class Switch( Node ): portBase = SWITCH_PORT_BASE # 0 for OF < 1.0, 1 for OF >= 1.0 - def __init__( self, name, opts='', listenPort=None, **kwargs): - Node.__init__( self, name, **kwargs ) + def __init__( self, name, dpid=None, opts='', listenPort=None, **params): + """dpid: dpid for switch (or None for default) + opts: additional switch options + listenPort: port to listen on for dpctl connections""" + Node.__init__( self, name, **params ) + self.dpid = dpid if dpid else self.defaultDpid() self.opts = opts self.listenPort = listenPort if self.listenPort: self.opts += ' --listen=ptcp:%i ' % self.listenPort + self.controlIntf = None + + def defaultDpid( self ): + "Derive dpid from switch name, s1 -> 1" + dpid = int( re.findall( '\d+', self.name )[ 0 ] ) + dpid = hex( dpid )[ 2: ] + dpid = '0' * ( 12 - len( dpid ) ) + dpid + return dpid def defaultIntf( self ): - "Return interface for HIGHEST port" - ports = self.intfs.keys() - if ports: - intf = self.intfs[ max( ports ) ] - return intf + "Return control interface, if any" + if not self.inNamespace: + error( "error: tried to access control interface of " + " switch %s in root namespace" % self.name ) + return self.controlIntf def sendCmd( self, *cmd, **kwargs ): """Send command to Node. @@ -440,15 +617,13 @@ class UserSwitch( Switch ): ofdlog = '/tmp/' + self.name + '-ofd.log' ofplog = '/tmp/' + self.name + '-ofp.log' self.cmd( 'ifconfig lo up' ) - mac_str = '' - if self.defaultMAC: - # ofdatapath expects a string of hex digits with no colons. - mac_str = ' -d ' + ''.join( self.defaultMAC.split( ':' ) ) - intfs = sorted( self.intfs.values() ) + ports = sorted( self.ports.values() ) + intfs = [ str( self.intfs[ p ] ) for p in ports ] if self.inNamespace: intfs = intfs[ :-1 ] self.cmd( 'ofdatapath -i ' + ','.join( intfs ) + - ' punix:/tmp/' + self.name + mac_str + ' --no-slicing ' + + ' punix:/tmp/' + self.name + ' -d ' + self.dpid + + ' --no-slicing ' + ' 1> ' + ofdlog + ' 2> ' + ofdlog + ' &' ) self.cmd( 'ofprotocol unix:/tmp/' + self.name + ' tcp:%s:%d' % ( controller.IP(), controller.port ) + @@ -461,61 +636,6 @@ class UserSwitch( Switch ): self.cmd( 'kill %ofprotocol' ) self.deleteIntfs() -class KernelSwitch( Switch ): - """Kernel-space switch. - Currently only works in root namespace.""" - - def __init__( self, name, dp=None, **kwargs ): - """Init. - name: name for switch - dp: netlink id (0, 1, 2, ...) - defaultMAC: default MAC as string; random value if None""" - Switch.__init__( self, name, **kwargs ) - self.dp = 'nl:%i' % dp - self.intf = 'of%i' % dp - if self.inNamespace: - error( "KernelSwitch currently only works" - " in the root namespace." ) - exit( 1 ) - - @staticmethod - def setup(): - "Ensure any dependencies are loaded; if not, try to load them." - pathCheck( 'ofprotocol', - moduleName='the OpenFlow reference kernel switch' - ' (openflow.org) (NOTE: not available in OpenFlow 1.0!)' ) - moduleDeps( subtract=OVS_KMOD, add=OF_KMOD ) - - def start( self, controllers ): - "Start up 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.cmd( 'dpctl adddp ' + self.dp ) - if self.defaultMAC: - self.cmd( 'ifconfig', self.intf, 'hw', 'ether', self.defaultMAC ) - ports = sorted( self.ports.values() ) - if len( ports ) != ports[ -1 ] + 1 - self.portBase: - raise Exception( 'only contiguous, zero-indexed port ranges' - 'supported: %s' % ports ) - intfs = [ self.intfs[ port ] for port in ports ] - self.cmd( 'dpctl', 'addif', self.dp, ' '.join( intfs ) ) - # Run protocol daemon - controller = controllers[ 0 ] - self.cmd( 'ofprotocol ' + self.dp + - ' tcp:%s:%d' % ( controller.IP(), controller.port ) + - ' --fail=closed ' + self.opts + - ' 1> ' + ofplog + ' 2>' + ofplog + ' &' ) - self.execed = False - - def stop( self ): - "Terminate kernel datapath." - quietRun( 'dpctl deldp ' + self.dp ) - self.cmd( 'kill %ofprotocol' ) - self.deleteIntfs() - class OVSLegacyKernelSwitch( Switch ): """Open VSwitch legacy kernel-space switch using ovs-openflowd. @@ -549,12 +669,6 @@ class OVSLegacyKernelSwitch( Switch ): # then create a new one monitoring the given interfaces quietRun( 'ovs-dpctl del-dp ' + self.dp ) self.cmd( 'ovs-dpctl add-dp ' + self.dp ) - mac_str = '' - if self.defaultMAC: - # ovs-openflowd expects a string of exactly 16 hex digits with no - # colons. - mac_str = ' --datapath-id=0000' + \ - ''.join( self.defaultMAC.split( ':' ) ) + ' ' ports = sorted( self.ports.values() ) if len( ports ) != ports[ -1 ] + 1 - self.portBase: raise Exception( 'only contiguous, one-indexed port ranges ' @@ -565,7 +679,8 @@ class OVSLegacyKernelSwitch( Switch ): controller = controllers[ 0 ] self.cmd( 'ovs-openflowd ' + self.dp + ' tcp:%s:%d' % ( controller.IP(), controller.port ) + - ' --fail=secure ' + self.opts + mac_str + + ' --fail=secure ' + self.opts + + ' --datapath-id=' + self.dpid + ' 1>' + ofplog + ' 2>' + ofplog + '&' ) self.execed = False @@ -579,13 +694,17 @@ class OVSLegacyKernelSwitch( Switch ): class OVSSwitch( Switch ): "Open vSwitch switch. Depends on ovs-vsctl." - def __init__( self, name, dp=None, **kwargs ): + def __init__( self, name, **params ): """Init. name: name for switch defaultMAC: default MAC as unsigned int; random value if None""" - Switch.__init__( self, name, **kwargs ) + Switch.__init__( self, name, **params ) + # self.dp is the text name for the datapath that + # we use for ovs-vsctl. This is different from the + # dpid, which is a 64-bit numerical value used by + # the openflow protocol. self.dp = name - + @staticmethod def setup(): "Make sure Open vSwitch is installed and working" @@ -609,7 +728,6 @@ class OVSSwitch( Switch ): self.cmd( 'ovs-vsctl del-br ', self.dp ) self.cmd( 'ovs-vsctl add-br', self.dp ) self.cmd( 'ovs-vsctl set-fail-mode', self.dp, 'secure' ) - # Add ports ports = sorted( self.ports.values() ) intfs = [ self.intfs[ port ] for port in ports ] # XXX: Ugly check - we should probably fix this! @@ -629,19 +747,21 @@ class OVSSwitch( Switch ): OVSKernelSwitch = OVSSwitch + class Controller( Node ): """A Controller is a Node that is running (or has execed?) an OpenFlow controller.""" def __init__( self, name, inNamespace=False, command='controller', - cargs='-v ptcp:%d', cdir=None, defaultIP="127.0.0.1", - port=6633 ): + cargs='-v ptcp:%d', cdir=None, ip="127.0.0.1", + port=6633, **params ): self.command = command self.cargs = cargs self.cdir = cdir + self.ip = ip self.port = port Node.__init__( self, name, inNamespace=inNamespace, - defaultIP=defaultIP ) + ip=ip, **params ) def start( self ): """Start on controller. @@ -664,9 +784,13 @@ class Controller( Node ): if self.intfs: ip = Node.IP( self, intf ) else: - ip = self.defaultIP + ip = self.ip return ip + +# BL: This really seems to be poorly specified, +# so it's going to go away! + class ControllerParams( object ): "Container for controller IP parameters."