[ONOS-7915] Run CHO test on Flex POD

Change-Id: Id8e59e044361a7781bed39f0878df97c2a67af8f
(cherry picked from commit c61aaa231a325558580fc55b774408a91a4dc0c1)
diff --git a/TestON/drivers/common/cli/emulator/mininetclidriver.py b/TestON/drivers/common/cli/emulator/mininetclidriver.py
index 5318ffb..661ada7 100644
--- a/TestON/drivers/common/cli/emulator/mininetclidriver.py
+++ b/TestON/drivers/common/cli/emulator/mininetclidriver.py
@@ -1935,7 +1935,7 @@
             main.log.exception( self.name + ": Uncaught exception!" )
             main.cleanAndExit()
 
-    def getSwitchRandom( self, timeout=60, nonCut=True, switchClasses=None, excludeNodes=[], excludeSwitches=[] ):
+    def getSwitchRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipSwitches=[] ):
         """
         Randomly get a switch from Mininet topology.
         If nonCut is True, it gets a list of non-cut switches (the deletion
@@ -1944,26 +1944,25 @@
         it just randomly returns one switch from all current switches in
         Mininet.
         excludeNodes will be pased to getGraphDict method
-        Switches specified in excludeSwitches will be excluded
+        Switches specified in skipSwitches will be excluded
         Returns the name of the chosen switch.
         """
         import random
         candidateSwitches = []
         try:
             if not nonCut:
-                switches = self.getSwitches( timeout=timeout, switchClasses=switchClasses )
+                switches = self.getSwitches( timeout=timeout, excludeNodes=excludeNodes )
                 assert len( switches ) != 0
                 for switchName in switches.keys():
                     candidateSwitches.append( switchName )
             else:
                 graphDict = self.getGraphDict( timeout=timeout, useId=False,
-                                               switchClasses=switchClasses,
                                                excludeNodes=excludeNodes )
                 if graphDict is None:
                     return None
                 self.graph.update( graphDict )
                 candidateSwitches = self.graph.getNonCutVertices()
-                candidateSwitches = [ switch for switch in candidateSwitches if switch not in excludeSwitches ]
+            candidateSwitches = [ switch for switch in candidateSwitches if switch not in skipSwitches ]
             if candidateSwitches is None:
                 return None
             elif len( candidateSwitches ) == 0:
@@ -2076,7 +2075,7 @@
             main.log.exception( self.name + ": Uncaught exception!" )
             main.cleanAndExit()
 
-    def getLinkRandom( self, timeout=60, nonCut=True, switchClasses=None, excludeNodes=[] ):
+    def getLinkRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipLinks=[] ):
         """
         Randomly get a link from Mininet topology.
         If nonCut is True, it gets a list of non-cut links (the deletion
@@ -2085,6 +2084,7 @@
         it just randomly returns one link from all current links in
         Mininet.
         excludeNodes will be passed to getLinks method to exclude unexpected links.
+        Any link that has either end included in skipLinks will be excluded
         Returns the link as a list, e.g. [ 's1', 's2' ]
         """
         import random
@@ -2100,12 +2100,13 @@
                     candidateLinks.append( [ link[ 'node1' ], link[ 'node2' ] ] )
             else:
                 graphDict = self.getGraphDict( timeout=timeout, useId=False,
-                                               switchClasses=switchClasses,
                                                excludeNodes=excludeNodes )
                 if graphDict is None:
                     return None
                 self.graph.update( graphDict )
                 candidateLinks = self.graph.getNonCutEdges()
+            candidateLinks = [ link for link in candidateLinks
+                               if link[0] not in skipLinks and link[1] not in skipLinks ]
             if candidateLinks is None:
                 return None
             elif len( candidateLinks ) == 0:
@@ -2821,12 +2822,14 @@
             main.log.exception( self.name + ": Uncaught exception!" )
             main.cleanAndExit()
 
-    def getSwitches( self, verbose=False, updateTimeout=1000, switchClasses=None ):
+    def getSwitches( self, verbose=False, updateTimeout=1000, excludeNodes=[] ):
         """
         Read switches from Mininet.
 
         Returns a dictionary whose keys are the switch names and the value is
         a dictionary containing information about the switch.
+        If excludeNodes is specified, switches with names included in excludeNodes
+        will be ingored.
         """
         # NOTE: To support new Mininet switch classes, just append the new
         # class to the switchClasses variable
@@ -2838,8 +2841,7 @@
         # <OVSSwitchNS s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None,s1-eth3:None pid=22550>
         # <OVSBridge s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=26830>
         # <UserSwitch s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=14737>
-        if not switchClasses:
-            switchClasses = r"(OVSSwitch)|(OVSBridge)|(OVSSwitchNS)|(IVSSwitch)|(LinuxBridge)|(UserSwitch)"
+        switchClasses = r"(OVSSwitch)|(OVSBridge)|(OVSSwitchNS)|(IVSSwitch)|(LinuxBridge)|(UserSwitch)"
         try:
             swRE = r"<(?P<class>" + switchClasses + r")" +\
                    r"(?P<options>\{.*\})?\s" +\
@@ -2854,6 +2856,8 @@
                 result = re.search( swRE, line, re.I )
                 if result:
                     name = result.group( 'name' )
+                    if name in excludeNodes:
+                        continue
                     dpid = str( self.getSwitchDPID( name ) ).zfill( 16 )
                     pid = result.group( 'pid' )
                     swClass = result.group( 'class' )
@@ -2967,8 +2971,8 @@
               'port1': str( port1 of_port )
               'port2': str( port2 of_port ) }
 
-        If either node1 or node2 name starts with any of the strings sepcified
-        in excludeNodes, the link will be excluded from the returned value
+        If either node1 or node2 name matches any of the names sepcified in
+        excludeNodes, the link will be excluded from the returned value
 
         Note: The port number returned is the eth#, not necessarily the of_port
               number. In Mininet, for OVS switch, these should be the same. For
@@ -2989,8 +2993,7 @@
                 if match:
                     node1 = match.group( 'node1' )
                     node2 = match.group( 'node2' )
-                    if any( node1.startswith( node ) for node in excludeNodes ) or \
-                       any( node2.startswith( node ) for node in excludeNodes ):
+                    if any( node1 == node or node2 == node for node in excludeNodes ):
                         continue
                     port1 = match.group( 'port1' )
                     port2 = match.group( 'port2' )
@@ -3461,7 +3464,7 @@
             main.cleanAndExit()
 
     def getGraphDict( self, timeout=60, useId=True, includeHost=False,
-                      switchClasses=None, excludeNodes=[] ):
+                      excludeNodes=[] ):
         """
         Return a dictionary which describes the latest Mininet topology data as a
         graph.
@@ -3480,9 +3483,8 @@
         topology.
         If includeHost == True, all hosts (and host-switch links) will be included
         in topology data.
-        if switchClasses == None, default switchClasses will be used when calling
-        getSwitches method.
-        excludeNodes will be passed to getLinks method to exclude unexpected links.
+        excludeNodes will be passed to getSwitches and getLinks methods to exclude
+        unexpected switches and links.
         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
@@ -3493,11 +3495,11 @@
         try:
             links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
             portDict = {}
-            switches = self.getSwitches( switchClasses=switchClasses )
+            switches = self.getSwitches( excludeNodes=excludeNodes )
             if includeHost:
                 hosts = self.getHosts()
             for link in links:
-                # FIXME: support 'includeHost' argument
+                # TODO: support 'includeHost' argument
                 if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
                     continue
                 nodeName1 = link[ 'node1' ]
@@ -3549,7 +3551,7 @@
                     for port in switches[ nodeName1 ][ 'ports' ]:
                         if port[ 'of_port' ] == str( portIndex ):
                             # Use -1 as index for disabled port
-                            if port[ 'enabled' ] == True:
+                            if port[ 'enabled' ]:
                                 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': portIndex }
                             else:
                                 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': -1 }
diff --git a/TestON/drivers/common/cli/hostdriver.py b/TestON/drivers/common/cli/hostdriver.py
index d8cc74d..7c7cd80 100644
--- a/TestON/drivers/common/cli/hostdriver.py
+++ b/TestON/drivers/common/cli/hostdriver.py
@@ -38,6 +38,7 @@
         self.handle = self
         self.name = None
         self.shortName = None
+        self.interfaces = []
         self.home = None
         self.inband = False
         self.prompt = "\$"
@@ -56,6 +57,10 @@
                 vars( self )[ key ] = connectargs[ key ]
             self.name = self.options[ 'name' ]
             self.shortName = self.options[ 'shortName' ]
+            self.interfaces.append( { 'ips': [ self.options[ 'ip' ] ],
+                                      'isUp': True,
+                                      'mac': self.options[ 'mac' ],
+                                      'name': None } )
 
             try:
                 if os.getenv( str( self.ip_address ) ) is not None:
@@ -109,13 +114,26 @@
         try:
             if self.handle:
                 # Disconnect from the host
-                self.handle.sendline( "" )
-                self.handle.expect( self.prompt )
-                self.handle.sendline( "exit" )
-                i = self.handle.expect( [ "closed", pexpect.TIMEOUT ], timeout=2 )
-                if i == 1:
-                    main.log.error( self.name + ": timeout when waiting for response" )
-                    main.log.error( "response: " + str( self.handle.before ) )
+                if not self.options[ 'inband' ] == 'True':
+                    self.handle.sendline( "" )
+                    self.handle.expect( self.prompt )
+                    self.handle.sendline( "exit" )
+                    i = self.handle.expect( [ "closed", pexpect.TIMEOUT ] )
+                    if i == 1:
+                        main.log.error( self.name + ": timeout when waiting for response" )
+                        main.log.error( "response: " + str( self.handle.before ) )
+                else:
+                    self.handle.sendline( "" )
+                    i = self.handle.expect( [ self.prompt, pexpect.TIMEOUT ], timeout=2 )
+                    if i == 1:
+                        main.log.warn( self.name + ": timeout when waiting for response" )
+                        main.log.warn( "response: " + str( self.handle.before ) )
+                    self.handle.sendline( "exit" )
+                    i = self.handle.expect( [ "closed", pexpect.TIMEOUT ], timeout=2 )
+                    if i == 1:
+                        main.log.warn( self.name + ": timeout when waiting for response" )
+                        main.log.warn( "response: " + str( self.handle.before ) )
+                return main.TRUE
         except TypeError:
             main.log.exception( self.name + ": Object not as expected" )
             response = main.FALSE
@@ -178,7 +196,7 @@
                 main.log.info( "Skip disconnecting the host via data plane" )
                 return main.TRUE
             self.handle.sendline( "" )
-            self.handle.expect( self.prompt )
+            self.handle.expect( self.prompt, timeout=2 )
             self.handle.sendline( "exit" )
             i = self.handle.expect( [ "closed", pexpect.TIMEOUT ], timeout=2 )
             if i == 1:
diff --git a/TestON/drivers/common/cli/networkdriver.py b/TestON/drivers/common/cli/networkdriver.py
index 828187c..1cd242c 100755
--- a/TestON/drivers/common/cli/networkdriver.py
+++ b/TestON/drivers/common/cli/networkdriver.py
@@ -32,6 +32,7 @@
 import re
 import types
 from drivers.common.clidriver import CLI
+from core.graph import Graph
 
 class NetworkDriver( CLI ):
 
@@ -45,7 +46,9 @@
         self.handle = None
         self.switches = {}
         self.hosts = {}
+        self.links = {}
         super( NetworkDriver, self ).__init__()
+        self.graph = Graph()
 
     def checkOptions( self, var, defaultVar ):
         if var is None or var == "":
@@ -261,17 +264,81 @@
             main.log.error( self.name + ": failed to disconnect inband hosts" )
             return main.FALSE
 
-    def getSwitches( self ):
+    def getSwitches( self, timeout=60, excludeNodes=[], includeStopped=False ):
         """
-        Return a dictionary which maps short names to switch component
+        Return a dictionary which maps short names to switch data
+        If includeStopped is True, stopped switches will also be included
         """
-        return self.switches
+        switches = {}
+        for switchName, switchComponent in self.switches.items():
+            if switchName in excludeNodes:
+                continue
+            if not includeStopped and not switchComponent.isup:
+                continue
+            dpid = switchComponent.dpid.replace( '0x', '' ).zfill( 16 )
+            ports = switchComponent.ports
+            swClass = 'Unknown'
+            pid = None
+            options = None
+            switches[ switchName ] = { "dpid": dpid,
+                                       "ports": ports,
+                                       "swClass": swClass,
+                                       "pid": pid,
+                                       "options": options }
+        return switches
 
-    def getHosts( self ):
+    def getHosts( self, hostClass=None ):
         """
-        Return a dictionary which maps short names to host component
+        Return a dictionary which maps short names to host data
         """
-        return self.hosts
+        hosts = {}
+        for hostName, hostComponent in self.hosts.items():
+            interfaces = hostComponent.interfaces
+            hosts[ hostName ] = { "interfaces": interfaces }
+        return hosts
+
+    def updateLinks( self, timeout=60, excludeNodes=[] ):
+        """
+        Update self.links by getting up-to-date port information from
+        switches
+        """
+        # TODO: also inlcude switch-to-host links
+        self.links = {}
+        for node1 in self.switches.keys():
+            if node1 in excludeNodes:
+                continue
+            self.links[ node1 ] = {}
+            self.switches[ node1 ].updatePorts()
+            for port in self.switches[ node1 ].ports:
+                if not port[ 'enabled' ]:
+                    continue
+                node2 = getattr( main, port[ 'node2' ] ).shortName
+                if node2 in excludeNodes:
+                    continue
+                port1 = port[ 'of_port' ]
+                port2 = port[ 'port2' ]
+                if not self.links[ node1 ].get( node2 ):
+                    self.links[ node1 ][ node2 ] = {}
+                # Check if this link already exists
+                if self.links.get( node2 ):
+                    if self.links[ node2 ].get( node1 ):
+                        if self.links[ node2 ].get( node1 ).get( port2 ):
+                            assert self.links[ node2 ][ node1 ][ port2 ] == port1
+                            continue
+                self.links[ node1 ][ node2 ][ port1 ] = port2
+
+    def getLinks( self, timeout=60, excludeNodes=[] ):
+        """
+        Return a list of links specify both node names and port numbers
+        """
+        self.updateLinks( timeout=timeout, excludeNodes=excludeNodes )
+        links = []
+        for node1, nodeLinks in self.links.items():
+            for node2, ports in nodeLinks.items():
+                for port1, port2 in ports.items():
+                    links.append( { 'node1': node1, 'node2': node2,
+                                    'port1': port1, 'port2': port2 } )
+        return links
 
     def getMacAddress( self, host ):
         """
@@ -279,8 +346,7 @@
         """
         import re
         try:
-            hosts = self.getHosts()
-            hostComponent = hosts[ host ]
+            hostComponent = self.hosts[ host ]
             response = hostComponent.ifconfig()
             pattern = r'HWaddr\s([0-9A-F]{2}[:-]){5}([0-9A-F]{2})'
             macAddressSearch = re.search( pattern, response, re.I )
@@ -297,8 +363,7 @@
             hostName: name of the host e.g. "h1"
             cmd: command to run on the host
         """
-        hosts = self.getHosts()
-        hostComponent = hosts[ hostName ]
+        hostComponent = self.hosts[ hostName ]
         if hostComponent:
             return hostComponent.command( cmd )
         return None
@@ -404,8 +469,7 @@
             returnValue = main.TRUE
             ipv6 = True if protocol == "IPv6" else False
             startTime = time.time()
-            hosts = self.getHosts()
-            hostPairs = itertools.permutations( list( hosts.values() ), 2 )
+            hostPairs = itertools.permutations( list( self.hosts.values() ), 2 )
             for hostPair in list( hostPairs ):
                 ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ]
                 pingResult = hostPair[ 0 ].ping( ipDst, ipv6=ipv6 )
@@ -446,9 +510,8 @@
         import time
         import itertools
         hostComponentList = []
-        hosts = self.getHosts()
         for hostName in hostList:
-            hostComponent = hosts[ hostName ]
+            hostComponent = self.hosts[ hostName ]
             if hostComponent:
                 hostComponentList.append( hostComponent )
         try:
@@ -504,10 +567,9 @@
             main.FALSE otherwise
         """
         try:
-            hosts = self.getHosts()
             if not hostList:
-                hostList = hosts.keys()
-            for hostName, hostComponent in hosts.items():
+                hostList = self.hosts.keys()
+            for hostName, hostComponent in self.hosts.items():
                 if hostName not in hostList:
                     continue
                 ipList = []
@@ -528,7 +590,7 @@
                         hostList.remove( hostName )
             return main.FALSE if hostList else main.TRUE
         except KeyError:
-            main.log.exception( self.name + ": host data not as expected: " + hosts )
+            main.log.exception( self.name + ": host data not as expected: " + self.hosts.keys() )
             return None
         except pexpect.EOF:
             main.log.error( self.name + ": EOF exception found" )
@@ -584,3 +646,272 @@
             " is " +
             ipAddressSearch.group( 1 ) )
         return ipAddressSearch.group( 1 )
+
+    def getLinkRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipLinks=[] ):
+        """
+        Randomly get a link from network 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.
+        excludeNodes will be passed to getLinks and getGraphDict method.
+        Any link that has either end included in skipLinks will be excluded.
+        Returns the link as a list, e.g. [ 's1', 's2' ].
+        """
+        import random
+        candidateLinks = []
+        try:
+            if not nonCut:
+                links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
+                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,
+                                               excludeNodes=excludeNodes )
+                if graphDict is None:
+                    return None
+                self.graph.update( graphDict )
+                candidateLinks = self.graph.getNonCutEdges()
+            candidateLinks = [ link for link in candidateLinks
+                               if link[0] not in skipLinks and link[1] not in skipLinks ]
+            if candidateLinks is 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 getSwitchRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipSwitches=[] ):
+        """
+        Randomly get a switch from network 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.
+        excludeNodes will be pased to getSwitches and getGraphDict method.
+        Switches specified in skipSwitches will be excluded.
+        Returns the name of the chosen switch.
+        """
+        import random
+        candidateSwitches = []
+        try:
+            if not nonCut:
+                switches = self.getSwitches( timeout=timeout, excludeNodes=excludeNodes )
+                assert len( switches ) != 0
+                for switchName in switches.keys():
+                    candidateSwitches.append( switchName )
+            else:
+                graphDict = self.getGraphDict( timeout=timeout, useId=False,
+                                               excludeNodes=excludeNodes )
+                if graphDict is None:
+                    return None
+                self.graph.update( graphDict )
+                candidateSwitches = self.graph.getNonCutVertices()
+            candidateSwitches = [ switch for switch in candidateSwitches if switch not in skipSwitches ]
+            if candidateSwitches is 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 getGraphDict( self, timeout=60, useId=True, includeHost=False,
+                      excludeNodes=[] ):
+        """
+        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 useId == True, dpid/mac will be used instead of names to identify
+        vertices, which is helpful when e.g. comparing network topology with ONOS
+        topology.
+        If includeHost == True, all hosts (and host-switch links) will be included
+        in topology data.
+        excludeNodes will be passed to getSwitches and getLinks methods to exclude
+        unexpected switches and links.
+        """
+        # TODO: support excludeNodes
+        graphDict = {}
+        try:
+            links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
+            portDict = {}
+            switches = self.getSwitches( excludeNodes=excludeNodes )
+            if includeHost:
+                hosts = self.getHosts()
+            for link in links:
+                # TODO: support 'includeHost' argument
+                if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
+                    continue
+                nodeName1 = link[ 'node1' ]
+                nodeName2 = link[ 'node2' ]
+                if not self.switches[ nodeName1 ].isup or not self.switches[ nodeName2 ].isup:
+                    continue
+                port1 = link[ 'port1' ]
+                port2 = link[ 'port2' ]
+                # Loop for two nodes
+                for i in range( 2 ):
+                    portIndex = port1
+                    if useId:
+                        node1 = 'of:' + str( switches[ nodeName1 ][ 'dpid' ] )
+                        node2 = 'of:' + str( switches[ nodeName2 ][ 'dpid' ] )
+                    else:
+                        node1 = nodeName1
+                        node2 = nodeName2
+                    if node1 not 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()
+                        pass
+                    for port in switches[ nodeName1 ][ 'ports' ]:
+                        if port[ 'of_port' ] == str( portIndex ):
+                            # Use -1 as index for disabled port
+                            if port[ 'enabled' ]:
+                                graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': portIndex }
+                            else:
+                                graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': -1 }
+                    # Swap two nodes/ports
+                    nodeName1, nodeName2 = nodeName2, nodeName1
+                    port1, port2 = port2, port1
+            # Remove links with disabled ports
+            linksToRemove = []
+            for node, edges in graphDict.items():
+                for neighbor, port in edges[ 'edges' ].items():
+                    if port[ 'port' ] == -1:
+                        linksToRemove.append( ( node, neighbor ) )
+            for node1, node2 in linksToRemove:
+                for i in range( 2 ):
+                    if graphDict.get( node1 )[ 'edges' ].get( node2 ):
+                        graphDict[ node1 ][ 'edges' ].pop( node2 )
+                    node1, node2 = node2, node1
+            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 pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception" )
+            return None
+
+    def switch( self, **switchargs ):
+        """
+        start/stop a switch
+        """
+        args = utilities.parse_args( [ "SW", "OPTION" ], **switchargs )
+        sw = args[ "SW" ] if args[ "SW" ] is not None else ""
+        option = args[ "OPTION" ] if args[ "OPTION" ] is not None else ""
+        try:
+            switchComponent = self.switches[ sw ]
+            if option == 'stop':
+                switchComponent.stopOfAgent()
+            elif option == 'start':
+                switchComponent.startOfAgent()
+            else:
+                main.log.warn( self.name + ": Unknown switch command" )
+                return main.FALSE
+            return main.TRUE
+        except KeyError:
+            main.log.error( self.name + ": Not able to find switch [}".format( sw ) )
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": TIMEOUT exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            return None
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception" )
+            main.cleanAndExit()
+
+    def discoverHosts( self, hostList=[], wait=1000, dstIp="6.6.6.6", dstIp6="1020::3fe" ):
+        '''
+        Hosts in hostList will do a single ARP/ND to a non-existent address for ONOS to
+        discover them. A host will use arping/ndisc6 to send ARP/ND depending on if it
+        has IPv4/IPv6 addresses configured.
+        Optional:
+            hostList: a list of names of the hosts that need to be discovered. If not
+                      specified mininet will send ping from all the hosts
+            wait: timeout for ARP/ND in milliseconds
+            dstIp: destination address used by IPv4 hosts
+            dstIp6: destination address used by IPv6 hosts
+        Returns:
+            main.TRUE if all packets were successfully sent. Otherwise main.FALSE
+        '''
+        try:
+            hosts = self.getHosts()
+            if not hostList:
+                hostList = hosts.keys()
+            discoveryResult = main.TRUE
+            for host in hostList:
+                flushCmd = ""
+                cmd = ""
+                if self.getIPAddress( host ):
+                    flushCmd = "sudo ip neigh flush all"
+                    cmd = "arping -c 1 -w {} {}".format( wait, dstIp )
+                    main.log.debug( "Sending IPv4 arping from host {}".format( host ) )
+                elif self.getIPAddress( host, proto='IPV6' ):
+                    flushCmd = "sudo ip -6 neigh flush all"
+                    intf = hosts[host]['interfaces'][0]['name']
+                    cmd = "ndisc6 -r 1 -w {} {} {}".format( wait, dstIp6, intf )
+                    main.log.debug( "Sending IPv6 ND from host {}".format( host ) )
+                else:
+                    main.log.warn( "No IP addresses configured on host {}, skipping discovery".format( host ) )
+                    discoveryResult = main.FALSE
+                if cmd:
+                    self.runCmdOnHost( host, flushCmd )
+                    self.runCmdOnHost( host, cmd )
+            return discoveryResult
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
diff --git a/TestON/drivers/common/cli/ofdpa/ofdpaswitchdriver.py b/TestON/drivers/common/cli/ofdpa/ofdpaswitchdriver.py
index ac909ee..08f691a 100644
--- a/TestON/drivers/common/cli/ofdpa/ofdpaswitchdriver.py
+++ b/TestON/drivers/common/cli/ofdpa/ofdpaswitchdriver.py
@@ -38,6 +38,7 @@
         """
         super( CLI, self ).__init__()
         self.name = None
+        self.shortName = None
         self.handle = None
         self.prompt = "~#"
         # Respect to bin folder
@@ -46,6 +47,8 @@
         self.tempDirectory = "/tmp/"
         self.conf = "ofagent.conf"
         self.switchDirectory = "/etc/ofagent/"
+        self.ports = []
+        self.isup = False
 
     def connect( self, **connectargs ):
         """
@@ -57,9 +60,19 @@
                 vars( self )[ key ] = connectargs[ key ]
             # Get the name
             self.name = self.options[ 'name' ]
+            self.shortName = self.options[ 'shortName' ]
             # Get the dpid
             self.dpid = self.options[ 'dpid' ]
             # Get ofagent patch
+            for key, value in self.options.items():
+                if re.match( 'link[\d]+', key ):
+                    self.ports.append( { 'enabled': True,
+                                         'ips': [ None ],
+                                         'mac': None,
+                                         'name': None,
+                                         'node2': value[ 'node2' ],
+                                         'port2': value[ 'port2' ],
+                                         'of_port': value[ 'port1' ] } )
             if 'confDir' in self.options:
                 self.switchDirectory = self.options[ 'confDir' ]
             # Parse the IP address
@@ -165,7 +178,7 @@
                 onosIp = "-t " + str( ip )
             elif isinstance( ip, types.ListType ):
                 for ipAddress in ip:
-                        onosIp += "-t " + str( ipAddress ) + " "
+                    onosIp += "-t " + str( ipAddress ) + " "
             else:
                 main.log.error( self.name + ": Invalid ip address" )
                 return main.FALSE
@@ -189,6 +202,10 @@
                 kwargs={},
                 attempts=10,
                 sleep=10)
+            if not assignResult:
+                self.isup = False
+            else:
+                self.isup = True
             # Done return true
             return assignResult
         # Errors handling
@@ -221,10 +238,21 @@
         """
         Create a backup file of the old configuration on the switch
         """
-        self.handle.sendline( "" )
-        self.handle.expect( self.prompt )
-        self.handle.sendline( "cp %s%s %s%s.backup" % (self.switchDirectory, self.conf, self.switchDirectory, self.conf) )
-        self.handle.expect( self.prompt )
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "cp %s%s %s%s.backup" % (self.switchDirectory, self.conf, self.switchDirectory, self.conf) )
+            self.handle.expect( self.prompt )
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
+            return main.FALSE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
 
     def pushCfg( self ):
         """
@@ -236,23 +264,75 @@
         os.system( "scp " + self.tempDirectory + self.conf + " " +
                    self.user_name + "@" + self.ip_address + ":" + self.switchDirectory)
 
+    def ofagentIsRunning( self ):
+        """
+        Return main.TRUE if service ofagentd is running on the
+        switch; otherwise main.FALSE
+        """
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "service ofagentd status" )
+            self.handle.expect( self.prompt )
+            response = self.handle.before
+            if "ofagentd is running" in response:
+                return main.TRUE
+            else:
+                return main.FALSE
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
+            return main.FALSE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
+
     def startOfAgent( self ):
         """
         Start the ofagent on the device
         """
-        self.handle.sendline( "" )
-        self.handle.expect( self.prompt )
-        self.handle.sendline( "service ofagentd start" )
-        self.handle.expect( self.prompt )
+        try:
+            if self.ofagentIsRunning():
+                main.log.warn( self.name + ": ofagentd is already running" )
+                return main.TRUE
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "service ofagentd start" )
+            self.handle.expect( self.prompt )
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
+            return main.FALSE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
 
     def stopOfAgent( self ):
         """
         Stop the ofagent on the device
         """
-        self.handle.sendline( "" )
-        self.handle.expect( self.prompt )
-        self.handle.sendline( "service ofagentd stop" )
-        self.handle.expect( self.prompt )
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "service ofagentd stop" )
+            self.handle.expect( self.prompt )
+            self.isup = False
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
+            return main.FALSE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
 
     def dumpFlows( self ):
         """
@@ -318,19 +398,89 @@
         Enable all the ports on the devices
         It needs to wait for the boot
         """
-        self.handle.sendline( "" )
-        self.handle.expect( self.prompt )
-        self.handle.sendline( "client_port_table_dump" )
-        self.handle.expect( self.prompt )
-        response = self.handle.before
-        if "Error from ofdpaClientInitialize()" in response:
-            main.log.warn( self.name + ": Not yet started" )
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "client_port_table_dump" )
+            self.handle.expect( self.prompt )
+            response = self.handle.before
+            if "Error from ofdpaClientInitialize()" in response:
+                main.log.warn( self.name + ": Not yet started" )
+                return main.FALSE
+            # Change port speed
+            self.handle.sendline( "sh portspeed" )
+            self.handle.expect( self.prompt )
+            response = self.handle.before
+            if "Failure calling" in response:
+                main.log.warn( self.name + ": failed to change port speed" )
+                return main.FALSE
+            return main.TRUE
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
             return main.FALSE
-        # Change port speed
-        self.handle.sendline( "sh portspeed" )
-        self.handle.expect( self.prompt )
-        response = self.handle.before
-        if "Failure calling" in response:
-            main.log.warn( self.name + ": failed to change port speed" )
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
+
+    def setPortSpeed( self, index, speed=40000 ):
+        """
+        Run client_drivshell on the switch to set speed for a
+        specific port
+        index: port index, e.g. 1
+        speed: port speed, e.g. 40000
+        """
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            cmd = "client_drivshell port {} sp={}".format( index, speed )
+            self.handle.sendline( cmd )
+            self.handle.expect( self.prompt )
+            response = self.handle.before
+            return main.TRUE
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
             return main.FALSE
-        return main.TRUE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()
+
+    def updatePorts( self ):
+        """
+        Get latest port status on the switch by running
+        client_port_table_dump commmand and parsing the output
+        """
+        try:
+            self.handle.sendline( "" )
+            self.handle.expect( self.prompt )
+            self.handle.sendline( "client_port_table_dump" )
+            self.handle.expect( self.prompt )
+            ports = self.handle.before
+            if "Error from ofdpaClientInitialize()" in ports:
+                main.log.warn( self.name + ": Not yet started" )
+                return main.FALSE
+            ports = re.findall( r"0x[\d]+.*port[\d]+:\r\r\n.*\r\r\n.*PeerFeature:.*\r\r\n", ports )
+            for port in ports:
+                m = re.match( r".*port([\d]+):\r\r\n.*state = (.*), mac", port )
+                index = m.group( 1 )
+                enabled = True if m.group( 2 ) == '0x00000000' else False
+                for p in self.ports:
+                    if p[ 'of_port' ] == index:
+                        p[ 'enabled' ] = enabled
+        except pexpect.TIMEOUT:
+            main.log.error( self.name + ": pexpect.TIMEOUT found" )
+            return main.FALSE
+        except pexpect.EOF:
+            main.log.error( self.name + ": EOF exception found" )
+            main.log.error( self.name + ":     " + self.handle.before )
+            main.cleanAndExit()
+        except Exception:
+            main.log.exception( self.name + ": Uncaught exception!" )
+            main.cleanAndExit()