Implement interfaces between graph utility and two drivers
- Add getOVSPort function to MininetCliDriver
- Add getGraphDict function to OnosCliDriver and MininetCliDriver
- Add delLinkRandom and delSwitchRandom functions to MininetCliDriver
Change-Id: I85d23aeb241ba004cfdde2b2501775f77863bf4c
diff --git a/TestON/drivers/common/cli/emulator/mininetclidriver.py b/TestON/drivers/common/cli/emulator/mininetclidriver.py
index 1209571..1632fee 100644
--- a/TestON/drivers/common/cli/emulator/mininetclidriver.py
+++ b/TestON/drivers/common/cli/emulator/mininetclidriver.py
@@ -38,6 +38,7 @@
import os
from math import pow
from drivers.common.cli.emulatordriver import Emulator
+from core.graph import Graph
class MininetCliDriver( Emulator ):
@@ -57,6 +58,7 @@
self.hostPrompt = "~#"
self.bashPrompt = "\$"
self.scapyPrompt = ">>>"
+ self.graph = Graph()
def connect( self, **connectargs ):
"""
@@ -1827,6 +1829,72 @@
main.cleanup()
main.exit()
+ def getSwitchRandom( self, timeout=60, nonCut=True ):
+ """
+ Randomly get a switch from Mininet topology.
+ If nonCut is True, it gets a list of non-cut switches (the deletion
+ of a non-cut switch will not increase the number of connected
+ components of a graph) and randomly returns one of them, otherwise
+ it just randomly returns one switch from all current switches in
+ Mininet.
+ Returns the name of the chosen switch.
+ """
+ import random
+ candidateSwitches = []
+ try:
+ if not nonCut:
+ switches = self.getSwitches( timeout=timeout )
+ assert len( switches ) != 0
+ for switchName in switches.keys():
+ candidateSwitches.append( switchName )
+ else:
+ graphDict = self.getGraphDict( timeout=timeout, useId=False )
+ if graphDict == None:
+ return None
+ self.graph.update( graphDict )
+ candidateSwitches = self.graph.getNonCutVertices()
+ if candidateSwitches == None:
+ return None
+ elif len( candidateSwitches ) == 0:
+ main.log.info( self.name + ": No candidate switch for deletion" )
+ return None
+ else:
+ switch = random.sample( candidateSwitches, 1 )
+ return switch[ 0 ]
+ except KeyError:
+ main.log.exception( self.name + ": KeyError exception found" )
+ return None
+ except AssertionError:
+ main.log.exception( self.name + ": AssertionError exception found" )
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception" )
+ return None
+
+ def delSwitchRandom( self, timeout=60, nonCut=True ):
+ """
+ Randomly delete a switch from Mininet topology.
+ If nonCut is True, it gets a list of non-cut switches (the deletion
+ of a non-cut switch will not increase the number of connected
+ components of a graph) and randomly chooses one for deletion,
+ otherwise it just randomly delete one switch from all current
+ switches in Mininet.
+ Returns the name of the deleted switch
+ """
+ try:
+ switch = self.getSwitchRandom( timeout, nonCut )
+ if switch == None:
+ return None
+ else:
+ deletionResult = self.delSwitch( switch )
+ if deletionResult:
+ return switch
+ else:
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception" )
+ return None
+
def addLink( self, node1, node2 ):
"""
add a link to the mininet topology
@@ -1892,6 +1960,75 @@
main.cleanup()
main.exit()
+ def getLinkRandom( self, timeout=60, nonCut=True ):
+ """
+ Randomly get a link from Mininet topology.
+ If nonCut is True, it gets a list of non-cut links (the deletion
+ of a non-cut link will not increase the number of connected
+ component of a graph) and randomly returns one of them, otherwise
+ it just randomly returns one link from all current links in
+ Mininet.
+ Returns the link as a list, e.g. [ 's1', 's2' ]
+ """
+ import random
+ candidateLinks = []
+ try:
+ if not nonCut:
+ links = self.getLinks( timeout=timeout )
+ assert len( links ) != 0
+ for link in links:
+ # Exclude host-switch link
+ if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
+ continue
+ candidateLinks.append( [ link[ 'node1' ], link[ 'node2' ] ] )
+ else:
+ graphDict = self.getGraphDict( timeout=timeout, useId=False )
+ if graphDict == None:
+ return None
+ self.graph.update( graphDict )
+ candidateLinks = self.graph.getNonCutEdges()
+ if candidateLinks == None:
+ return None
+ elif len( candidateLinks ) == 0:
+ main.log.info( self.name + ": No candidate link for deletion" )
+ return None
+ else:
+ link = random.sample( candidateLinks, 1 )
+ return link[ 0 ]
+ except KeyError:
+ main.log.exception( self.name + ": KeyError exception found" )
+ return None
+ except AssertionError:
+ main.log.exception( self.name + ": AssertionError exception found" )
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception" )
+ return None
+
+ def delLinkRandom( self, timeout=60, nonCut=True ):
+ """
+ Randomly delete a link from Mininet topology.
+ If nonCut is True, it gets a list of non-cut links (the deletion
+ of a non-cut link will not increase the number of connected
+ component of a graph) and randomly chooses one for deletion,
+ otherwise it just randomly delete one link from all current links
+ in Mininet.
+ Returns the deleted link as a list, e.g. [ 's1', 's2' ]
+ """
+ try:
+ link = self.getLinkRandom( timeout, nonCut )
+ if link == None:
+ return None
+ else:
+ deletionResult = self.delLink( link[ 0 ], link[ 1 ] )
+ if deletionResult:
+ return link
+ else:
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception" )
+ return None
+
def addHost( self, hostname, **kwargs ):
"""
Add a host to the mininet topology
@@ -2476,6 +2613,48 @@
'enabled': isUp } )
return ports
+ def getOVSPorts( self, nodeName ):
+ """
+ Read ports from OVS by executing 'ovs-ofctl dump-ports-desc' command.
+
+ Returns a list of dictionaries containing information about each
+ port of the given switch.
+ """
+ command = "sh ovs-ofctl dump-ports-desc " + str( nodeName )
+ try:
+ response = self.execute(
+ cmd=command,
+ prompt="mininet>",
+ timeout=10 )
+ ports = []
+ if response:
+ for line in response.split( "\n" ):
+ # Regex patterns to parse 'ovs-ofctl dump-ports-desc' output
+ # Example port:
+ # 1(s1-eth1): addr:ae:60:72:77:55:51
+ pattern = "(?P<index>\d+)\((?P<name>[^-]+-eth(?P<port>\d+))\):\saddr:(?P<mac>([a-f0-9]{2}:){5}[a-f0-9]{2})"
+ result = re.search( pattern, line )
+ if result:
+ index = result.group( 'index' )
+ name = result.group( 'name' )
+ # This port number is extracted from port name
+ port = result.group( 'port' )
+ mac = result.group( 'mac' )
+ ports.append( { 'index': index,
+ 'name': name,
+ 'port': port,
+ 'mac': mac } )
+ return ports
+ except pexpect.EOF:
+ main.log.error( self.name + ": EOF exception found" )
+ main.log.error( self.name + ": " + self.handle.before )
+ main.cleanup()
+ main.exit()
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception!" )
+ main.cleanup()
+ main.exit()
+
def getSwitches( self, verbose=False ):
"""
Read switches from Mininet.
@@ -2963,6 +3142,100 @@
return switchList
+ def getGraphDict( self, timeout=60, useId=True, includeHost=False ):
+ """
+ Return a dictionary which describes the latest Mininet topology data as a
+ graph.
+ An example of the dictionary:
+ { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
+ vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
+ Each vertex should at least have an 'edges' attribute which describes the
+ adjacency information. The value of 'edges' attribute is also represented by
+ a dictionary, which maps each edge (identified by the neighbor vertex) to a
+ list of attributes.
+ An example of the edges dictionary:
+ 'edges': { vertex2: { 'port': ..., 'weight': ... },
+ vertex3: { 'port': ..., 'weight': ... } }
+ If useId == True, dpid/mac will be used instead of names to identify
+ vertices, which is helpful when e.g. comparing Mininet topology with ONOS
+ topology.
+ If includeHost == True, all hosts (and host-switch links) will be included
+ in topology data.
+ Note that link or switch that are brought down by 'link x x down' or 'switch
+ x down' commands still show in the output of Mininet CLI commands such as
+ 'links', 'dump', etc. Thus, to ensure the correctness of this function, it is
+ recommended to use delLink() or delSwitch functions to simulate link/switch
+ down, and addLink() or addSwitch to add them back.
+ """
+ graphDict = {}
+ try:
+ links = self.getLinks( timeout=timeout )
+ portDict = {}
+ if useId:
+ switches = self.getSwitches()
+ if includeHost:
+ hosts = self.getHosts()
+ for link in links:
+ # FIXME: support 'includeHost' argument
+ if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
+ continue
+ nodeName1 = link[ 'node1' ]
+ nodeName2 = link[ 'node2' ]
+ port1 = link[ 'port1' ]
+ port2 = link[ 'port2' ]
+ # Loop for two nodes
+ for i in range( 2 ):
+ # Get port index from OVS
+ # The index extracted from port name may be inconsistent with ONOS
+ portIndex = -1
+ if not nodeName1 in portDict.keys():
+ portList = self.getOVSPorts( nodeName1 )
+ if len( portList ) == 0:
+ main.log.warn( self.name + ": No port found on switch " + nodeName1 )
+ return None
+ portDict[ nodeName1 ] = portList
+ for port in portDict[ nodeName1 ]:
+ if port[ 'port' ] == port1:
+ portIndex = port[ 'index' ]
+ break
+ if portIndex == -1:
+ main.log.warn( self.name + ": Cannot find port index for interface {}-eth{}".format( nodeName1, port1 ) )
+ return None
+ if useId:
+ node1 = 'of:' + str( switches[ nodeName1 ][ 'dpid' ] )
+ node2 = 'of:' + str( switches[ nodeName2 ][ 'dpid' ] )
+ else:
+ node1 = nodeName1
+ node2 = nodeName2
+ if not node1 in graphDict.keys():
+ if useId:
+ graphDict[ node1 ] = { 'edges':{},
+ 'dpid':switches[ nodeName1 ][ 'dpid' ],
+ 'name':nodeName1,
+ 'ports':switches[ nodeName1 ][ 'ports' ],
+ 'swClass':switches[ nodeName1 ][ 'swClass' ],
+ 'pid':switches[ nodeName1 ][ 'pid' ],
+ 'options':switches[ nodeName1 ][ 'options' ] }
+ else:
+ graphDict[ node1 ] = { 'edges':{} }
+ else:
+ # Assert node2 is not connected to any current links of node1
+ assert node2 not in graphDict[ node1 ][ 'edges' ].keys()
+ graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port':portIndex }
+ # Swap two nodes/ports
+ nodeName1, nodeName2 = nodeName2, nodeName1
+ port1, port2 = port2, port1
+ return graphDict
+ except KeyError:
+ main.log.exception( self.name + ": KeyError exception found" )
+ return None
+ except AssertionError:
+ main.log.exception( self.name + ": AssertionError exception found" )
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception" )
+ return None
+
def update( self ):
"""
updates the port address and status information for
diff --git a/TestON/drivers/common/cli/onosclidriver.py b/TestON/drivers/common/cli/onosclidriver.py
index 1262b09..59e61ca 100755
--- a/TestON/drivers/common/cli/onosclidriver.py
+++ b/TestON/drivers/common/cli/onosclidriver.py
@@ -23,6 +23,7 @@
import time
import os
from drivers.common.clidriver import CLI
+from core.graph import Graph
class OnosCliDriver( CLI ):
@@ -34,6 +35,7 @@
self.name = None
self.home = None
self.handle = None
+ self.graph = Graph()
super( CLI, self ).__init__()
def connect( self, **connectargs ):
@@ -4855,3 +4857,72 @@
main.log.exception( self.name + ": Uncaught exception!" )
main.cleanup()
main.exit()
+
+ def getGraphDict( self, timeout=60, includeHost=False ):
+ """
+ Return a dictionary which describes the latest network topology data as a
+ graph.
+ An example of the dictionary:
+ { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
+ vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
+ Each vertex should at least have an 'edges' attribute which describes the
+ adjacency information. The value of 'edges' attribute is also represented by
+ a dictionary, which maps each edge (identified by the neighbor vertex) to a
+ list of attributes.
+ An example of the edges dictionary:
+ 'edges': { vertex2: { 'port': ..., 'weight': ... },
+ vertex3: { 'port': ..., 'weight': ... } }
+ If includeHost == True, all hosts (and host-switch links) will be included
+ in topology data.
+ """
+ graphDict = {}
+ try:
+ links = self.links()
+ links = json.loads( links )
+ devices = self.devices()
+ devices = json.loads( devices )
+ idToDevice = {}
+ for device in devices:
+ idToDevice[ device[ 'id' ] ] = device
+ if includeHost:
+ hosts = self.hosts()
+ # FIXME: support 'includeHost' argument
+ for link in links:
+ nodeA = link[ 'src' ][ 'device' ]
+ nodeB = link[ 'dst' ][ 'device' ]
+ assert idToDevice[ nodeA ][ 'available' ] and idToDevice[ nodeB ][ 'available' ]
+ if not nodeA in graphDict.keys():
+ graphDict[ nodeA ] = { 'edges':{},
+ 'dpid':idToDevice[ nodeA ][ 'id' ][3:],
+ 'type':idToDevice[ nodeA ][ 'type' ],
+ 'available':idToDevice[ nodeA ][ 'available' ],
+ 'role':idToDevice[ nodeA ][ 'role' ],
+ 'mfr':idToDevice[ nodeA ][ 'mfr' ],
+ 'hw':idToDevice[ nodeA ][ 'hw' ],
+ 'sw':idToDevice[ nodeA ][ 'sw' ],
+ 'serial':idToDevice[ nodeA ][ 'serial' ],
+ 'chassisId':idToDevice[ nodeA ][ 'chassisId' ],
+ 'annotations':idToDevice[ nodeA ][ 'annotations' ]}
+ else:
+ # Assert nodeB is not connected to any current links of nodeA
+ assert nodeB not in graphDict[ nodeA ][ 'edges' ].keys()
+ graphDict[ nodeA ][ 'edges' ][ nodeB ] = { 'port':link[ 'src' ][ 'port' ],
+ 'type':link[ 'type' ],
+ 'state':link[ 'state' ] }
+ return graphDict
+ except ( TypeError, ValueError ):
+ main.log.exception( self.name + ": Object not as expected" )
+ return None
+ except KeyError:
+ main.log.exception( self.name + ": KeyError exception found" )
+ return None
+ except AssertionError:
+ main.log.exception( self.name + ": AssertionError exception found" )
+ return None
+ except pexpect.EOF:
+ main.log.error( self.name + ": EOF exception found" )
+ main.log.error( self.name + ": " + self.handle.before )
+ return None
+ except Exception:
+ main.log.exception( self.name + ": Uncaught exception!" )
+ return None