diff --git a/examples/multiLink.py b/examples/multiLink.py new file mode 100755 index 0000000000000000000000000000000000000000..28a67a80baace4cdb25c1e980d8a0801d957b754 --- /dev/null +++ b/examples/multiLink.py @@ -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() diff --git a/examples/test/test_multiLink.py b/examples/test/test_multiLink.py new file mode 100755 index 0000000000000000000000000000000000000000..6b4bca24b075180f38cf53fe294e4f552d2657fe --- /dev/null +++ b/examples/test/test_multiLink.py @@ -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() diff --git a/mininet/net.py b/mininet/net.py index b74b93002c79cf04c7b35f718d7d674788a47f96..8f89babbab21c1b149a13e2271d833ddf286d67e 100755 --- a/mininet/net.py +++ b/mininet/net.py @@ -321,25 +321,36 @@ def items( self ): "return (key,value) tuple list for every node in net" 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, cls=None, **params ): """"Add a link from node1 to node2 - node1: source node - node2: dest node - port1: source port - port2: dest port + node1: source node (or name) + node2: dest node (or name) + port1: source port (optional) + port2: dest port (optional) + cls: link class (optional) + params: additional link params (optional) returns: link object""" - mac1 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | 0x020000000000 ) - mac2 = macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | 0x020000000000 ) - defaults = { 'port1': port1, - 'port2': port2, - 'addr1': mac1, - 'addr2': mac2, - 'intf': self.intf } - defaults.update( params ) - if not cls: - cls = self.link - link = cls( node1, node2, **defaults ) + # Accept node objects or names + node1 = node1 if type( node1 ) != str else self[ node1 ] + node2 = node2 if type( node2 ) != str else self[ node2 ] + options = dict( params ) + # Port is optional + if port1 is not None: + options.setdefault( 'port1', port1 ) + if port2 is not None: + options.setdefault( 'port2', port2 ) + # Set default MAC - this should probably be in Link + 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 ) return link @@ -397,12 +408,10 @@ def buildFromTopo( self, topo=None ): info( switchName + ' ' ) info( '\n*** Adding links:\n' ) - for srcName, dstName in topo.links(sort=True): - src, dst = self.nameToNode[ srcName ], self.nameToNode[ dstName ] - params = topo.linkInfo( srcName, dstName ) - srcPort, dstPort = topo.port( srcName, dstName ) - self.addLink( src, dst, srcPort, dstPort, **params ) - info( '(%s, %s) ' % ( src.name, dst.name ) ) + for srcName, dstName, params in topo.links( + sort=True, withInfo=True ): + self.addLink( **params ) + info( '(%s, %s) ' % ( srcName, dstName ) ) info( '\n' ) diff --git a/mininet/topo.py b/mininet/topo.py index cc458fd87602e53dc0afab9616d098ef514f4167..af3eac5f55707d83217d85b5101ed27134462fb0 100644 --- a/mininet/topo.py +++ b/mininet/topo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -'''@package topo +"""@package topo Network topology creation. @@ -9,46 +9,95 @@ 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 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 ): - self.data = {} - - def add_node( self, node ): - "Add node to graph" - self.data.setdefault( node, [] ) - - def add_edge( self, src, dest ): - "Add edge to graph" - src, dest = sorted( ( src, dest ) ) - self.add_node( src ) - self.add_node( dest ) - self.data[ src ].append( dest ) + 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)""" + 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""" + 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 type( k ) is int ] + key = max( [ 0 ] + keys ) + 1 + entry[ key ] = attr_dict + return key - def nodes( self ): - "Return list of graph nodes" - return self.data.keys() + 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( self ): + def edges_iter( self, data=False, keys=False ): "Iterator: return graph edges" - for src in self.data.keys(): - for dest in self.data[ src ]: - yield ( src, dest ) + for src, entry in self.edge.iteritems(): + for dst, keys in entry.iteritems(): + 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 ): - "Return link dict for the given node" - return self.data[node] + "Return link dict for given src node" + return self.edge[ node ] + def __len__( self ): + "Return the number of nodes" + return len( self.node ) -class Topo(object): + 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): + def __init__( self, *args, **params ): """Topo object. Optional named parameters: hinfo: default host options @@ -56,150 +105,182 @@ def __init__(self, *args, **params): lopts: default link options calls build()""" self.g = MultiGraph() - self.node_info = {} - self.link_info = {} # (src, dst) tuples hash to EdgeInfo objects self.hopts = params.pop( 'hopts', {} ) self.sopts = params.pop( 'sopts', {} ) 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 ) def build( self, *args, **params ): "Override this method to build your topology." pass - def addNode(self, name, **opts): + def addNode( self, name, **opts ): """Add Node to graph. name: name opts: node options returns: node name""" - self.g.add_node(name) - self.node_info[name] = opts + self.g.add_node( name, **opts ) return name - def addHost(self, name, **opts): + 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) + return self.addNode( name, **opts ) - def addSwitch(self, 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) + result = self.addNode( name, isSwitch=True, **opts ) return result - def addLink(self, node1, node2, port1=None, port2=None, - **opts): + 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 - self.addPort(node1, node2, port1, port2) - key = tuple(self.sorted([node1, node2])) - self.link_info[key] = opts - self.g.add_edge(*key) + port1, port2 = self.addPort( node1, node2, port1, port2 ) + opts.update( node1=node1, node2=node2, port1=port1, port2=port2 ) + self.g.add_edge(node1, node2, key, opts ) return key - def addPort(self, src, dst, sport=None, dport=None): - '''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): + 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.''' - info = self.node_info[n] - return info and info.get('isSwitch', False) - - def switches(self, sort=True): - '''Return switches. - sort: sort switches alphabetically - @return 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 - @return dpids list of dpids - ''' - return [n for n in self.nodes(sort) if not self.isSwitch(n)] - - def links(self, sort=True): - '''Return links. - sort: sort links alphabetically - @return links list of name pairs - ''' - if not sort: - return self.g.edges() - else: - links = [tuple(self.sorted(e)) for e in self.g.edges()] - return sorted( links, key=naturalSeq ) - - def port(self, src, dst): - '''Get port number. - - @param src source switch name - @param dst destination switch name - @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 ): - "Return link metadata" - src, dst = self.sorted([src, dst]) - return self.link_info[(src, dst)] - - def setlinkInfo( self, src, dst, info ): - "Set link metadata" - src, dst = self.sorted([src, dst]) - self.link_info[(src, dst)] = info + 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 sorted: + 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" - info = self.node_info[ name ] - return info if info is not None else {} + return self.g.node[ name ] def setNodeInfo( self, name, info ): "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 def sorted( items ): "Items sorted in natural (i.e. alphabetical) order" - return sorted(items, key=natural) + return sorted( items, key=natural ) class SingleSwitchTopo( Topo ): @@ -228,6 +309,7 @@ def build( self, k=2 ): self.addLink( host, switch, port1=0, port2=( k - h + 1 ) ) + class LinearTopo( Topo ): "Linear topology of k switches, with n hosts per switch." diff --git a/mininet/util.py b/mininet/util.py index bb1935f0ce2a438233555ad0ec996a405a6cebc8..0c76a9995d174ffe57ff0a30c59eb8e360c5cbff 100644 --- a/mininet/util.py +++ b/mininet/util.py @@ -448,7 +448,7 @@ def natural( text ): def num( s ): "Convert text segment to int if necessary" 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 ): "Natural sort key function for sequences"