Add showOvsdbMdsal.py tool 83/23983/3
authorFlavio Fernandes <ffernand@redhat.com>
Fri, 10 Jul 2015 13:48:35 +0000 (09:48 -0400)
committerFlavio Fernandes <ffernand@redhat.com>
Mon, 13 Jul 2015 19:24:21 +0000 (15:24 -0400)
This tool can provide useful info in regards to ovsdb's mdsal structures.
It looks at the operational or config trees in mdsal, depending on the
parameter '--config':

Patch set 2: code review

$ ./showOvsdbMdsal.py -h
Usage: showOvsdbMdsal.py [options]

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -d, --debug           Verbosity. Can be provided multiple times for more
                        debug.
  -n, --noalias         Do not map nodeId of bridges to an alias
  -i ODLIP, --ip=ODLIP  opendaylights ip address
  -t ODLPORT, --port=ODLPORT
                        opendaylights listening tcp port on restconf
                        northbound
  -u ODLUSERNAME, --user=ODLUSERNAME
                        opendaylight restconf username
  -p ODLPASSWORD, --password=ODLPASSWORD
                        opendaylight restconf password
  -c, --config          parse mdsal restconf config tree instead of
                        operational tree
  -f, --hide-flows      hide flows

Examples of what to expect:

  https://gist.githubusercontent.com/anonymous/3a952cec49ef92461752/raw/7abfedcd7790acc5b5cef07a2c08da0405f303c6/gistify465337.txt

Change-Id: Ica94e00a14d17682acdedad3b495833de330afc4
Signed-off-by: Flavio Fernandes <ffernand@redhat.com>
resources/commons/README
resources/commons/showOvsdbMdsal.py [new file with mode: 0755]

index d557ff5180bb38c9727b2e6475132353ea293de3..0e34991087dd6a6e1b9aa644f1b300a78f2f609d 100644 (file)
@@ -14,3 +14,5 @@ Contents
 4. OVSDB_Southbound.postman_collection : Collection of RESTCONF APIs for using the OVSDB MD-SAL Southbound
 
 5. Neutron-v2.0-LBaaS-API-Examples_July15.json.postman_collection.txt : Collection of REST-APIs to interact with LBaas pool/pool member/loadbalancer.
+
+6. showOvsdbMdsal.py : Dumps mdsal related info from running ODL that is related to ovsdb and netvirt. Use 'showOvsdbMdsal.py -h' for usage
diff --git a/resources/commons/showOvsdbMdsal.py b/resources/commons/showOvsdbMdsal.py
new file mode 100755 (executable)
index 0000000..05102cb
--- /dev/null
@@ -0,0 +1,718 @@
+#!/usr/bin/env python
+
+import urllib2, base64, json, sys, optparse
+
+# globals
+CONST_DEFAULT_DEBUG=0
+options = None
+state = None
+jsonTopologyNodes = []
+jsonInventoryNodes = []
+flowInfoNodes = {}
+nodeIdToDpidCache = {}
+
+CONST_OPERATIONAL = 'operational'
+CONST_CONFIG = 'config'
+CONST_NET_TOPOLOGY = 'network-topology'
+CONST_TOPOLOGY = 'topology'
+CONST_TP_OF_INTERNAL = 65534
+CONST_ALIASES = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet',
+                 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango',
+                 'uniform', 'victor', 'whiskey', 'xray', 'yankee', 'zulu']
+
+
+class State:
+    def __init__(self):
+        self.nextAliasIndex = 0
+        self.nextAliasWrap = 0
+        self.nodeIdToAlias = {}
+
+        self.bridgeNodes = {}
+        self.ovsdbNodes = {}
+        self.ofLinks = {}
+
+    def __repr__(self):
+        return 'State {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
+            'nextAliasIndex', self.nextAliasIndex,
+            'nextAliasWrap', self.nextAliasWrap,
+            'bridgeNodes_ids', self.bridgeNodes.keys(),
+            'ovsdbNodes_ids', self.ovsdbNodes.keys(),
+            'nodeIdToAlias', self.nodeIdToAlias)
+
+    def registerBridgeNode(self, bridgeNode):
+        self.bridgeNodes[bridgeNode.nodeId] = bridgeNode
+
+    def registerOvsdbNode(self, ovsdbNode):
+        self.ovsdbNodes[ovsdbNode.nodeId] = ovsdbNode
+
+    def getNextAlias(self, nodeId):
+        result = CONST_ALIASES[ self.nextAliasIndex ]
+        if self.nextAliasWrap > 0:
+            result += '_' + str(self.nextAliasWrap)
+
+        if CONST_ALIASES[ self.nextAliasIndex ] == CONST_ALIASES[-1]:
+            self.nextAliasIndex = 0
+            self.nextAliasWrap += 1
+        else:
+            self.nextAliasIndex += 1
+
+        self.nodeIdToAlias[ nodeId ] = result
+        return result
+
+# --
+
+class TerminationPoint:
+    def __init__(self, name, ofPort, tpType, mac='', ifaceId=''):
+        self.name = name
+        self.ofPort = ofPort
+        self.tpType = tpType
+        self.mac = mac
+        self.ifaceId = ifaceId
+
+    def __repr__(self):
+        result = '{} {}:{}'.format(self.name, 'of', self.ofPort)
+
+        if self.tpType != '':
+            result += ' {}:{}'.format('type', self.tpType)
+        if self.mac != '':
+            result += ' {}:{}'.format('mac', self.mac)
+        if self.ifaceId != '':
+            result += ' {}:{}'.format('ifaceId', self.ifaceId)
+
+        return '{' + result + '}'
+
+# --
+
+class BridgeNode:
+    def __init__(self, nodeId, dpId, name, controllerTarget, controllerConnected):
+        global state
+        self.alias = state.getNextAlias(nodeId)
+        self.nodeId = nodeId
+        self.dpId = dpId
+        self.name = name
+        self.controllerTarget = controllerTarget
+        self.controllerConnected = controllerConnected
+        self.tps = []
+
+    def getOpenflowName(self):
+        if self.dpId is None:
+            return self.nodeId
+        return dataPathIdToOfFormat(self.dpId)
+
+    def addTerminationPoint(self, terminationPoint):
+        self.tps.append(terminationPoint)
+
+    def __repr__(self):
+        return 'BridgeNode {}:{} {}:{} {}:{} {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
+            'alias', self.alias,
+            'nodeId', self.nodeId,
+            'dpId', self.dpId,
+            'openflowName', self.getOpenflowName(),
+            'name', self.name,
+            'controllerTarget', self.controllerTarget,
+            'controllerConnected', self.controllerConnected,
+            'tps', self.tps)
+
+# --
+
+class OvsdbNode:
+    def __init__(self, nodeId, inetMgr, inetNode, otherLocalIp, ovsVersion):
+        global state
+        if inetNode != '':
+            self.alias = inetNode
+        else:
+            self.alias = nodeId
+        self.nodeId = nodeId
+        self.inetMgr = inetMgr
+        self.inetNode = inetNode
+        self.otherLocalIp = otherLocalIp
+        self.ovsVersion = ovsVersion
+
+    def __repr__(self):
+        return 'OvsdbNode {}:{} {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
+            'alias', self.alias,
+            'nodeId', self.nodeId,
+            'inetMgr', self.inetMgr,
+            'inetNode', self.inetNode,
+            'otherLocalIp', self.otherLocalIp,
+            'ovsVersion', self.ovsVersion)
+
+# ======================================================================
+
+def printError(msg):
+    sys.stderr.write(msg)
+
+# ======================================================================
+
+def prt(msg, logLevel=0):
+    prtCommon(msg, logLevel)
+def prtLn(msg, logLevel=0):
+    prtCommon('{}\n'.format(msg), logLevel)
+def prtCommon(msg, logLevel):
+    if options.debug >= logLevel:
+        sys.stdout.write(msg)
+
+# ======================================================================
+
+def getMdsalTreeType():
+    if options.useConfigTree:
+        return CONST_CONFIG
+    return CONST_OPERATIONAL
+
+# --
+
+def grabJson(url):
+
+    try:
+        request = urllib2.Request(url)
+        # You need the replace to handle encodestring adding a trailing newline
+        # (https://docs.python.org/2/library/base64.html#base64.encodestring)
+        base64string = base64.encodestring('{}:{}'.format(options.odlUsername, options.odlPassword)).replace('\n', '')
+        request.add_header('Authorization', 'Basic {}'.format(base64string))
+        result = urllib2.urlopen(request)
+    except urllib2.URLError, e:
+        printError('Unable to send request: {}\n'.format(e))
+        sys.exit(1)
+
+    if (result.code != 200):
+        printError( '{}\n{}\n\nError: unexpected code: {}\n'.format(result.info(), result.read(), result.code) )
+        sys.exit(1)
+
+    data = json.load(result)
+    prtLn(data, 4)
+    return data
+
+# --
+
+def grabInventoryJson(mdsalTreeType):
+    global jsonInventoryNodes
+
+    url = 'http://{}:{}/restconf/{}/opendaylight-inventory:nodes/'.format(options.odlIp, options.odlPort, mdsalTreeType)
+    data = grabJson(url)
+
+    if not 'nodes' in data:
+        printError( '{}\n\nError: did not find nodes in {}'.format(data, url) )
+        sys.exit(1)
+
+    data2 = data['nodes']
+    if not 'node' in data2:
+        printError( '{}\n\nError: did not find node in {}'.format(data2, url) )
+        sys.exit(1)
+
+    jsonInventoryNodes = data2['node']
+
+# --
+
+def parseInventoryJson(mdsalTreeType):
+    global jsonInventoryNodes
+    global flowInfoNodes
+
+    for nodeDict in jsonInventoryNodes:
+        if not 'id' in nodeDict:
+            continue
+
+        bridgeOfId = nodeDict.get('id')
+        prtLn('inventory node {} has keys {}'.format(bridgeOfId, nodeDict.keys()), 3)
+
+        # locate bridge Node
+        bridgeNodeId = None
+        bridgeNode = None
+        for currNodeId in state.bridgeNodes.keys():
+            if state.bridgeNodes[currNodeId].getOpenflowName() == bridgeOfId:
+                bridgeNodeId = currNodeId
+                bridgeNode = state.bridgeNodes[currNodeId]
+                break
+
+        if bridgeNodeId is None:
+            prtLn('inventory node {}'.format(bridgeOfId), 1)
+        else:
+            prtLn('inventory node {}, aka {}, aka {}'.format(bridgeOfId, bridgeNodeId, showPrettyName(bridgeNodeId)), 1)
+
+        flowInfoNode = {}
+
+        indent = ' ' * 2
+        prtLn('{}features: {}'.format(indent, nodeDict.get('flow-node-inventory:switch-features', {})), 2)
+        prt('{}sw: {}'.format(indent, nodeDict.get('flow-node-inventory:software')), 2)
+        prt('{}hw: {}'.format(indent, nodeDict.get('flow-node-inventory:hardware')), 2)
+        prt('{}manuf: {}'.format(indent, nodeDict.get('flow-node-inventory:manufacturer')), 2)
+        prtLn('{}ip: {}'.format(indent, nodeDict.get('flow-node-inventory:ip-address')), 2)
+
+
+        for inventoryEntry in nodeDict.get('flow-node-inventory:table', []):
+            if 'id' in inventoryEntry:
+                currTableId = inventoryEntry.get('id')
+                for currFlow in inventoryEntry.get('flow', []):
+                    prtLn('{}table {}: {}'.format(indent, currTableId, currFlow.get('id')), 1)
+                    prtLn('{}{}'.format(indent * 2, currFlow), 2)
+
+                    if currTableId in flowInfoNode:
+                        flowInfoNode[ currTableId ].append( currFlow.get('id') )
+                    else:
+                        flowInfoNode[ currTableId ] = [ currFlow.get('id') ]
+
+        prtLn('', 1)
+
+        for currTableId in flowInfoNode.keys():
+            flowInfoNode[currTableId].sort()
+
+        # store info collected in flowInfoNodes
+        flowInfoNodes[bridgeOfId] = flowInfoNode
+
+# --
+
+def grabTopologyJson(mdsalTreeType):
+    global jsonTopologyNodes
+
+    url = 'http://{}:{}/restconf/{}/network-topology:network-topology/'.format(options.odlIp, options.odlPort, mdsalTreeType)
+    data = grabJson(url)
+
+    if not CONST_NET_TOPOLOGY in data:
+        printError( '{}\n\nError: did not find {} in data'.format(data, CONST_NET_TOPOLOGY) )
+        sys.exit(1)
+
+    data2 = data[CONST_NET_TOPOLOGY]
+    if not CONST_TOPOLOGY in data2:
+        printError( '{}\n\nError: did not find {} in data2'.format(data2, CONST_TOPOLOGY) )
+        sys.exit(1)
+
+    jsonTopologyNodes = data2[CONST_TOPOLOGY]
+
+# --
+
+def buildDpidCache():
+    global jsonTopologyNodes
+    global nodeIdToDpidCache
+
+    # only needed if not parsing operational tree
+    if getMdsalTreeType() == CONST_OPERATIONAL:
+        return
+
+    jsonTopologyNodesSave = jsonTopologyNodes
+    grabTopologyJson(CONST_OPERATIONAL)
+    jsonTopologyNodesLocal = jsonTopologyNodes
+    jsonTopologyNodes = jsonTopologyNodesSave
+
+    for nodeDict in jsonTopologyNodesLocal:
+        if nodeDict.get('topology-id') != 'ovsdb:1':
+            continue
+        for node in nodeDict.get('node', []):
+            if node.get('node-id') is None or node.get('ovsdb:datapath-id') is None:
+                continue
+            nodeIdToDpidCache[ node.get('node-id') ] = node.get('ovsdb:datapath-id')
+
+# --
+
+def parseTopologyJson(mdsalTreeType):
+    for nodeDict in jsonTopologyNodes:
+        if not 'topology-id' in nodeDict:
+            continue
+        prtLn('{} {} keys are: {}'.format(mdsalTreeType, nodeDict['topology-id'], nodeDict.keys()), 3)
+        if 'node' in nodeDict:
+            nodeIndex = 0
+            for currNode in nodeDict['node']:
+                parseTopologyJsonNode('', mdsalTreeType, nodeDict['topology-id'], nodeIndex, currNode)
+                nodeIndex += 1
+            prtLn('', 2)
+        if (mdsalTreeType == CONST_OPERATIONAL) and (nodeDict['topology-id'] == 'flow:1') and ('link' in nodeDict):
+            parseTopologyJsonFlowLink(nodeDict['link'])
+
+    prtLn('', 1)
+
+# --
+
+def parseTopologyJsonNode(indent, mdsalTreeType, topologyId, nodeIndex, node):
+    if node.get('node-id') is None:
+        printError( 'Warning: unexpected node: {}\n'.format(node) )
+        return
+    prt('{} {} node[{}] {} '.format(indent + mdsalTreeType, topologyId, nodeIndex, node.get('node-id')), 2)
+    if 'ovsdb:bridge-name' in node:
+        prtLn('', 2)
+        parseTopologyJsonNodeBridge(indent + '  ', mdsalTreeType, topologyId, nodeIndex, node)
+    elif 'ovsdb:connection-info' in node:
+        prtLn('', 2)
+        parseTopologyJsonNodeOvsdb(indent + '  ', mdsalTreeType, topologyId, nodeIndex, node)
+    else:
+        prtLn('keys: {}'.format(node.keys()), 2)
+
+# --
+
+def parseTopologyJsonNodeOvsdb(indent, mdsalTreeType, topologyId, nodeIndex, node):
+    keys = node.keys()
+    keys.sort()
+    for k in keys:
+        prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
+
+    connectionInfoRaw = node.get('ovsdb:connection-info')
+    connectionInfo = {}
+    if type(connectionInfoRaw) is dict:
+        connectionInfo['inetMgr'] = connectionInfoRaw.get('local-ip') + ':' + str( connectionInfoRaw.get('local-port') )
+        connectionInfo['inetNode'] = connectionInfoRaw.get('remote-ip') + ':' + str( connectionInfoRaw.get('remote-port') )
+    otherConfigsRaw = node.get('ovsdb:openvswitch-other-configs')
+    otherLocalIp = ''
+    if type(otherConfigsRaw) is list:
+        for currOtherConfig in otherConfigsRaw:
+            if type(currOtherConfig) is dict and \
+                    currOtherConfig.get('other-config-key') == 'local_ip':
+                otherLocalIp = currOtherConfig.get('other-config-value')
+                break
+
+    ovsdbNode = OvsdbNode(node.get('node-id'), connectionInfo.get('inetMgr'), connectionInfo.get('inetNode'), otherLocalIp, node.get('ovsdb:ovs-version'))
+    state.registerOvsdbNode(ovsdbNode)
+    prtLn('Added {}'.format(ovsdbNode), 1)
+
+# --
+
+def parseTopologyJsonNodeBridge(indent, mdsalTreeType, topologyId, nodeIndex, node):
+
+    controllerTarget = None
+    controllerConnected = None
+    controllerEntries = node.get('ovsdb:controller-entry')
+    if type(controllerEntries) is list:
+        for currControllerEntry in controllerEntries:
+            if type(currControllerEntry) is dict:
+                controllerTarget = currControllerEntry.get('target')
+                controllerConnected = currControllerEntry.get('is-connected')
+                break
+
+    nodeId = node.get('node-id')
+    dpId = node.get('ovsdb:datapath-id', nodeIdToDpidCache.get(nodeId))
+    bridgeNode = BridgeNode(nodeId, dpId, node.get('ovsdb:bridge-name'), controllerTarget, controllerConnected)
+
+    keys = node.keys()
+    keys.sort()
+    for k in keys:
+        if k == 'termination-point' and len(node[k]) > 0:
+            tpIndex = 0
+            for tp in node[k]:
+                terminationPoint = parseTopologyJsonNodeBridgeTerminationPoint('%stermination-point[%d] :' % (indent, tpIndex), mdsalTreeType, topologyId, nodeIndex, node, tp)
+
+                # skip boring tps
+                if terminationPoint.ofPort == CONST_TP_OF_INTERNAL and \
+                        (terminationPoint.name == 'br-ex' or terminationPoint.name == 'br-int'):
+                    pass
+                else:
+                    bridgeNode.addTerminationPoint(terminationPoint)
+
+                tpIndex += 1
+        else:
+            prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
+
+    state.registerBridgeNode(bridgeNode)
+    prtLn('Added {}'.format(bridgeNode), 1)
+
+
+# --
+
+def parseTopologyJsonNodeBridgeTerminationPoint(indent, mdsalTreeType, topologyId, nodeIndex, node, tp):
+    attachedMac = ''
+    ifaceId = ''
+
+    keys = tp.keys()
+    keys.sort()
+    for k in keys:
+        if (k == 'ovsdb:port-external-ids' or k == 'ovsdb:interface-external-ids') and len(tp[k]) > 0:
+            extIdIndex = 0
+            for extId in tp[k]:
+                prtLn('{} {}[{}] {} : {}'.format(indent, k, extIdIndex, extId.get('external-id-key'), extId.get('external-id-value')), 2)
+                extIdIndex += 1
+
+                if extId.get('external-id-key') == 'attached-mac':
+                    attachedMac = extId.get('external-id-value')
+                if extId.get('external-id-key') == 'iface-id':
+                    ifaceId = extId.get('external-id-value')
+        else:
+            prtLn('{} {} : {}'.format(indent, k, tp[k]), 2)
+
+    return TerminationPoint(tp.get('ovsdb:name'),
+                            tp.get('ovsdb:ofport'),
+                            tp.get('ovsdb:interface-type', '').split('-')[-1],
+                            attachedMac, ifaceId)
+
+# --
+
+def parseTopologyJsonFlowLink(link):
+    linkCount = 0
+    spc = ' ' * 2
+    for currLinkDict in link:
+        linkCount += 1
+        linkId = currLinkDict.get('link-id')
+        linkDest = currLinkDict.get('destination', {}).get('dest-tp')
+        linkSrc = currLinkDict.get('source', {}).get('source-tp')
+
+        linkDestNode = currLinkDict.get('destination', {}).get('dest-node')
+        linkSrcNode = currLinkDict.get('source', {}).get('source-node')
+        prtLn('{} {} {} => {}:{} -> {}:{}'.format(spc, linkCount, linkId, linkSrcNode, linkSrc.split(':')[-1], linkDestNode, linkDest.split(':')[-1]), 3)
+
+        if linkId != linkSrc:
+            printError('Warning: ignoring link with unexpected id: %s != %s\n' % (linkId, linkSrc))
+            continue
+        else:
+            state.ofLinks[linkSrc] = linkDest
+
+# --
+
+def showPrettyNamesMap():
+    spc = ' ' * 2
+    if not options.useAlias or len(state.bridgeNodes) == 0:
+        return
+
+    prtLn('aliasMap:', 0)
+    resultMap = {}
+    for bridge in state.bridgeNodes.values():
+        resultMap[ bridge.alias ] = bridge.getOpenflowName()
+
+    resultMapKeys = resultMap.keys()
+    resultMapKeys.sort()
+
+    for resultMapKey in resultMapKeys:
+        prtLn('{0}{1: <10} -> {2}'.format(spc, resultMapKey, resultMap[resultMapKey]), 0)
+    prtLn('', 0)
+
+# --
+
+def showNodesPretty():
+    if len(state.ovsdbNodes) == 0:
+        showBridgeOnlyNodes()
+        return
+
+    aliasDict = { state.ovsdbNodes[nodeId].alias : nodeId for nodeId in state.ovsdbNodes.keys() }
+    aliasDictKeys = aliasDict.keys()
+    aliasDictKeys.sort()
+    for ovsAlias in aliasDictKeys:
+        ovsdbNode = state.ovsdbNodes[ aliasDict[ovsAlias] ]
+
+        prt('ovsdbNode:{} mgr:{} version:{}'.format(ovsAlias, ovsdbNode.inetMgr, ovsdbNode.ovsVersion), 0)
+        if ovsdbNode.inetNode.split(':')[0] != ovsdbNode.otherLocalIp:
+            prt(' **localIp:{}'.format(ovsdbNode.otherLocalIp), 0)
+        prtLn('', 0)
+        showPrettyBridgeNodes('  ', getNodeBridgeIds(ovsdbNode.nodeId), ovsdbNode)
+    showBridgeOnlyNodes(True)
+    prtLn('', 0)
+
+# --
+
+def showFlowInfoPretty():
+    global flowInfoNodes
+    spc = ' ' * 2
+
+    if not options.showFlows:
+        return
+
+    if len(flowInfoNodes) == 0:
+        prtLn('no flowInfo found\n', 0)
+        return
+
+    # translate flowKeys (openflow:123124) into their alias format
+    # then sort it and translate back, so we list them in the order
+    flowInfoNodeKeysDict = {}
+    for flowInfoNodeKey in flowInfoNodes.keys():
+        flowInfoNodeKeysDict[ showPrettyName(flowInfoNodeKey) ] = flowInfoNodeKey
+    flowInfoNodeKeysKeys = flowInfoNodeKeysDict.keys()
+    flowInfoNodeKeysKeys.sort()
+
+    flowInfoNodesKeys = [ flowInfoNodeKeysDict[ x ] for x in flowInfoNodeKeysKeys ]
+
+    nodeIdToDpidCacheReverse = {dataPathIdToOfFormat(v): k for k, v in nodeIdToDpidCache.items()}
+    nodesVisited = 0
+    for flowInfoNodeKey in flowInfoNodesKeys:
+        if nodesVisited > 0: prtLn('', 0)
+
+        nodeName = showPrettyName(flowInfoNodeKey)
+        if nodeName == flowInfoNodeKey:
+            nodeName += '  ( {} )'.format( nodeIdToDpidCacheReverse.get(flowInfoNodeKey, 'node_not_in_topology') )
+
+        prtLn('{} tree flows at {}'.format(getMdsalTreeType(), nodeName), 0)
+        flowInfoNode = flowInfoNodes[flowInfoNodeKey]
+        flowInfoTables = flowInfoNode.keys()
+        flowInfoTables.sort()
+        for flowInfoTable in flowInfoTables:
+            for rule in flowInfoNode[flowInfoTable]:
+                prtLn('{}table {}: {}'.format(spc, flowInfoTable, rule), 0)
+        nodesVisited += 1
+
+    prtLn('', 0)
+
+# --
+
+def getNodeBridgeIds(nodeIdFilter = None):
+    resultMap = {}
+    for bridge in state.bridgeNodes.values():
+        if nodeIdFilter is None or nodeIdFilter in bridge.nodeId:
+            resultMap[ bridge.alias ] = bridge.nodeId
+    resultMapKeys = resultMap.keys()
+    resultMapKeys.sort()
+    return [ resultMap[x] for x in resultMapKeys ]
+
+# --
+
+def showPrettyBridgeNodes(indent, bridgeNodeIds, ovsdbNode = None):
+    if bridgeNodeIds is None:
+        return
+
+    for nodeId in bridgeNodeIds:
+        bridgeNode = state.bridgeNodes[nodeId]
+        prt('{}{}:{}'.format(indent, showPrettyName(nodeId), bridgeNode.name), 0)
+
+        if ovsdbNode is None or \
+                bridgeNode.controllerTarget is None or \
+                bridgeNode.controllerTarget == '' or \
+                ovsdbNode.inetMgr.split(':')[0] != bridgeNode.controllerTarget.split(':')[-2] or \
+                bridgeNode.controllerConnected != True:
+            prt(' controller:{}'.format(bridgeNode.controllerTarget), 0)
+            prt(' connected:{}'.format(bridgeNode.controllerConnected), 0)
+        prtLn('', 0)
+        showPrettyTerminationPoints(indent + '  ', bridgeNode.tps)
+
+# --
+
+def showBridgeOnlyNodes(showOrphansOnly = False):
+    if len(state.bridgeNodes) == 0:
+        return
+
+    # group bridges by nodeId prefix
+    resultMap = {}
+    for bridge in state.bridgeNodes.values():
+        nodePrefix = bridge.nodeId.split('/bridge/')[0]
+
+        if showOrphansOnly and nodePrefix in state.ovsdbNodes:
+            continue
+
+        if nodePrefix in resultMap:
+            resultMap[nodePrefix][bridge.alias] = bridge.nodeId
+        else:
+            resultMap[nodePrefix] = { bridge.alias: bridge.nodeId }
+    resultMapKeys = resultMap.keys()
+    resultMapKeys.sort()
+
+    if len(resultMapKeys) == 0:
+        return  #noop
+
+    for nodePrefix in resultMapKeys:
+        nodePrefixEntriesKeys = resultMap[nodePrefix].keys()
+        nodePrefixEntriesKeys.sort()
+        # prtLn('Bridges in {}: {}'.format(nodePrefix, nodePrefixEntriesKeys), 0)
+        prtLn('Bridges in {}'.format(nodePrefix), 0)
+        nodeIds = [ resultMap[nodePrefix][nodePrefixEntry] for nodePrefixEntry in nodePrefixEntriesKeys ]
+        showPrettyBridgeNodes('  ', nodeIds)
+
+    prtLn('', 0)
+
+# --
+
+def showPrettyTerminationPoints(indent, tps):
+
+    tpsDict = {}
+    for tp in tps:
+        tpsDict[ tp.ofPort ] = tp
+
+    tpDictKeys = tpsDict.keys()
+    tpDictKeys.sort()
+    for tpKey in tpDictKeys:
+        tp = tpsDict[tpKey]
+        prt('{}of:{} {}'.format(indent, tp.ofPort, tp.name), 0)
+        if tp.mac != '':
+            prt(' {}:{}'.format('mac', tp.mac), 0)
+        if tp.ifaceId != '':
+            prt(' {}:{}'.format('ifaceId', tp.ifaceId), 0)
+
+        prtLn('', 0)
+
+# --
+
+def dataPathIdToOfFormat(dpId):
+    return 'openflow:' + str( int('0x' + dpId.replace(':',''), 16) )
+
+# --
+
+def showPrettyName(name):
+    if not options.useAlias:
+        return name
+
+    # handle both openflow:138604958315853:2 and openflow:138604958315853 (aka dpid)
+    # also handle ovsdb://uuid/5c72ec51-1e71-4a04-ab0b-b044fb5f4dc0/bridge/br-int  (aka nodeId)
+    #
+    nameSplit = name.split(':')
+    ofName = ':'.join(nameSplit[:2])
+    ofPart = ''
+    if len(nameSplit) > 2:
+        ofPart = ':' + ':'.join(nameSplit[2:])
+
+    for bridge in state.bridgeNodes.values():
+        if bridge.getOpenflowName() == ofName or bridge.nodeId == name:
+            return '{}{}'.format(bridge.alias, ofPart)
+
+    # not found, return paramIn
+    return name
+
+# --
+
+def showOfLinks():
+    spc = ' ' * 2
+    ofLinksKeys = state.ofLinks.keys()
+    ofLinksKeys.sort()
+    ofLinksKeysVisited = set()
+
+    if len(ofLinksKeys) == 0:
+        # prtLn('no ofLinks found\n', 0)
+        return
+
+    prtLn('ofLinks (discover via lldp):', 0)
+    for ofLinkKey in ofLinksKeys:
+        if ofLinkKey in ofLinksKeysVisited:
+            continue
+        if state.ofLinks.get( state.ofLinks[ofLinkKey] ) == ofLinkKey:
+            prtLn('{}{} <-> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
+            ofLinksKeysVisited.add(state.ofLinks[ofLinkKey])
+        else:
+            prtLn('{}{} -> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
+        ofLinksKeysVisited.add(ofLinkKey)
+    prtLn('', 0)
+
+# --
+
+def parseArgv():
+    global options
+
+    parser = optparse.OptionParser(version="0.1")
+    parser.add_option("-d", "--debug", action="count", dest="debug", default=CONST_DEFAULT_DEBUG,
+                      help="Verbosity. Can be provided multiple times for more debug.")
+    parser.add_option("-n", "--noalias", action="store_false", dest="useAlias", default=True,
+                      help="Do not map nodeId of bridges to an alias")
+    parser.add_option("-i", "--ip", action="store", type="string", dest="odlIp", default="localhost",
+                      help="opendaylights ip address")
+    parser.add_option("-t", "--port", action="store", type="string", dest="odlPort", default="8080",
+                      help="opendaylights listening tcp port on restconf northbound")
+    parser.add_option("-u", "--user", action="store", type="string", dest="odlUsername", default="admin",
+                      help="opendaylight restconf username")
+    parser.add_option("-p", "--password", action="store", type="string", dest="odlPassword", default="admin",
+                      help="opendaylight restconf password")
+    parser.add_option("-c", "--config", action="store_true", dest="useConfigTree", default=False,
+                      help="parse mdsal restconf config tree instead of operational tree")
+    parser.add_option("-f", "--hide-flows", action="store_false", dest="showFlows", default=True,
+                      help="hide flows")
+
+    (options, args) = parser.parse_args(sys.argv)
+    prtLn('argv options:{} args:{}'.format(options, args), 2)
+
+# --
+
+def doMain():
+    global state
+
+    state = State()
+    parseArgv()
+    buildDpidCache()
+    grabTopologyJson(getMdsalTreeType())
+    grabInventoryJson(getMdsalTreeType())
+    parseTopologyJson(getMdsalTreeType())
+    parseInventoryJson(getMdsalTreeType())
+    showPrettyNamesMap()
+    showNodesPretty()
+    showFlowInfoPretty()
+    showOfLinks()
+
+# --
+
+if __name__ == "__main__":
+    doMain()
+    sys.exit(0)