You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
12 KiB
Python
359 lines
12 KiB
Python
#!/usr/bin/env python
|
|
"""@package topo
|
|
|
|
Network topology creation.
|
|
|
|
@author Brandon Heller (brandonh@stanford.edu)
|
|
|
|
This package includes code to represent network topologies.
|
|
|
|
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.
|
|
"""
|
|
|
|
from mininet.util import irange, natural, naturalSeq
|
|
|
|
# pylint: disable=too-many-arguments
|
|
|
|
|
|
class MultiGraph( object ):
|
|
"Utility class to track nodes and edges - replaces networkx.MultiGraph"
|
|
|
|
def __init__( self ):
|
|
self.node = {}
|
|
self.edge = {}
|
|
|
|
def add_node( self, node, attr_dict=None, **attrs):
|
|
"""Add node to graph
|
|
attr_dict: attribute dict (optional)
|
|
attrs: more attributes (optional)
|
|
warning: updates attr_dict with attrs"""
|
|
attr_dict = {} if attr_dict is None else attr_dict
|
|
attr_dict.update( attrs )
|
|
self.node[ node ] = attr_dict
|
|
|
|
def add_edge( self, src, dst, key=None, attr_dict=None, **attrs ):
|
|
"""Add edge to graph
|
|
key: optional key
|
|
attr_dict: optional attribute dict
|
|
attrs: more attributes
|
|
warning: updates attr_dict with attrs"""
|
|
attr_dict = {} if attr_dict is None else attr_dict
|
|
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 isinstance( k, int ) ]
|
|
key = max( [ 0 ] + keys ) + 1
|
|
entry[ key ] = attr_dict
|
|
return key
|
|
|
|
def nodes( self, data=False):
|
|
"""Return list of graph nodes
|
|
data: return list of ( node, attrs)"""
|
|
return self.node.items() if data else self.node.keys()
|
|
|
|
def edges_iter( self, data=False, keys=False ):
|
|
"Iterator: return graph edges, optionally with data and keys"
|
|
for src, entry in self.edge.items():
|
|
for dst, entrykeys in entry.items():
|
|
if src > dst:
|
|
# Skip duplicate edges
|
|
continue
|
|
for k, attrs in entrykeys.items():
|
|
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 ):
|
|
"Return link dict for given src 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 ):
|
|
"Data center network representation for structured multi-trees."
|
|
|
|
def __init__( self, *args, **params ):
|
|
"""Topo object.
|
|
Optional named parameters:
|
|
hinfo: default host options
|
|
sopts: default switch options
|
|
lopts: default link options
|
|
calls build()"""
|
|
self.g = MultiGraph()
|
|
self.hopts = params.pop( 'hopts', {} )
|
|
self.sopts = params.pop( 'sopts', {} )
|
|
self.lopts = params.pop( 'lopts', {} )
|
|
# ports[src][dst][sport] is port on dst that connects to src
|
|
self.ports = {}
|
|
self.build( *args, **params )
|
|
|
|
def build( self, *args, **params ):
|
|
"Override this method to build your topology."
|
|
pass
|
|
|
|
def addNode( self, name, **opts ):
|
|
"""Add Node to graph.
|
|
name: name
|
|
opts: node options
|
|
returns: node name"""
|
|
self.g.add_node( name, **opts )
|
|
return name
|
|
|
|
def addHost( self, name, **opts ):
|
|
"""Convenience method: Add host to graph.
|
|
name: host name
|
|
opts: host options
|
|
returns: host name"""
|
|
if not opts and self.hopts:
|
|
opts = self.hopts
|
|
return self.addNode( name, **opts )
|
|
|
|
def addSwitch( self, name, **opts ):
|
|
"""Convenience method: Add switch to graph.
|
|
name: switch name
|
|
opts: switch options
|
|
returns: switch name"""
|
|
if not opts and self.sopts:
|
|
opts = self.sopts
|
|
result = self.addNode( name, isSwitch=True, **opts )
|
|
return result
|
|
|
|
def addLink( self, node1, node2, port1=None, port2=None,
|
|
key=None, **opts ):
|
|
"""node1, node2: nodes to link together
|
|
port1, port2: ports (optional)
|
|
opts: link options (optional)
|
|
returns: link info key"""
|
|
if not opts and self.lopts:
|
|
opts = self.lopts
|
|
port1, port2 = self.addPort( node1, node2, port1, port2 )
|
|
opts = dict( opts )
|
|
opts.update( node1=node1, node2=node2, port1=port1, port2=port2 )
|
|
return self.g.add_edge(node1, node2, key, opts )
|
|
|
|
def nodes( self, sort=True ):
|
|
"Return nodes in graph"
|
|
if sort:
|
|
return self.sorted( self.g.nodes() )
|
|
else:
|
|
return self.g.nodes()
|
|
|
|
def isSwitch( self, n ):
|
|
"Returns true if node is a switch."
|
|
return self.g.node[ n ].get( 'isSwitch', False )
|
|
|
|
def switches( self, sort=True ):
|
|
"""Return switches.
|
|
sort: sort switches alphabetically
|
|
returns: dpids list of dpids"""
|
|
return [ n for n in self.nodes( sort ) if self.isSwitch( n ) ]
|
|
|
|
def hosts( self, sort=True ):
|
|
"""Return hosts.
|
|
sort: sort hosts alphabetically
|
|
returns: list of hosts"""
|
|
return [ n for n in self.nodes( sort ) if not self.isSwitch( n ) ]
|
|
|
|
def iterLinks( self, withKeys=False, withInfo=False ):
|
|
"""Return links (iterator)
|
|
withKeys: return link keys
|
|
withInfo: return link info
|
|
returns: list of ( src, dst [,key, info ] )"""
|
|
for _src, _dst, key, info in self.g.edges_iter( data=True, keys=True ):
|
|
node1, node2 = info[ 'node1' ], info[ 'node2' ]
|
|
if withKeys:
|
|
if withInfo:
|
|
yield( node1, node2, key, info )
|
|
else:
|
|
yield( node1, node2, key )
|
|
else:
|
|
if withInfo:
|
|
yield( node1, node2, info )
|
|
else:
|
|
yield( node1, node2 )
|
|
|
|
def links( self, sort=False, withKeys=False, withInfo=False ):
|
|
"""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 sort:
|
|
return links
|
|
# Ignore info when sorting
|
|
tupleSize = 3 if withKeys else 2
|
|
return sorted( links, key=( lambda l: naturalSeq( l[ :tupleSize ] ) ) )
|
|
|
|
# This legacy port management mechanism is clunky and will probably
|
|
# be removed at some point.
|
|
|
|
def addPort( self, src, dst, sport=None, dport=None ):
|
|
"""Generate port mapping for new edge.
|
|
src: source switch name
|
|
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 port( self, src, dst ):
|
|
"""Get port numbers.
|
|
src: source switch name
|
|
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 ):
|
|
"Return metadata (dict) for node"
|
|
return self.g.node[ name ]
|
|
|
|
def setNodeInfo( self, name, info ):
|
|
"Set metadata (dict) for node"
|
|
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
|
|
def sorted( items ):
|
|
"Items sorted in natural (i.e. alphabetical) order"
|
|
return sorted( items, key=natural )
|
|
|
|
|
|
# Our idiom defines additional parameters in build(param...)
|
|
# pylint: disable=arguments-differ
|
|
|
|
class SingleSwitchTopo( Topo ):
|
|
"Single switch connected to k hosts."
|
|
|
|
def build( self, k=2, **_opts ):
|
|
"k: number of hosts"
|
|
self.k = k
|
|
switch = self.addSwitch( 's1' )
|
|
for h in irange( 1, k ):
|
|
host = self.addHost( 'h%s' % h )
|
|
self.addLink( host, switch )
|
|
|
|
|
|
class SingleSwitchReversedTopo( Topo ):
|
|
"""Single switch connected to k hosts, with reversed ports.
|
|
The lowest-numbered host is connected to the highest-numbered port.
|
|
Useful to verify that Mininet properly handles custom port
|
|
numberings."""
|
|
|
|
def build( self, k=2 ):
|
|
"k: number of hosts"
|
|
self.k = k
|
|
switch = self.addSwitch( 's1' )
|
|
for h in irange( 1, k ):
|
|
host = self.addHost( 'h%s' % h )
|
|
self.addLink( host, switch,
|
|
port1=0, port2=( k - h + 1 ) )
|
|
|
|
|
|
class MinimalTopo( SingleSwitchTopo ):
|
|
"Minimal topology with two hosts and one switch"
|
|
def build( self ):
|
|
return SingleSwitchTopo.build( self, k=2 )
|
|
|
|
|
|
class LinearTopo( Topo ):
|
|
"Linear topology of k switches, with n hosts per switch."
|
|
|
|
def build( self, k=2, n=1, **_opts):
|
|
"""k: number of switches
|
|
n: number of hosts per switch"""
|
|
self.k = k
|
|
self.n = n
|
|
|
|
if n == 1:
|
|
def genHostName( i, _j ):
|
|
return 'h%s' % i
|
|
else:
|
|
def genHostName( i, j ):
|
|
return 'h%ss%d' % ( j, i )
|
|
|
|
lastSwitch = None
|
|
for i in irange( 1, k ):
|
|
# Add switch
|
|
switch = self.addSwitch( 's%s' % i )
|
|
# Add hosts to switch
|
|
for j in irange( 1, n ):
|
|
host = self.addHost( genHostName( i, j ) )
|
|
self.addLink( host, switch )
|
|
# Connect switch to previous
|
|
if lastSwitch:
|
|
self.addLink( switch, lastSwitch )
|
|
lastSwitch = switch
|
|
|
|
# pylint: enable=arguments-differ
|