""" link.py: interface and link abstractions for mininet It seems useful to bundle functionality for interfaces into a single class. Also it seems useful to enable the possibility of multiple flavors of links, including: - simple veth pairs - tunneled links - patchable links (which can be disconnected and reconnected via a patchbay) - link simulators (e.g. wireless) Basic division of labor: Nodes: know how to execute commands Intfs: know how to configure themselves Links: know how to connect nodes together Intf: basic interface object that can configure itself TCIntf: interface with bandwidth limiting and delay via tc Link: basic link class for creating veth pairs """ import re from mininet.log import info, error, debug from mininet.util import makeIntfPair # Make pylint happy: # pylint: disable=too-many-arguments class Intf( object ): "Basic interface object that can configure itself." def __init__( self, name, node=None, port=None, link=None, mac=None, **params ): """name: interface name (e.g. h1-eth0) node: owning node (where this intf most likely lives) link: parent link if we're part of a link other arguments are passed to config()""" self.node = node self.name = name self.link = link self.mac = mac self.ip, self.prefixLen = None, None # if interface is lo, we know the ip is 127.0.0.1. # This saves an ifconfig command per node if self.name == 'lo': self.ip = '127.0.0.1' self.prefixLen = 8 # Add to node (and move ourselves if necessary ) if node: moveIntfFn = params.pop( 'moveIntfFn', None ) if moveIntfFn: node.addIntf( self, port=port, moveIntfFn=moveIntfFn ) else: node.addIntf( self, port=port ) # Save params for future reference self.params = params self.config( **params ) def cmd( self, *args, **kwargs ): "Run a command in our owning node" return self.node.cmd( *args, **kwargs ) def ifconfig( self, *args ): "Configure ourselves using ifconfig" return self.cmd( 'ifconfig', self.name, *args ) def setIP( self, ipstr, prefixLen=None ): """Set our IP address""" # This is a sign that we should perhaps rethink our prefix # mechanism and/or the way we specify IP addresses if '/' in ipstr: self.ip, self.prefixLen = ipstr.split( '/' ) return self.ifconfig( ipstr, 'up' ) else: if prefixLen is None: raise Exception( 'No prefix length set for IP address %s' % ( ipstr, ) ) self.ip, self.prefixLen = ipstr, prefixLen return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) ) def setMAC( self, macstr ): """Set the MAC address for an interface. macstr: MAC address as string""" self.mac = macstr return ( self.ifconfig( 'down' ) + self.ifconfig( 'hw', 'ether', macstr ) + self.ifconfig( 'up' ) ) _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' ) _macMatchRegex = re.compile( r'..:..:..:..:..:..' ) def updateIP( self ): "Return updated IP address based on ifconfig" # use pexec instead of node.cmd so that we dont read # backgrounded output from the cli. ifconfig, _err, _exitCode = self.node.pexec( 'ifconfig %s' % self.name ) ips = self._ipMatchRegex.findall( ifconfig ) self.ip = ips[ 0 ] if ips else None return self.ip def updateMAC( self ): "Return updated MAC address based on ifconfig" ifconfig = self.ifconfig() macs = self._macMatchRegex.findall( ifconfig ) self.mac = macs[ 0 ] if macs else None return self.mac # Instead of updating ip and mac separately, # use one ifconfig call to do it simultaneously. # This saves an ifconfig command, which improves performance. def updateAddr( self ): "Return IP address and MAC address based on ifconfig." ifconfig = self.ifconfig() ips = self._ipMatchRegex.findall( ifconfig ) macs = self._macMatchRegex.findall( ifconfig ) self.ip = ips[ 0 ] if ips else None self.mac = macs[ 0 ] if macs else None return self.ip, self.mac def IP( self ): "Return IP address" return self.ip def MAC( self ): "Return MAC address" return self.mac def isUp( self, setUp=False ): "Return whether interface is up" if setUp: cmdOutput = self.ifconfig( 'up' ) # no output indicates success if cmdOutput: error( "Error setting %s up: %s " % ( self.name, cmdOutput ) ) return False else: return True else: return "UP" in self.ifconfig() def rename( self, newname ): "Rename interface" if self.node and self.name in self.node.nameToIntf: # rename intf in node's nameToIntf self.node.nameToIntf[newname] = self.node.nameToIntf.pop(self.name) self.ifconfig( 'down' ) result = self.cmd( 'ip link set', self.name, 'name', newname ) self.name = newname self.ifconfig( 'up' ) return result # 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 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 = list( param.items() )[ 0 ] f = getattr( self, method, None ) if not f or value is None: return None if isinstance( value, list ): result = f( *value ) elif isinstance( value, dict ): result = f( **value ) else: result = f( value ) results[ name ] = result return result def config( self, mac=None, ip=None, ifconfig=None, up=True, **_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, 'isUp', up=up ) self.setParam( r, 'ifconfig', ifconfig=ifconfig ) return r def delete( self ): "Delete interface" self.cmd( 'ip link del ' + self.name ) # We used to do this, but it slows us down: # if self.node.inNamespace: # Link may have been dumped into root NS # quietRun( 'ip link del ' + self.name ) self.node.delIntf( self ) self.link = None def status( self ): "Return intf status as a string" links, _err, _result = self.node.pexec( 'ip link show' ) if self.name in links: return "OK" else: return "MISSING" def __repr__( self ): return '<%s %s>' % ( self.__class__.__name__, self.name ) def __str__( self ): return self.name class TCIntf( Intf ): """Interface customized by tc (traffic control) utility Allows specification of bandwidth limits (various methods) as well as delay, loss and max queue length""" # The parameters we use seem to work reasonably up to 1 Gb/sec # For higher data rates, we will probably need to change them. bwParamMax = 1000 def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False, latency_ms=None, enable_ecn=False, enable_red=False ): "Return tc commands to set bandwidth" cmds, parent = [], ' root ' if bw and ( bw < 0 or bw > self.bwParamMax ): error( 'Bandwidth limit', bw, 'is outside supported range 0..%d' % self.bwParamMax, '- ignoring\n' ) elif bw is not None: # BL: this seems a bit brittle... if ( speedup > 0 and self.node.name[0:1] == 's' ): bw = speedup # This may not be correct - we should look more closely # at the semantics of burst (and cburst) to make sure we # are specifying the correct sizes. For now I have used # the same settings we had in the mininet-hifi code. if use_hfsc: cmds += [ '%s qdisc add dev %s root handle 5:0 hfsc default 1', '%s class add dev %s parent 5:0 classid 5:1 hfsc sc ' + 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ] elif use_tbf: if latency_ms is None: latency_ms = 15.0 * 8 / bw cmds += [ '%s qdisc add dev %s root handle 5: tbf ' + 'rate %fMbit burst 15000 latency %fms' % ( bw, latency_ms ) ] else: cmds += [ '%s qdisc add dev %s root handle 5:0 htb default 1', '%s class add dev %s parent 5:0 classid 5:1 htb ' + 'rate %fMbit burst 15k' % bw ] parent = ' parent 5:1 ' # ECN or RED if enable_ecn: cmds += [ '%s qdisc add dev %s' + parent + 'handle 6: red limit 1000000 ' + 'min 30000 max 35000 avpkt 1500 ' + 'burst 20 ' + 'bandwidth %fmbit probability 1 ecn' % bw ] parent = ' parent 6: ' elif enable_red: cmds += [ '%s qdisc add dev %s' + parent + 'handle 6: red limit 1000000 ' + 'min 30000 max 35000 avpkt 1500 ' + 'burst 20 ' + 'bandwidth %fmbit probability 1' % bw ] parent = ' parent 6: ' return cmds, parent @staticmethod def delayCmds( parent, delay=None, jitter=None, loss=None, max_queue_size=None ): "Internal method: return tc commands for delay and loss" cmds = [] if loss and ( loss < 0 or loss > 100 ): error( 'Bad loss percentage', loss, '%%\n' ) else: # Delay/jitter/loss/max queue size netemargs = '%s%s%s%s' % ( 'delay %s ' % delay if delay is not None else '', '%s ' % jitter if jitter is not None else '', 'loss %.5f ' % loss if (loss is not None and loss > 0) else '', 'limit %d' % max_queue_size if max_queue_size is not None else '' ) if netemargs: cmds = [ '%s qdisc add dev %s ' + parent + ' handle 10: netem ' + netemargs ] parent = ' parent 10:1 ' return cmds, parent def tc( self, cmd, tc='tc' ): "Execute tc command for our interface" c = cmd % (tc, self) # Add in tc command and our name debug(" *** executing command: %s\n" % c) return self.cmd( c ) def config( # pylint: disable=arguments-renamed,arguments-differ self, bw=None, delay=None, jitter=None, loss=None, gro=False, txo=True, rxo=True, speedup=0, use_hfsc=False, use_tbf=False, latency_ms=None, enable_ecn=False, enable_red=False, max_queue_size=None, **params ): """Configure the port and set its properties. bw: bandwidth in b/s (e.g. '10m') delay: transmit delay (e.g. '1ms' ) jitter: jitter (e.g. '1ms') loss: loss (e.g. '1%' ) gro: enable GRO (False) txo: enable transmit checksum offload (True) rxo: enable receive checksum offload (True) speedup: experimental switch-side bw option use_hfsc: use HFSC scheduling use_tbf: use TBF scheduling latency_ms: TBF latency parameter enable_ecn: enable ECN (False) enable_red: enable RED (False) max_queue_size: queue limit parameter for netem""" # Support old names for parameters gro = not params.pop( 'disable_gro', not gro ) result = Intf.config( self, **params) def on( isOn ): "Helper method: bool -> 'on'/'off'" return 'on' if isOn else 'off' # Set offload parameters with ethool self.cmd( 'ethtool -K', self, 'gro', on( gro ), 'tx', on( txo ), 'rx', on( rxo ) ) # Optimization: return if nothing else to configure # Question: what happens if we want to reset things? if ( bw is None and not delay and not loss and max_queue_size is None ): return None # Clear existing configuration tcoutput = self.tc( '%s qdisc show dev %s' ) if "priomap" not in tcoutput and "noqueue" not in tcoutput: cmds = [ '%s qdisc del dev %s root' ] else: cmds = [] # Bandwidth limits via various methods bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup, use_hfsc=use_hfsc, use_tbf=use_tbf, latency_ms=latency_ms, enable_ecn=enable_ecn, enable_red=enable_red ) cmds += bwcmds # Delay/jitter/loss/max_queue_size using netem delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter, loss=loss, max_queue_size=max_queue_size, parent=parent ) cmds += delaycmds # Ugly but functional: display configuration info stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) + ( [ '%s delay' % delay ] if delay is not None else [] ) + ( [ '%s jitter' % jitter ] if jitter is not None else [] ) + ( ['%.5f%% loss' % loss ] if loss is not None else [] ) + ( [ 'ECN' ] if enable_ecn else [ 'RED' ] if enable_red else [] ) ) info( '(' + ' '.join( stuff ) + ') ' ) # Execute all the commands in our node debug("at map stage w/cmds: %s\n" % cmds) tcoutputs = [ self.tc(cmd) for cmd in cmds ] for output in tcoutputs: if output != '': error( "*** Error: %s" % output ) debug( "cmds:", cmds, '\n' ) debug( "outputs:", tcoutputs, '\n' ) result[ 'tcoutputs'] = tcoutputs result[ 'parent' ] = parent return result class Link( object ): """A basic link is just a veth pair. Other types of links could be tunnels, link emulators, etc..""" # pylint: disable=too-many-branches def __init__( self, node1, node2, port1=None, port2=None, intfName1=None, intfName2=None, addr1=None, addr2=None, intf=Intf, cls1=None, cls2=None, params1=None, params2=None, fast=True, **params ): """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) params1: parameters for interface 1 (optional) params2: parameters for interface 2 (optional) **params: additional parameters for both interfaces""" # This is a bit awkward; it seems that having everything in # params is more orthogonal, but being able to specify # in-line arguments is more convenient! So we support both. params1 = dict( params1 ) if params1 else {} params2 = dict( params2 ) if params2 else {} if port1 is not None: params1[ 'port' ] = port1 if port2 is not None: params2[ 'port' ] = port2 if 'port' not in params1: params1[ 'port' ] = node1.newPort() if 'port' not in params2: params2[ 'port' ] = node2.newPort() if not intfName1: intfName1 = self.intfName( node1, params1[ 'port' ] ) if not intfName2: intfName2 = self.intfName( node2, params2[ 'port' ] ) # Update with remaining parameter list params1.update( params ) params2.update( params ) self.fast = fast if fast: params1.setdefault( 'moveIntfFn', self._ignore ) params2.setdefault( 'moveIntfFn', self._ignore ) self.makeIntfPair( intfName1, intfName2, addr1, addr2, node1, node2, deleteIntfs=False ) else: self.makeIntfPair( intfName1, intfName2, addr1, addr2 ) if not cls1: cls1 = intf if not cls2: cls2 = intf intf1 = cls1( name=intfName1, node=node1, link=self, mac=addr1, **params1 ) intf2 = cls2( name=intfName2, node=node2, link=self, mac=addr2, **params2 ) # All we are is dust in the wind, and our two interfaces self.intf1, self.intf2 = intf1, intf2 # pylint: enable=too-many-branches @staticmethod def _ignore( *args, **kwargs ): "Ignore any arguments" pass def intfName( self, node, n ): "Construct a canonical interface name node-ethN for interface n." # Leave this as an instance method for now assert self return node.name + '-eth' + repr( n ) @classmethod def makeIntfPair( cls, intfname1, intfname2, addr1=None, addr2=None, node1=None, node2=None, deleteIntfs=True ): """Create pair of interfaces intfname1: name for interface 1 intfname2: name for interface 2 addr1: MAC address for interface 1 (optional) addr2: MAC address for interface 2 (optional) node1: home node for interface 1 (optional) node2: home node for interface 2 (optional) (override this method [and possibly delete()] to change link type)""" # Leave this as a class method for now assert cls return makeIntfPair( intfname1, intfname2, addr1, addr2, node1, node2, deleteIntfs=deleteIntfs ) def delete( self ): "Delete this link" self.intf1.delete() self.intf1 = None self.intf2.delete() self.intf2 = None def stop( self ): "Override to stop and clean up link as needed" self.delete() def status( self ): "Return link status as a string" return "(%s %s)" % ( self.intf1.status(), self.intf2.status() ) def __str__( self ): return '%s<->%s' % ( self.intf1, self.intf2 ) class OVSIntf( Intf ): "Patch interface on an OVSSwitch" def ifconfig( self, *args ): cmd = ' '.join( args ) if cmd == 'up': # OVSIntf is always up return else: raise Exception( 'OVSIntf cannot do ifconfig ' + cmd ) class OVSLink( Link ): """Link that makes patch links between OVSSwitches Warning: in testing we have found that no more than ~64 OVS patch links should be used in row.""" def __init__( self, node1, node2, **kwargs ): "See Link.__init__() for options" if 'OVSSwitch' not in globals(): # pylint: disable=import-outside-toplevel,cyclic-import from mininet.node import OVSSwitch self.isPatchLink = False if ( isinstance( node1, OVSSwitch ) and isinstance( node2, OVSSwitch ) ): self.isPatchLink = True kwargs.update( cls1=OVSIntf, cls2=OVSIntf ) Link.__init__( self, node1, node2, **kwargs ) # pylint: disable=arguments-renamed, arguments-differ, signature-differs def makeIntfPair( self, *args, **kwargs ): "Usually delegated to OVSSwitch" if self.isPatchLink: return None, None else: return Link.makeIntfPair( *args, **kwargs ) class TCLink( Link ): "Link with TC interfaces" def __init__( self, *args, **kwargs): kwargs.setdefault( 'cls1', TCIntf ) kwargs.setdefault( 'cls2', TCIntf ) Link.__init__( self, *args, **kwargs) class TCULink( TCLink ): """TCLink with default settings optimized for UserSwitch (txo=rxo=0/False). Unfortunately with recent Linux kernels, enabling TX and RX checksum offload on veth pairs doesn't work well with UserSwitch: either it gets terrible performance or TCP packets with bad checksums are generated, forwarded, and *dropped* due to having bad checksums! OVS and LinuxBridge seem to cope with this somehow, but it is likely to be an issue with many software Ethernet bridges.""" def __init__( self, *args, **kwargs ): kwargs.update( txo=False, rxo=False ) TCLink.__init__( self, *args, **kwargs )