reduce debug logging 13/71213/2
authorSam Hague <shague@redhat.com>
Mon, 23 Apr 2018 13:09:07 +0000 (09:09 -0400)
committerSam Hague <shague@redhat.com>
Mon, 23 Apr 2018 15:17:50 +0000 (15:17 +0000)
Change-Id: Ieec658adc5a0f3ccfe31daf1d8ee79178c8036d7
Signed-off-by: Sam Hague <shague@redhat.com>
resources/tools/odltools/csit/robotfiles.py
resources/tools/odltools/old/ovs_get_data.py [new file with mode: 0644]
resources/tools/odltools/old/showOvsdbMdsal.py [new file with mode: 0644]

index f4992d2cfd61660cc5b5f7b4b1e4c515572eea60..791e2e7b7072b010d5b692fe06acbf62b8497fca 100644 (file)
@@ -23,8 +23,8 @@ class RobotFiles:
         self.datafilepath = infile
         self.pdata = {}
         self.re_normalize_text = re.compile(r"( \n)|(\[A\[C.*)")
+        # uri=restconf/config/interface-service-bindings:service-bindings, headers=None json=None</msg>
         self.re_uri = re.compile(r"uri=(?P<uri>.*),")
-        self.re_model = re.compile(r"uri=(?P<uri>.*),")
 
     def gunzip_output_file(self):
         infile = self.datafilepath
@@ -93,8 +93,8 @@ class RobotFiles:
         tag = element.tag
         text = element.text
         attribs = element.attrib
-        if logger.isEnabledFor(logging.DEBUG):
-            logger.debug("%s - %s - %s - %s - %s - %s", state.state, state.command, event, tag, (text is not None), attribs)
+        if logger.isEnabledFor(logging.DEBUG) and text is not None and attribs:
+            logger.debug("process_element: %s - %s - %s - %s - %s - %s", state.state, state.command, event, tag, (text is not None), attribs)
         if event == "end":
             if element.tag == "test":
                 state.pdata['nodes'] = state.nodes
@@ -112,27 +112,33 @@ class RobotFiles:
                 state.test_id = element.get("id")
                 state.pdata["name"] = element.get("name")
                 state.state = "test"
+                state.command = ""
         elif event == "start" and state.state == "test":
             # <kw type="teardown" name="Get Test Teardown Debugs" library="OpenStackOperations">
             if element.tag == "kw" and element.get("name") == "Get Test Teardown Debugs":
                 state.state = "debugs"
+                state.command = ""
         elif event == "start" and state.state == "debugs":
             # <arg>Get DumpFlows And Ovsconfig</arg>
             if element.tag == "kw" and element.get("name") == "Get DumpFlows And Ovsconfig":
                 state.state = "nodes"
+                state.command = ""
             # <arg>Get Model Dump</arg>
             if element.tag == "kw" and element.get("name") == "Get Model Dump":
                 state.state = "models"
+                state.command = ""
         elif event == "start" and state.state == "nodes":
             # <arg>${OS_CONTROL_NODE_IP}</arg>
             if element.tag == "arg" and element.text is not None and "${OS_" in element.text:
                 state.node = element.text[element.text.find("{") + 1:element.text.find("}")]
                 state.nodes[state.node] = {}
                 state.state = "nodes2"
+                state.command = ""
         elif event == "start" and state.state == "nodes2":
             # <kw name="Write Commands Until Expected Prompt" library="Utils">
             if element.tag == "kw" and element.get("name") == "Write Commands Until Expected Prompt":
                 state.state = "kw"
+                state.command = ""
         elif event == "start" and state.state == "kw":
             # <arg>ip -o link</arg>
             if element.tag == "arg" and element.text is not None:
@@ -152,9 +158,10 @@ class RobotFiles:
                     # [jenkins@rele ^Mng-36618-350-devstack-newton-0 ~]&gt; ip -o link</msg>
                     if text.find("jenkins") != -1:
                         state.state = "nodes2"
+                        state.command = ""
                     else:
-                        state.command = text
                         state.state = "msg"
+                        state.command = text
         elif state.state == "msg":
             # <msg timestamp="20170414 07:31:21.786" level="INFO">
             if element.tag == "msg" and element.text is not None:
@@ -163,6 +170,7 @@ class RobotFiles:
                 # this command is the last one
                 if state.command == "sudo ovs-ofctl dump-group-stats br-int -OOpenFlow13":
                     state.state = "debugs"
+                    state.command = ""
                 else:
                     # still more debugs for this node
                     state.state = "nodes2"
@@ -172,8 +180,8 @@ class RobotFiles:
             if element.tag == "msg" and element.text is not None and element.text.find("uri") != -1:
                 uri = self.re_uri.search(element.text)
                 if uri is not None and "uri" in uri.group():
-                    state.command = uri.group("uri")
                     state.state = "uri"
+                    state.command = uri.group("uri")
         elif event == "start" and state.state == "models":
             # <kw type="foritem" name="${model} = config/neutronvpn:router-interfaces-map">
             if element.tag == "kw" and "name" in element.attrib:
@@ -182,19 +190,22 @@ class RobotFiles:
                 if len(name_split) == 2:
                     model = name_split[1]
                 if model is not None:
-                    state.command = model
                     state.state = "uri"
+                    state.command = model
         elif state.state == "uri":
             if element.tag == "msg" and element.text is not None and element.text.find("pretty_output") != -1:
                 state.state = "dump"
+                # do not clear the state.command
         elif state.state == "dump":
             if element.tag == "msg" and element.text is not None:
                 state.models[state.command] = element.text
                 # if state.command == "restconf/operational/rendered-service-path:rendered-service-path":
                 if state.command == "restconf/operational/opendaylight-inventory:nodes":
                     state.state = "done"
+                    state.command = ""
                 else:
                     state.state = "models"
+                    state.command = ""
 
     def parse_xml_data_file(self):
         state = self.State()
@@ -205,8 +216,8 @@ class RobotFiles:
                 self.process_element(state, event, element)
                 element.clear()
                 # debugging code to stop after the named test case is processed
-                # if "s1-t2" in self.pdata:
-                    break
+                if "s1-t1" in self.pdata:
+                    break
             root.clear()
 
     def write_pdata(self):
@@ -224,7 +235,7 @@ class RobotFiles:
             mdir = tdir + "/models"
             self.mkdir(mdir)
             for mindex, (model, mdata) in enumerate(test['models'].items()):
-                filename = mdir + "/" + self.fix_model_name(model) + ".txt"
+                filename = mdir + "/" + self.fix_model_name(model) + ".json"
                 with open(filename, 'w') as fp:
                     if mdata is not None:
                         fp.writelines(mdata)
diff --git a/resources/tools/odltools/old/ovs_get_data.py b/resources/tools/odltools/old/ovs_get_data.py
new file mode 100644 (file)
index 0000000..6508486
--- /dev/null
@@ -0,0 +1,92 @@
+import sys
+import re
+
+from datetime import datetime as dt
+from datetime import timedelta as td
+
+base_time = '2017-07-31 20:19:00,698'
+#base_time = None
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S,%f'
+
+def get_ofctl_flows(file_name='ofctl-flows.log'):
+    flow_list = []
+    with open(file_name, 'rb') as f:
+        data = f.read().splitlines()
+        for row in data:
+            meta = row.split(', ')
+            matches, actions = meta.pop().split()
+            flow_list.append(get_flow_dict(meta, matches, actions))
+    return flow_list
+
+
+def get_flow_dict(meta, matches, actions):
+    actions = actions.strip('actions=')
+    flow_dict = {}
+    flow_dict['meta'] = get_meta_dict(meta)
+    flow_dict['matches'] = get_matches_dict(matches)
+    flow_dict['actions'] = get_actions_dict(actions)
+    return flow_dict
+
+
+def get_meta_dict(meta_str):
+    meta = {}
+    for field in meta_str:
+        k, v = field.split('=')
+        if base_time and k == 'duration':
+            v = get_flow_time(v)
+            k = 'installed'
+        meta[k] = v
+    return meta
+
+
+def get_matches_dict(matches_str):
+    return matches_str
+''' Note: This mostly works requires bit of rework for matches like:
+    arp,arp_sa=abc etc.
+    matches = {}
+    for match in matches_str.split(','):
+        if '=' in match:
+            k, v = match.split('=')
+        else:
+            k, v = match, ""
+        matches[k] = v
+    return matches
+'''
+
+
+def get_flow_time(duration):
+    duration = duration.strip('s')
+    time = dt.strptime(base_time, TIME_FORMAT) + td(0, float(duration))
+    time_str = time.strftime(TIME_FORMAT)
+    return time_str
+
+
+def get_actions_dict(action_str):
+    return action_str
+''' todo: Action parsing is WiP and requires much complex logic
+    actions_dict = {}
+    actions = action_str.split(',')
+    actions = iter(actions)
+    for action in actions:
+        if ':' in action:
+            k, v = action.split(':')
+            actions_dict[k] = v
+        elif action.startswith('resubmit'):
+            k = 'resubmit'
+            port = action.strip('resubmit(')
+            try:
+                next_action = next(actions)
+                if next_action.endswith(')'):
+                    table = next_action.strip(')')
+                else:
+                    table = action
+            except StopIteration:
+                break;
+            actions_dict[k] = '('+port+','+table+')'
+    return actions_dict
+'''
+
+
+def print_flow_dict(flows):
+    for flow in flows:
+        print flow['meta'], flow['matches'], flow['actions']
\ No newline at end of file
diff --git a/resources/tools/odltools/old/showOvsdbMdsal.py b/resources/tools/odltools/old/showOvsdbMdsal.py
new file mode 100644 (file)
index 0000000..3d566eb
--- /dev/null
@@ -0,0 +1,725 @@
+#!/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 make_it_a_string(param):
+    result = ""
+    try:
+        result = str( param )
+    except:
+        pass
+    return result
+
+# ======================================================================
+
+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'] = make_it_a_string(connectionInfoRaw.get('local-ip')) + ':' + make_it_a_string(connectionInfoRaw.get('local-port'))
+        connectionInfo['inetNode'] = make_it_a_string(connectionInfoRaw.get('remote-ip')) + ':' + make_it_a_string(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 ] = '{0: <25} {1: <7} {2}'.format(bridge.getOpenflowName(), bridge.name, bridge.dpId)
+
+    for resultMapKey in sorted(resultMap):
+        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)