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