""" 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 """ from mininet.log import info, error, debug from mininet.util import makeIntfPair, quietRun import mininet.node import re 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' # Add to node (and move ourselves if necessary ) 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" 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 = param.items()[ 0 ] f = getattr( self, method, None ) if not f or value is None: return 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 ) if self.node.inNamespace: # Link may have been dumped into root NS quietRun( 'ip link del ' + self.name ) 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""" 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 > 1000 ): error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\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 * 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 delay and delay < 0: error( 'Negative delay', delay, '\n' ) elif jitter and jitter < 0: error( 'Negative jitter', jitter, '\n' ) elif 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 %d ' % loss if loss is not None 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( self, bw=None, delay=None, jitter=None, loss=None, disable_gro=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." result = Intf.config( self, **params) # Disable GRO if disable_gro: self.cmd( 'ethtool -K %s gro off' % self ) # 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 # Clear existing configuration tcoutput = self.tc( '%s qdisc show dev %s' ) if "priomap" 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 [] ) + ( ['%d%% 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..""" 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 ): """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 params2: parameters for interface 2""" # 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. if params1 is None: params1 = {} if params2 is None: params2 = {} # Allow passing in params1=params2 if params2 is params1: params2 = dict( params1 ) 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' ] ) 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 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 ): """Create pair of interfaces intfname1: name of interface 1 intfname2: name of interface 2 (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 ) def delete( self ): "Delete this link" self.intf1.delete() self.intf2.delete() def stop( self ): "Override to stop and clean up link as needed" pass 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, cmd ): 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" def __init__( self, node1, node2, **kwargs ): "See Link.__init__() for options" self.isPatchLink = False if ( type( node1 ) is mininet.node.OVSSwitch and type( node2 ) is mininet.node.OVSSwitch ): self.isPatchLink = True kwargs.update( cls1=OVSIntf, cls2=OVSIntf ) Link.__init__( self, node1, node2, **kwargs ) 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 symmetric TC interfaces configured via opts" def __init__( self, node1, node2, port1=None, port2=None, intfName1=None, intfName2=None, addr1=None, addr2=None, **params ): Link.__init__( self, node1, node2, port1=port1, port2=port2, intfName1=intfName1, intfName2=intfName2, cls1=TCIntf, cls2=TCIntf, addr1=addr1, addr2=addr2, params1=params, params2=params )