3 import urllib2, base64, json, sys, optparse
10 jsonInventoryNodes = []
12 nodeIdToDpidCache = {}
14 CONST_OPERATIONAL = 'operational'
15 CONST_CONFIG = 'config'
16 CONST_NET_TOPOLOGY = 'network-topology'
17 CONST_TOPOLOGY = 'topology'
18 CONST_TP_OF_INTERNAL = 65534
19 CONST_ALIASES = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet',
20 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango',
21 'uniform', 'victor', 'whiskey', 'xray', 'yankee', 'zulu']
26 self.nextAliasIndex = 0
27 self.nextAliasWrap = 0
28 self.nodeIdToAlias = {}
35 return 'State {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
36 'nextAliasIndex', self.nextAliasIndex,
37 'nextAliasWrap', self.nextAliasWrap,
38 'bridgeNodes_ids', self.bridgeNodes.keys(),
39 'ovsdbNodes_ids', self.ovsdbNodes.keys(),
40 'nodeIdToAlias', self.nodeIdToAlias)
42 def registerBridgeNode(self, bridgeNode):
43 self.bridgeNodes[bridgeNode.nodeId] = bridgeNode
45 def registerOvsdbNode(self, ovsdbNode):
46 self.ovsdbNodes[ovsdbNode.nodeId] = ovsdbNode
48 def getNextAlias(self, nodeId):
49 result = CONST_ALIASES[ self.nextAliasIndex ]
50 if self.nextAliasWrap > 0:
51 result += '_' + str(self.nextAliasWrap)
53 if CONST_ALIASES[ self.nextAliasIndex ] == CONST_ALIASES[-1]:
54 self.nextAliasIndex = 0
55 self.nextAliasWrap += 1
57 self.nextAliasIndex += 1
59 self.nodeIdToAlias[ nodeId ] = result
64 class TerminationPoint:
65 def __init__(self, name, ofPort, tpType, mac='', ifaceId=''):
70 self.ifaceId = ifaceId
73 result = '{} {}:{}'.format(self.name, 'of', self.ofPort)
76 result += ' {}:{}'.format('type', self.tpType)
78 result += ' {}:{}'.format('mac', self.mac)
79 if self.ifaceId != '':
80 result += ' {}:{}'.format('ifaceId', self.ifaceId)
82 return '{' + result + '}'
87 def __init__(self, nodeId, dpId, name, controllerTarget, controllerConnected):
89 self.alias = state.getNextAlias(nodeId)
93 self.controllerTarget = controllerTarget
94 self.controllerConnected = controllerConnected
97 def getOpenflowName(self):
100 return dataPathIdToOfFormat(self.dpId)
102 def addTerminationPoint(self, terminationPoint):
103 self.tps.append(terminationPoint)
106 return 'BridgeNode {}:{} {}:{} {}:{} {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
108 'nodeId', self.nodeId,
110 'openflowName', self.getOpenflowName(),
112 'controllerTarget', self.controllerTarget,
113 'controllerConnected', self.controllerConnected,
119 def __init__(self, nodeId, inetMgr, inetNode, otherLocalIp, ovsVersion):
122 self.alias = inetNode
126 self.inetMgr = inetMgr
127 self.inetNode = inetNode
128 self.otherLocalIp = otherLocalIp
129 self.ovsVersion = ovsVersion
132 return 'OvsdbNode {}:{} {}:{} {}:{} {}:{} {}:{} {}:{}'.format(
134 'nodeId', self.nodeId,
135 'inetMgr', self.inetMgr,
136 'inetNode', self.inetNode,
137 'otherLocalIp', self.otherLocalIp,
138 'ovsVersion', self.ovsVersion)
140 # ======================================================================
143 sys.stderr.write(msg)
145 # ======================================================================
147 def prt(msg, logLevel=0):
148 prtCommon(msg, logLevel)
149 def prtLn(msg, logLevel=0):
150 prtCommon('{}\n'.format(msg), logLevel)
151 def prtCommon(msg, logLevel):
152 if options.debug >= logLevel:
153 sys.stdout.write(msg)
155 # ======================================================================
157 def getMdsalTreeType():
158 if options.useConfigTree:
160 return CONST_OPERATIONAL
167 request = urllib2.Request(url)
168 # You need the replace to handle encodestring adding a trailing newline
169 # (https://docs.python.org/2/library/base64.html#base64.encodestring)
170 base64string = base64.encodestring('{}:{}'.format(options.odlUsername, options.odlPassword)).replace('\n', '')
171 request.add_header('Authorization', 'Basic {}'.format(base64string))
172 result = urllib2.urlopen(request)
173 except urllib2.URLError, e:
174 printError('Unable to send request: {}\n'.format(e))
177 if (result.code != 200):
178 printError( '{}\n{}\n\nError: unexpected code: {}\n'.format(result.info(), result.read(), result.code) )
181 data = json.load(result)
187 def grabInventoryJson(mdsalTreeType):
188 global jsonInventoryNodes
190 url = 'http://{}:{}/restconf/{}/opendaylight-inventory:nodes/'.format(options.odlIp, options.odlPort, mdsalTreeType)
193 if not 'nodes' in data:
194 printError( '{}\n\nError: did not find nodes in {}'.format(data, url) )
197 data2 = data['nodes']
198 if not 'node' in data2:
199 printError( '{}\n\nError: did not find node in {}'.format(data2, url) )
202 jsonInventoryNodes = data2['node']
206 def parseInventoryJson(mdsalTreeType):
207 global jsonInventoryNodes
210 for nodeDict in jsonInventoryNodes:
211 if not 'id' in nodeDict:
214 bridgeOfId = nodeDict.get('id')
215 prtLn('inventory node {} has keys {}'.format(bridgeOfId, nodeDict.keys()), 3)
220 for currNodeId in state.bridgeNodes.keys():
221 if state.bridgeNodes[currNodeId].getOpenflowName() == bridgeOfId:
222 bridgeNodeId = currNodeId
223 bridgeNode = state.bridgeNodes[currNodeId]
226 if bridgeNodeId is None:
227 prtLn('inventory node {}'.format(bridgeOfId), 1)
229 prtLn('inventory node {}, aka {}, aka {}'.format(bridgeOfId, bridgeNodeId, showPrettyName(bridgeNodeId)), 1)
234 prtLn('{}features: {}'.format(indent, nodeDict.get('flow-node-inventory:switch-features', {})), 2)
235 prt('{}sw: {}'.format(indent, nodeDict.get('flow-node-inventory:software')), 2)
236 prt('{}hw: {}'.format(indent, nodeDict.get('flow-node-inventory:hardware')), 2)
237 prt('{}manuf: {}'.format(indent, nodeDict.get('flow-node-inventory:manufacturer')), 2)
238 prtLn('{}ip: {}'.format(indent, nodeDict.get('flow-node-inventory:ip-address')), 2)
241 for inventoryEntry in nodeDict.get('flow-node-inventory:table', []):
242 if 'id' in inventoryEntry:
243 currTableId = inventoryEntry.get('id')
244 for currFlow in inventoryEntry.get('flow', []):
245 prtLn('{}table {}: {}'.format(indent, currTableId, currFlow.get('id')), 1)
246 prtLn('{}{}'.format(indent * 2, currFlow), 2)
248 if currTableId in flowInfoNode:
249 flowInfoNode[ currTableId ].append( currFlow.get('id') )
251 flowInfoNode[ currTableId ] = [ currFlow.get('id') ]
255 for currTableId in flowInfoNode.keys():
256 flowInfoNode[currTableId].sort()
258 # store info collected in flowInfoNodes
259 flowInfoNodes[bridgeOfId] = flowInfoNode
263 def grabTopologyJson(mdsalTreeType):
264 global jsonTopologyNodes
266 url = 'http://{}:{}/restconf/{}/network-topology:network-topology/'.format(options.odlIp, options.odlPort, mdsalTreeType)
269 if not CONST_NET_TOPOLOGY in data:
270 printError( '{}\n\nError: did not find {} in data'.format(data, CONST_NET_TOPOLOGY) )
273 data2 = data[CONST_NET_TOPOLOGY]
274 if not CONST_TOPOLOGY in data2:
275 printError( '{}\n\nError: did not find {} in data2'.format(data2, CONST_TOPOLOGY) )
278 jsonTopologyNodes = data2[CONST_TOPOLOGY]
282 def buildDpidCache():
283 global jsonTopologyNodes
284 global nodeIdToDpidCache
286 # only needed if not parsing operational tree
287 if getMdsalTreeType() == CONST_OPERATIONAL:
290 jsonTopologyNodesSave = jsonTopologyNodes
291 grabTopologyJson(CONST_OPERATIONAL)
292 jsonTopologyNodesLocal = jsonTopologyNodes
293 jsonTopologyNodes = jsonTopologyNodesSave
295 for nodeDict in jsonTopologyNodesLocal:
296 if nodeDict.get('topology-id') != 'ovsdb:1':
298 for node in nodeDict.get('node', []):
299 if node.get('node-id') is None or node.get('ovsdb:datapath-id') is None:
301 nodeIdToDpidCache[ node.get('node-id') ] = node.get('ovsdb:datapath-id')
305 def parseTopologyJson(mdsalTreeType):
306 for nodeDict in jsonTopologyNodes:
307 if not 'topology-id' in nodeDict:
309 prtLn('{} {} keys are: {}'.format(mdsalTreeType, nodeDict['topology-id'], nodeDict.keys()), 3)
310 if 'node' in nodeDict:
312 for currNode in nodeDict['node']:
313 parseTopologyJsonNode('', mdsalTreeType, nodeDict['topology-id'], nodeIndex, currNode)
316 if (mdsalTreeType == CONST_OPERATIONAL) and (nodeDict['topology-id'] == 'flow:1') and ('link' in nodeDict):
317 parseTopologyJsonFlowLink(nodeDict['link'])
323 def parseTopologyJsonNode(indent, mdsalTreeType, topologyId, nodeIndex, node):
324 if node.get('node-id') is None:
325 printError( 'Warning: unexpected node: {}\n'.format(node) )
327 prt('{} {} node[{}] {} '.format(indent + mdsalTreeType, topologyId, nodeIndex, node.get('node-id')), 2)
328 if 'ovsdb:bridge-name' in node:
330 parseTopologyJsonNodeBridge(indent + ' ', mdsalTreeType, topologyId, nodeIndex, node)
331 elif 'ovsdb:connection-info' in node:
333 parseTopologyJsonNodeOvsdb(indent + ' ', mdsalTreeType, topologyId, nodeIndex, node)
335 prtLn('keys: {}'.format(node.keys()), 2)
339 def parseTopologyJsonNodeOvsdb(indent, mdsalTreeType, topologyId, nodeIndex, node):
343 prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
345 connectionInfoRaw = node.get('ovsdb:connection-info')
347 if type(connectionInfoRaw) is dict:
348 connectionInfo['inetMgr'] = connectionInfoRaw.get('local-ip') + ':' + str( connectionInfoRaw.get('local-port') )
349 connectionInfo['inetNode'] = connectionInfoRaw.get('remote-ip') + ':' + str( connectionInfoRaw.get('remote-port') )
350 otherConfigsRaw = node.get('ovsdb:openvswitch-other-configs')
352 if type(otherConfigsRaw) is list:
353 for currOtherConfig in otherConfigsRaw:
354 if type(currOtherConfig) is dict and \
355 currOtherConfig.get('other-config-key') == 'local_ip':
356 otherLocalIp = currOtherConfig.get('other-config-value')
359 ovsdbNode = OvsdbNode(node.get('node-id'), connectionInfo.get('inetMgr'), connectionInfo.get('inetNode'), otherLocalIp, node.get('ovsdb:ovs-version'))
360 state.registerOvsdbNode(ovsdbNode)
361 prtLn('Added {}'.format(ovsdbNode), 1)
365 def parseTopologyJsonNodeBridge(indent, mdsalTreeType, topologyId, nodeIndex, node):
367 controllerTarget = None
368 controllerConnected = None
369 controllerEntries = node.get('ovsdb:controller-entry')
370 if type(controllerEntries) is list:
371 for currControllerEntry in controllerEntries:
372 if type(currControllerEntry) is dict:
373 controllerTarget = currControllerEntry.get('target')
374 controllerConnected = currControllerEntry.get('is-connected')
377 nodeId = node.get('node-id')
378 dpId = node.get('ovsdb:datapath-id', nodeIdToDpidCache.get(nodeId))
379 bridgeNode = BridgeNode(nodeId, dpId, node.get('ovsdb:bridge-name'), controllerTarget, controllerConnected)
384 if k == 'termination-point' and len(node[k]) > 0:
387 terminationPoint = parseTopologyJsonNodeBridgeTerminationPoint('%stermination-point[%d] :' % (indent, tpIndex), mdsalTreeType, topologyId, nodeIndex, node, tp)
390 if terminationPoint.ofPort == CONST_TP_OF_INTERNAL and \
391 (terminationPoint.name == 'br-ex' or terminationPoint.name == 'br-int'):
394 bridgeNode.addTerminationPoint(terminationPoint)
398 prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
400 state.registerBridgeNode(bridgeNode)
401 prtLn('Added {}'.format(bridgeNode), 1)
406 def parseTopologyJsonNodeBridgeTerminationPoint(indent, mdsalTreeType, topologyId, nodeIndex, node, tp):
413 if (k == 'ovsdb:port-external-ids' or k == 'ovsdb:interface-external-ids') and len(tp[k]) > 0:
416 prtLn('{} {}[{}] {} : {}'.format(indent, k, extIdIndex, extId.get('external-id-key'), extId.get('external-id-value')), 2)
419 if extId.get('external-id-key') == 'attached-mac':
420 attachedMac = extId.get('external-id-value')
421 if extId.get('external-id-key') == 'iface-id':
422 ifaceId = extId.get('external-id-value')
424 prtLn('{} {} : {}'.format(indent, k, tp[k]), 2)
426 return TerminationPoint(tp.get('ovsdb:name'),
427 tp.get('ovsdb:ofport'),
428 tp.get('ovsdb:interface-type', '').split('-')[-1],
429 attachedMac, ifaceId)
433 def parseTopologyJsonFlowLink(link):
436 for currLinkDict in link:
438 linkId = currLinkDict.get('link-id')
439 linkDest = currLinkDict.get('destination', {}).get('dest-tp')
440 linkSrc = currLinkDict.get('source', {}).get('source-tp')
442 linkDestNode = currLinkDict.get('destination', {}).get('dest-node')
443 linkSrcNode = currLinkDict.get('source', {}).get('source-node')
444 prtLn('{} {} {} => {}:{} -> {}:{}'.format(spc, linkCount, linkId, linkSrcNode, linkSrc.split(':')[-1], linkDestNode, linkDest.split(':')[-1]), 3)
446 if linkId != linkSrc:
447 printError('Warning: ignoring link with unexpected id: %s != %s\n' % (linkId, linkSrc))
450 state.ofLinks[linkSrc] = linkDest
454 def showPrettyNamesMap():
456 if not options.useAlias or len(state.bridgeNodes) == 0:
459 prtLn('aliasMap:', 0)
461 for bridge in state.bridgeNodes.values():
462 resultMap[ bridge.alias ] = '{0: <25} {1: <7} {2}'.format(bridge.getOpenflowName(), bridge.name, bridge.dpId)
464 for resultMapKey in sorted(resultMap):
465 prtLn('{0}{1: <10} -> {2}'.format(spc, resultMapKey, resultMap[resultMapKey]), 0)
470 def showNodesPretty():
471 if len(state.ovsdbNodes) == 0:
472 showBridgeOnlyNodes()
475 aliasDict = { state.ovsdbNodes[nodeId].alias : nodeId for nodeId in state.ovsdbNodes.keys() }
476 aliasDictKeys = aliasDict.keys()
478 for ovsAlias in aliasDictKeys:
479 ovsdbNode = state.ovsdbNodes[ aliasDict[ovsAlias] ]
481 prt('ovsdbNode:{} mgr:{} version:{}'.format(ovsAlias, ovsdbNode.inetMgr, ovsdbNode.ovsVersion), 0)
482 if ovsdbNode.inetNode.split(':')[0] != ovsdbNode.otherLocalIp:
483 prt(' **localIp:{}'.format(ovsdbNode.otherLocalIp), 0)
485 showPrettyBridgeNodes(' ', getNodeBridgeIds(ovsdbNode.nodeId), ovsdbNode)
486 showBridgeOnlyNodes(True)
491 def showFlowInfoPretty():
495 if not options.showFlows:
498 if len(flowInfoNodes) == 0:
499 prtLn('no flowInfo found\n', 0)
502 # translate flowKeys (openflow:123124) into their alias format
503 # then sort it and translate back, so we list them in the order
504 flowInfoNodeKeysDict = {}
505 for flowInfoNodeKey in flowInfoNodes.keys():
506 flowInfoNodeKeysDict[ showPrettyName(flowInfoNodeKey) ] = flowInfoNodeKey
507 flowInfoNodeKeysKeys = flowInfoNodeKeysDict.keys()
508 flowInfoNodeKeysKeys.sort()
510 flowInfoNodesKeys = [ flowInfoNodeKeysDict[ x ] for x in flowInfoNodeKeysKeys ]
512 nodeIdToDpidCacheReverse = {dataPathIdToOfFormat(v): k for k, v in nodeIdToDpidCache.items()}
514 for flowInfoNodeKey in flowInfoNodesKeys:
515 if nodesVisited > 0: prtLn('', 0)
517 nodeName = showPrettyName(flowInfoNodeKey)
518 if nodeName == flowInfoNodeKey:
519 nodeName += ' ( {} )'.format( nodeIdToDpidCacheReverse.get(flowInfoNodeKey, 'node_not_in_topology') )
521 prtLn('{} tree flows at {}'.format(getMdsalTreeType(), nodeName), 0)
522 flowInfoNode = flowInfoNodes[flowInfoNodeKey]
523 flowInfoTables = flowInfoNode.keys()
524 flowInfoTables.sort()
525 for flowInfoTable in flowInfoTables:
526 for rule in flowInfoNode[flowInfoTable]:
527 prtLn('{}table {}: {}'.format(spc, flowInfoTable, rule), 0)
534 def getNodeBridgeIds(nodeIdFilter = None):
536 for bridge in state.bridgeNodes.values():
537 if nodeIdFilter is None or nodeIdFilter in bridge.nodeId:
538 resultMap[ bridge.alias ] = bridge.nodeId
539 resultMapKeys = resultMap.keys()
541 return [ resultMap[x] for x in resultMapKeys ]
545 def showPrettyBridgeNodes(indent, bridgeNodeIds, ovsdbNode = None):
546 if bridgeNodeIds is None:
549 for nodeId in bridgeNodeIds:
550 bridgeNode = state.bridgeNodes[nodeId]
551 prt('{}{}:{}'.format(indent, showPrettyName(nodeId), bridgeNode.name), 0)
553 if ovsdbNode is None or \
554 bridgeNode.controllerTarget is None or \
555 bridgeNode.controllerTarget == '' or \
556 ovsdbNode.inetMgr.split(':')[0] != bridgeNode.controllerTarget.split(':')[-2] or \
557 bridgeNode.controllerConnected != True:
558 prt(' controller:{}'.format(bridgeNode.controllerTarget), 0)
559 prt(' connected:{}'.format(bridgeNode.controllerConnected), 0)
561 showPrettyTerminationPoints(indent + ' ', bridgeNode.tps)
565 def showBridgeOnlyNodes(showOrphansOnly = False):
566 if len(state.bridgeNodes) == 0:
569 # group bridges by nodeId prefix
571 for bridge in state.bridgeNodes.values():
572 nodePrefix = bridge.nodeId.split('/bridge/')[0]
574 if showOrphansOnly and nodePrefix in state.ovsdbNodes:
577 if nodePrefix in resultMap:
578 resultMap[nodePrefix][bridge.alias] = bridge.nodeId
580 resultMap[nodePrefix] = { bridge.alias: bridge.nodeId }
581 resultMapKeys = resultMap.keys()
584 if len(resultMapKeys) == 0:
587 for nodePrefix in resultMapKeys:
588 nodePrefixEntriesKeys = resultMap[nodePrefix].keys()
589 nodePrefixEntriesKeys.sort()
590 # prtLn('Bridges in {}: {}'.format(nodePrefix, nodePrefixEntriesKeys), 0)
591 prtLn('Bridges in {}'.format(nodePrefix), 0)
592 nodeIds = [ resultMap[nodePrefix][nodePrefixEntry] for nodePrefixEntry in nodePrefixEntriesKeys ]
593 showPrettyBridgeNodes(' ', nodeIds)
599 def showPrettyTerminationPoints(indent, tps):
603 tpsDict[ tp.ofPort ] = tp
605 tpDictKeys = tpsDict.keys()
607 for tpKey in tpDictKeys:
609 prt('{}of:{} {}'.format(indent, tp.ofPort, tp.name), 0)
611 prt(' {}:{}'.format('mac', tp.mac), 0)
613 prt(' {}:{}'.format('ifaceId', tp.ifaceId), 0)
619 def dataPathIdToOfFormat(dpId):
620 return 'openflow:' + str( int('0x' + dpId.replace(':',''), 16) )
624 def showPrettyName(name):
625 if not options.useAlias:
628 # handle both openflow:138604958315853:2 and openflow:138604958315853 (aka dpid)
629 # also handle ovsdb://uuid/5c72ec51-1e71-4a04-ab0b-b044fb5f4dc0/bridge/br-int (aka nodeId)
631 nameSplit = name.split(':')
632 ofName = ':'.join(nameSplit[:2])
634 if len(nameSplit) > 2:
635 ofPart = ':' + ':'.join(nameSplit[2:])
637 for bridge in state.bridgeNodes.values():
638 if bridge.getOpenflowName() == ofName or bridge.nodeId == name:
639 return '{}{}'.format(bridge.alias, ofPart)
641 # not found, return paramIn
648 ofLinksKeys = state.ofLinks.keys()
650 ofLinksKeysVisited = set()
652 if len(ofLinksKeys) == 0:
653 # prtLn('no ofLinks found\n', 0)
656 prtLn('ofLinks (discover via lldp):', 0)
657 for ofLinkKey in ofLinksKeys:
658 if ofLinkKey in ofLinksKeysVisited:
660 if state.ofLinks.get( state.ofLinks[ofLinkKey] ) == ofLinkKey:
661 prtLn('{}{} <-> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
662 ofLinksKeysVisited.add(state.ofLinks[ofLinkKey])
664 prtLn('{}{} -> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
665 ofLinksKeysVisited.add(ofLinkKey)
673 parser = optparse.OptionParser(version="0.1")
674 parser.add_option("-d", "--debug", action="count", dest="debug", default=CONST_DEFAULT_DEBUG,
675 help="Verbosity. Can be provided multiple times for more debug.")
676 parser.add_option("-n", "--noalias", action="store_false", dest="useAlias", default=True,
677 help="Do not map nodeId of bridges to an alias")
678 parser.add_option("-i", "--ip", action="store", type="string", dest="odlIp", default="localhost",
679 help="opendaylights ip address")
680 parser.add_option("-t", "--port", action="store", type="string", dest="odlPort", default="8080",
681 help="opendaylights listening tcp port on restconf northbound")
682 parser.add_option("-u", "--user", action="store", type="string", dest="odlUsername", default="admin",
683 help="opendaylight restconf username")
684 parser.add_option("-p", "--password", action="store", type="string", dest="odlPassword", default="admin",
685 help="opendaylight restconf password")
686 parser.add_option("-c", "--config", action="store_true", dest="useConfigTree", default=False,
687 help="parse mdsal restconf config tree instead of operational tree")
688 parser.add_option("-f", "--hide-flows", action="store_false", dest="showFlows", default=True,
691 (options, args) = parser.parse_args(sys.argv)
692 prtLn('argv options:{} args:{}'.format(options, args), 2)
702 grabTopologyJson(getMdsalTreeType())
703 grabInventoryJson(getMdsalTreeType())
704 parseTopologyJson(getMdsalTreeType())
705 parseInventoryJson(getMdsalTreeType())
713 if __name__ == "__main__":