It appears that under certain conditions, such as when a namespace exits, both ends of a veth pair may get dumped into the root namespace. We therefore now remove an interface both from its home namespace and from the root namespace.
402 lines
15 KiB
Python
402 lines
15 KiB
Python
"""
|
|
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
|
|
from time import sleep
|
|
import re
|
|
|
|
class Intf( object ):
|
|
|
|
"Basic interface object that can configure itself."
|
|
|
|
def __init__( self, name, node=None, port=None, link=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, self.ip, self.prefixLen = None, None, None
|
|
# 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:
|
|
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"
|
|
ifconfig = self.ifconfig()
|
|
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
|
|
|
|
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:
|
|
self.ifconfig( 'up' )
|
|
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 type( value ) is list:
|
|
result = f( *value )
|
|
elif type( value ) is 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 )
|
|
self.updateIP()
|
|
self.updateMAC()
|
|
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 __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
|
|
cmds = [ '%s qdisc del dev %s root' ]
|
|
|
|
# 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 ]
|
|
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,
|
|
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 would be more orthogonal, but being able to specify
|
|
# in-line arguments is more convenient!
|
|
if port1 is None:
|
|
port1 = node1.newPort()
|
|
if port2 is None:
|
|
port2 = node2.newPort()
|
|
if not intfName1:
|
|
intfName1 = self.intfName( node1, port1 )
|
|
if not intfName2:
|
|
intfName2 = self.intfName( node2, port2 )
|
|
|
|
self.makeIntfPair( intfName1, intfName2 )
|
|
|
|
if not cls1:
|
|
cls1 = intf
|
|
if not cls2:
|
|
cls2 = intf
|
|
if not params1:
|
|
params1 = {}
|
|
if not params2:
|
|
params2 = {}
|
|
|
|
intf1 = cls1( name=intfName1, node=node1, port=port1,
|
|
link=self, **params1 )
|
|
intf2 = cls2( name=intfName2, node=node2, port=port2,
|
|
link=self, **params2 )
|
|
|
|
# All we are is dust in the wind, and our two interfaces
|
|
self.intf1, self.intf2 = intf1, intf2
|
|
|
|
@classmethod
|
|
def intfName( cls, node, n ):
|
|
"Construct a canonical interface name node-ethN for interface n."
|
|
return node.name + '-eth' + repr( n )
|
|
|
|
@classmethod
|
|
def makeIntfPair( cls, intf1, intf2 ):
|
|
"""Create pair of interfaces
|
|
intf1: name of interface 1
|
|
intf2: name of interface 2
|
|
(override this class method [and possibly delete()]
|
|
to change link type)"""
|
|
makeIntfPair( intf1, intf2 )
|
|
|
|
def delete( self ):
|
|
"Delete this link"
|
|
self.intf1.delete()
|
|
self.intf2.delete()
|
|
|
|
def __str__( self ):
|
|
return '%s<->%s' % ( self.intf1, self.intf2 )
|
|
|
|
class TCLink( Link ):
|
|
"Link with symmetric TC interfaces configured via opts"
|
|
def __init__( self, node1, node2, port1=None, port2=None,
|
|
intfName1=None, intfName2=None, **params ):
|
|
Link.__init__( self, node1, node2, port1=port1, port2=port2,
|
|
intfName1=intfName1, intfName2=intfName2,
|
|
cls1=TCIntf,
|
|
cls2=TCIntf,
|
|
params1=params,
|
|
params2=params)
|