Merge pull request #416 from mininet/devel/multitopo

Multi-link topology support
This commit is contained in:
lantz
2014-11-07 16:16:25 -08:00
5 changed files with 310 additions and 129 deletions
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/python
"""
This is a simple example that demonstrates multiple links
between nodes.
"""
from mininet.cli import CLI
from mininet.log import lg, info
from mininet.net import Mininet
from mininet.topo import Topo
def runMultiLink():
topo = simpleMultiLinkTopo( n=2 )
net = Mininet( topo=topo )
net.start()
CLI( net )
net.stop()
class simpleMultiLinkTopo( Topo ):
def __init__( self, n, **kwargs ):
Topo.__init__( self, **kwargs )
h1, h2 = self.addHost( 'h1' ), self.addHost( 'h2' )
s1 = self.addSwitch( 's1' )
for _ in range( n ):
self.addLink( s1, h1 )
self.addLink( s1, h2 )
if __name__ == '__main__':
lg.setLogLevel( 'info' )
runMultiLink()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python
'''
Test for multiple links between nodes
validates mininet interfaces against systems interfaces
'''
import unittest
import pexpect
class testMultiLink( unittest.TestCase ):
prompt = 'mininet>'
def testMultiLink(self):
p = pexpect.spawn( 'python -m mininet.examples.multiLink' )
p.expect( self.prompt )
p.sendline( 'intfs' )
p.expect( 's(\d): lo' )
intfsOutput = p.before
# parse interfaces from mininet intfs, and store them in a list
hostToIntfs = intfsOutput.split( '\r\n' )[ 1:3 ]
intfList = []
for hostToIntf in hostToIntfs:
intfList += [ intf for intf in
hostToIntf.split()[1].split(',') ]
# get interfaces from system by running ifconfig on every host
sysIntfList = []
opts = [ 'h(\d)-eth(\d)', self.prompt ]
p.expect( self.prompt )
p.sendline( 'h1 ifconfig' )
while True:
p.expect( opts )
if p.after == self.prompt:
break
sysIntfList.append( p.after )
p.sendline( 'h2 ifconfig' )
while True:
p.expect( opts )
if p.after == self.prompt:
break
sysIntfList.append( p.after )
failMsg = ( 'The systems interfaces and mininet interfaces\n'
'are not the same' )
self.assertEqual( sysIntfList, intfList, msg=failMsg )
p.sendline( 'exit' )
p.wait()
if __name__ == '__main__':
unittest.main()
+30 -21
View File
@@ -321,25 +321,36 @@ class Mininet( object ):
"return (key,value) tuple list for every node in net" "return (key,value) tuple list for every node in net"
return zip( self.keys(), self.values() ) return zip( self.keys(), self.values() )
@staticmethod
def randMac():
"Return a random, non-multicast MAC address"
return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff |
0x020000000000 )
def addLink( self, node1, node2, port1=None, port2=None, def addLink( self, node1, node2, port1=None, port2=None,
cls=None, **params ): cls=None, **params ):
""""Add a link from node1 to node2 """"Add a link from node1 to node2
node1: source node node1: source node (or name)
node2: dest node node2: dest node (or name)
port1: source port port1: source port (optional)
port2: dest port port2: dest port (optional)
cls: link class (optional)
params: additional link params (optional)
returns: link object""" returns: link object"""
mac1 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | 0x020000000000 ) # Accept node objects or names
mac2 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | 0x020000000000 ) node1 = node1 if type( node1 ) != str else self[ node1 ]
defaults = { 'port1': port1, node2 = node2 if type( node2 ) != str else self[ node2 ]
'port2': port2, options = dict( params )
'addr1': mac1, # Port is optional
'addr2': mac2, if port1 is not None:
'intf': self.intf } options.setdefault( 'port1', port1 )
defaults.update( params ) if port2 is not None:
if not cls: options.setdefault( 'port2', port2 )
cls = self.link # Set default MAC - this should probably be in Link
link = cls( node1, node2, **defaults ) options.setdefault( 'addr1', self.randMac() )
options.setdefault( 'addr2', self.randMac() )
cls = self.link if cls is None else cls
link = cls( node1, node2, **options )
self.links.append( link ) self.links.append( link )
return link return link
@@ -397,12 +408,10 @@ class Mininet( object ):
info( switchName + ' ' ) info( switchName + ' ' )
info( '\n*** Adding links:\n' ) info( '\n*** Adding links:\n' )
for srcName, dstName in topo.links(sort=True): for srcName, dstName, params in topo.links(
src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ] sort=True, withInfo=True ):
params = topo.linkInfo( srcName, dstName ) self.addLink( **params )
srcPort, dstPort = topo.port( srcName, dstName ) info( '(%s, %s) ' % ( srcName, dstName ) )
self.addLink( src, dst, srcPort, dstPort, **params )
info( '(%s, %s) ' % ( src.name, dst.name ) )
info( '\n' ) info( '\n' )
+186 -104
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
'''@package topo """@package topo
Network topology creation. Network topology creation.
@@ -9,46 +9,95 @@ This package includes code to represent network topologies.
A Topo object can be a topology database for NOX, can represent a physical A Topo object can be a topology database for NOX, can represent a physical
setup for testing, and can even be emulated with the Mininet package. setup for testing, and can even be emulated with the Mininet package.
''' """
from mininet.util import irange, natural, naturalSeq from mininet.util import irange, natural, naturalSeq
class MultiGraph( object ): class MultiGraph( object ):
"Utility class to track nodes and edges - replaces networkx.Graph" "Utility class to track nodes and edges - replaces networkx.MultiGraph"
def __init__( self ): def __init__( self ):
self.data = {} self.node = {}
self.edge = {}
def add_node( self, node ): def add_node( self, node, attr_dict=None, **attrs):
"Add node to graph" """Add node to graph
self.data.setdefault( node, [] ) attr_dict: attribute dict (optional)
attrs: more attributes (optional)"""
attr_dict = {} if attr_dict is None else attr_dict
attr_dict.update( attrs )
self.node[ node ] = attr_dict
def add_edge( self, src, dest ): def add_edge( self, src, dst, key=None, attr_dict=None, **attrs ):
"Add edge to graph" """Add edge to graph
src, dest = sorted( ( src, dest ) ) key: optional key
self.add_node( src ) attr_dict: optional attribute dict"""
self.add_node( dest ) attr_dict = {} if attr_dict is None else attr_dict
self.data[ src ].append( dest ) attr_dict.update( attrs )
self.node.setdefault( src, {} )
self.node.setdefault( dst, {} )
self.edge.setdefault( src, {} )
self.edge.setdefault( dst, {} )
self.edge[ src ].setdefault( dst, {} )
entry = self.edge[ dst ][ src ] = self.edge[ src ][ dst ]
# If no key, pick next ordinal number
if key is None:
keys = [ k for k in entry.keys() if type( k ) is int ]
key = max( [ 0 ] + keys ) + 1
entry[ key ] = attr_dict
return key
def nodes( self ): def nodes( self, data=False):
"Return list of graph nodes" """Return list of graph nodes
return self.data.keys() data: return list of ( node, attrs)"""
return self.node.items() if data else self.node.keys()
def edges( self ): def edges_iter( self, data=False, keys=False ):
"Iterator: return graph edges" "Iterator: return graph edges"
for src in self.data.keys(): for src, entry in self.edge.iteritems():
for dest in self.data[ src ]: for dst, keys in entry.iteritems():
yield ( src, dest ) if src > dst:
# Skip duplicate edges
continue
for k, attrs in keys.iteritems():
if data:
if keys:
yield( src, dst, k, attrs )
else:
yield( src, dst, attrs )
else:
if keys:
yield( src, dst, k )
else:
yield( src, dst )
def edges( self, data=False, keys=False ):
"Return list of graph edges"
return list( self.edges_iter( data=data, keys=keys ) )
def __getitem__( self, node ): def __getitem__( self, node ):
"Return link dict for the given node" "Return link dict for given src node"
return self.data[node] return self.edge[ node ]
def __len__( self ):
"Return the number of nodes"
return len( self.node )
def convertTo( self, cls, data=False, keys=False ):
"""Convert to a new object of networkx.MultiGraph-like class cls
data: include node and edge data
keys: include edge keys as well as edge data"""
g = cls()
g.add_nodes_from( self.nodes( data=data ) )
g.add_edges_from( self.edges( data=( data or keys ), keys=keys ) )
return g
class Topo(object): class Topo( object ):
"Data center network representation for structured multi-trees." "Data center network representation for structured multi-trees."
def __init__(self, *args, **params): def __init__( self, *args, **params ):
"""Topo object. """Topo object.
Optional named parameters: Optional named parameters:
hinfo: default host options hinfo: default host options
@@ -56,150 +105,182 @@ class Topo(object):
lopts: default link options lopts: default link options
calls build()""" calls build()"""
self.g = MultiGraph() self.g = MultiGraph()
self.node_info = {}
self.link_info = {} # (src, dst) tuples hash to EdgeInfo objects
self.hopts = params.pop( 'hopts', {} ) self.hopts = params.pop( 'hopts', {} )
self.sopts = params.pop( 'sopts', {} ) self.sopts = params.pop( 'sopts', {} )
self.lopts = params.pop( 'lopts', {} ) self.lopts = params.pop( 'lopts', {} )
self.ports = {} # ports[src][dst] is port on src that connects to dst self.ports = {} # ports[src][dst][sport] is port on dst that connects to src
self.build( *args, **params ) self.build( *args, **params )
def build( self, *args, **params ): def build( self, *args, **params ):
"Override this method to build your topology." "Override this method to build your topology."
pass pass
def addNode(self, name, **opts): def addNode( self, name, **opts ):
"""Add Node to graph. """Add Node to graph.
name: name name: name
opts: node options opts: node options
returns: node name""" returns: node name"""
self.g.add_node(name) self.g.add_node( name, **opts )
self.node_info[name] = opts
return name return name
def addHost(self, name, **opts): def addHost( self, name, **opts ):
"""Convenience method: Add host to graph. """Convenience method: Add host to graph.
name: host name name: host name
opts: host options opts: host options
returns: host name""" returns: host name"""
if not opts and self.hopts: if not opts and self.hopts:
opts = self.hopts opts = self.hopts
return self.addNode(name, **opts) return self.addNode( name, **opts )
def addSwitch(self, name, **opts): def addSwitch( self, name, **opts ):
"""Convenience method: Add switch to graph. """Convenience method: Add switch to graph.
name: switch name name: switch name
opts: switch options opts: switch options
returns: switch name""" returns: switch name"""
if not opts and self.sopts: if not opts and self.sopts:
opts = self.sopts opts = self.sopts
result = self.addNode(name, isSwitch=True, **opts) result = self.addNode( name, isSwitch=True, **opts )
return result return result
def addLink(self, node1, node2, port1=None, port2=None, def addLink( self, node1, node2, port1=None, port2=None,
**opts): key=None, **opts ):
"""node1, node2: nodes to link together """node1, node2: nodes to link together
port1, port2: ports (optional) port1, port2: ports (optional)
opts: link options (optional) opts: link options (optional)
returns: link info key""" returns: link info key"""
if not opts and self.lopts: if not opts and self.lopts:
opts = self.lopts opts = self.lopts
self.addPort(node1, node2, port1, port2) port1, port2 = self.addPort( node1, node2, port1, port2 )
key = tuple(self.sorted([node1, node2])) opts.update( node1=node1, node2=node2, port1=port1, port2=port2 )
self.link_info[key] = opts self.g.add_edge(node1, node2, key, opts )
self.g.add_edge(*key)
return key return key
def addPort(self, src, dst, sport=None, dport=None): def nodes( self, sort=True ):
'''Generate port mapping for new edge.
@param src source switch name
@param dst destination switch name
'''
self.ports.setdefault(src, {})
self.ports.setdefault(dst, {})
# New port: number of outlinks + base
src_base = 1 if self.isSwitch(src) else 0
dst_base = 1 if self.isSwitch(dst) else 0
if sport is None:
sport = len(self.ports[src]) + src_base
if dport is None:
dport = len(self.ports[dst]) + dst_base
self.ports[src][dst] = sport
self.ports[dst][src] = dport
def nodes(self, sort=True):
"Return nodes in graph" "Return nodes in graph"
if sort: if sort:
return self.sorted( self.g.nodes() ) return self.sorted( self.g.nodes() )
else: else:
return self.g.nodes() return self.g.nodes()
def isSwitch(self, n): def isSwitch( self, n ):
'''Returns true if node is a switch.''' "Returns true if node is a switch."
info = self.node_info[n] return self.g.node[ n ].get( 'isSwitch', False )
return info and info.get('isSwitch', False)
def switches(self, sort=True): def switches( self, sort=True ):
'''Return switches. """Return switches.
sort: sort switches alphabetically sort: sort switches alphabetically
@return dpids list of dpids returns: dpids list of dpids"""
''' return [ n for n in self.nodes( sort ) if self.isSwitch( n ) ]
return [n for n in self.nodes(sort) if self.isSwitch(n)]
def hosts(self, sort=True): def hosts( self, sort=True ):
'''Return hosts. """Return hosts.
sort: sort hosts alphabetically sort: sort hosts alphabetically
@return dpids list of dpids returns: list of hosts"""
''' return [ n for n in self.nodes( sort ) if not self.isSwitch( n ) ]
return [n for n in self.nodes(sort) if not self.isSwitch(n)]
def links(self, sort=True): def iterLinks( self, withKeys=False, withInfo=False ):
'''Return links. """Return links (iterator)
sort: sort links alphabetically withKeys: return link keys
@return links list of name pairs withInfo: return link info
''' returns: list of ( src, dst [,key, info ] )"""
if not sort: for src, dst, key, info in self.g.edges_iter( data=True, keys=True ):
return self.g.edges() node1, node2 = info[ 'node1' ], info[ 'node2' ]
if withKeys:
if withInfo:
yield( node1, node2, key, info )
else: else:
links = [tuple(self.sorted(e)) for e in self.g.edges()] yield( node1, node2, key )
return sorted( links, key=naturalSeq ) else:
if withInfo:
yield( node1, node2, info )
else:
yield( node1, node2 )
def port(self, src, dst): def links( self, sort=False, withKeys=False, withInfo=False ):
'''Get port number. """Return links
sort: sort links alphabetically, preserving (src, dst) order
withKeys: return link keys
withInfo: return link info
returns: list of ( src, dst [,key, info ] )"""
links = list( self.iterLinks( withKeys, withInfo ) )
if not sorted:
return links
# Ignore info when sorting
tupleSize = 3 if withKeys else 2
return sorted( links, key=( lambda l: naturalSeq( l[ :tupleSize ] ) ) )
@param src source switch name # This legacy port management mechanism is clunky and will probably
@param dst destination switch name # be removed at some point.
@return tuple (src_port, dst_port):
src_port: port on source switch leading to the destination switch
dst_port: port on destination switch leading to the source switch
'''
if src in self.ports and dst in self.ports[src]:
assert dst in self.ports and src in self.ports[dst]
return self.ports[src][dst], self.ports[dst][src]
def linkInfo( self, src, dst ): def addPort( self, src, dst, sport=None, dport=None ):
"Return link metadata" """Generate port mapping for new edge.
src, dst = self.sorted([src, dst]) src: source switch name
return self.link_info[(src, dst)] dst: destination switch name"""
# Initialize if necessary
ports = self.ports
ports.setdefault( src, {} )
ports.setdefault( dst, {} )
# New port: number of outlinks + base
if sport is None:
src_base = 1 if self.isSwitch( src ) else 0
sport = len( ports[ src ] ) + src_base
if dport is None:
dst_base = 1 if self.isSwitch( dst ) else 0
dport = len( ports[ dst ] ) + dst_base
ports[ src ][ sport ] = ( dst, dport )
ports[ dst ][ dport ] = ( src, sport )
return sport, dport
def setlinkInfo( self, src, dst, info ): def port( self, src, dst ):
"Set link metadata" """Get port numbers.
src, dst = self.sorted([src, dst]) src: source switch name
self.link_info[(src, dst)] = info dst: destination switch name
sport: optional source port (otherwise use lowest src port)
returns: tuple (sport, dport), where
sport = port on source switch leading to the destination switch
dport = port on destination switch leading to the source switch
Note that you can also look up ports using linkInfo()"""
# A bit ugly and slow vs. single-link implementation ;-(
ports = [ ( sport, entry[ 1 ] )
for sport, entry in self.ports[ src ].items()
if entry[ 0 ] == dst ]
return ports if len( ports ) != 1 else ports[ 0 ]
def _linkEntry( self, src, dst, key=None ):
"Helper function: return link entry and key"
entry = self.g[ src ][ dst ]
if key is None:
key = min( entry )
return entry, key
def linkInfo( self, src, dst, key=None ):
"Return link metadata dict"
entry, key = self._linkEntry( src, dst, key )
return entry[ key ]
def setlinkInfo( self, src, dst, info, key=None ):
"Set link metadata dict"
entry, key = self._linkEntry( src, dst, key )
entry[ key ] = info
def nodeInfo( self, name ): def nodeInfo( self, name ):
"Return metadata (dict) for node" "Return metadata (dict) for node"
info = self.node_info[ name ] return self.g.node[ name ]
return info if info is not None else {}
def setNodeInfo( self, name, info ): def setNodeInfo( self, name, info ):
"Set metadata (dict) for node" "Set metadata (dict) for node"
self.node_info[ name ] = info self.g.node[ name ] = info
def convertTo( self, cls, data=True, keys=True ):
"""Convert to a new object of networkx.MultiGraph-like class cls
data: include node and edge data (default True)
keys: include edge keys as well as edge data (default True)"""
return self.g.convertTo( cls, data=data, keys=keys )
@staticmethod @staticmethod
def sorted( items ): def sorted( items ):
"Items sorted in natural (i.e. alphabetical) order" "Items sorted in natural (i.e. alphabetical) order"
return sorted(items, key=natural) return sorted( items, key=natural )
class SingleSwitchTopo( Topo ): class SingleSwitchTopo( Topo ):
@@ -228,6 +309,7 @@ class SingleSwitchReversedTopo( Topo ):
self.addLink( host, switch, self.addLink( host, switch,
port1=0, port2=( k - h + 1 ) ) port1=0, port2=( k - h + 1 ) )
class LinearTopo( Topo ): class LinearTopo( Topo ):
"Linear topology of k switches, with n hosts per switch." "Linear topology of k switches, with n hosts per switch."
+1 -1
View File
@@ -448,7 +448,7 @@ def natural( text ):
def num( s ): def num( s ):
"Convert text segment to int if necessary" "Convert text segment to int if necessary"
return int( s ) if s.isdigit() else s return int( s ) if s.isdigit() else s
return [ num( s ) for s in re.split( r'(\d+)', text ) ] return [ num( s ) for s in re.split( r'(\d+)', str( text ) ) ]
def naturalSeq( t ): def naturalSeq( t ):
"Natural sort key function for sequences" "Natural sort key function for sequences"