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 # ======================================================================
142 def make_it_a_string(param):
145 result = str( param )
150 # ======================================================================
153 sys.stderr.write(msg)
155 # ======================================================================
157 def prt(msg, logLevel=0):
158 prtCommon(msg, logLevel)
159 def prtLn(msg, logLevel=0):
160 prtCommon('{}\n'.format(msg), logLevel)
161 def prtCommon(msg, logLevel):
162 if options.debug >= logLevel:
163 sys.stdout.write(msg)
165 # ======================================================================
167 def getMdsalTreeType():
168 if options.useConfigTree:
170 return CONST_OPERATIONAL
177 request = urllib2.Request(url)
178 # You need the replace to handle encodestring adding a trailing newline
179 # (https://docs.python.org/2/library/base64.html#base64.encodestring)
180 base64string = base64.encodestring('{}:{}'.format(options.odlUsername, options.odlPassword)).replace('\n', '')
181 request.add_header('Authorization', 'Basic {}'.format(base64string))
182 result = urllib2.urlopen(request)
183 except urllib2.URLError, e:
184 printError('Unable to send request: {}\n'.format(e))
187 if (result.code != 200):
188 printError( '{}\n{}\n\nError: unexpected code: {}\n'.format(result.info(), result.read(), result.code) )
191 data = json.load(result)
197 def grabInventoryJson(mdsalTreeType):
198 global jsonInventoryNodes
200 url = 'http://{}:{}/restconf/{}/opendaylight-inventory:nodes/'.format(options.odlIp, options.odlPort, mdsalTreeType)
203 if not 'nodes' in data:
204 printError( '{}\n\nError: did not find nodes in {}'.format(data, url) )
207 data2 = data['nodes']
208 if not 'node' in data2:
209 printError( '{}\n\nError: did not find node in {}'.format(data2, url) )
212 jsonInventoryNodes = data2['node']
216 def parseInventoryJson(mdsalTreeType):
217 global jsonInventoryNodes
220 for nodeDict in jsonInventoryNodes:
221 if not 'id' in nodeDict:
224 bridgeOfId = nodeDict.get('id')
225 prtLn('inventory node {} has keys {}'.format(bridgeOfId, nodeDict.keys()), 3)
230 for currNodeId in state.bridgeNodes.keys():
231 if state.bridgeNodes[currNodeId].getOpenflowName() == bridgeOfId:
232 bridgeNodeId = currNodeId
233 bridgeNode = state.bridgeNodes[currNodeId]
236 if bridgeNodeId is None:
237 prtLn('inventory node {}'.format(bridgeOfId), 1)
239 prtLn('inventory node {}, aka {}, aka {}'.format(bridgeOfId, bridgeNodeId, showPrettyName(bridgeNodeId)), 1)
244 prtLn('{}features: {}'.format(indent, nodeDict.get('flow-node-inventory:switch-features', {})), 2)
245 prt('{}sw: {}'.format(indent, nodeDict.get('flow-node-inventory:software')), 2)
246 prt('{}hw: {}'.format(indent, nodeDict.get('flow-node-inventory:hardware')), 2)
247 prt('{}manuf: {}'.format(indent, nodeDict.get('flow-node-inventory:manufacturer')), 2)
248 prtLn('{}ip: {}'.format(indent, nodeDict.get('flow-node-inventory:ip-address')), 2)
251 for inventoryEntry in nodeDict.get('flow-node-inventory:table', []):
252 if 'id' in inventoryEntry:
253 currTableId = inventoryEntry.get('id')
254 for currFlow in inventoryEntry.get('flow', []):
255 prtLn('{}table {}: {}'.format(indent, currTableId, currFlow.get('id')), 1)
256 prtLn('{}{}'.format(indent * 2, currFlow), 2)
258 if currTableId in flowInfoNode:
259 flowInfoNode[ currTableId ].append( currFlow.get('id') )
261 flowInfoNode[ currTableId ] = [ currFlow.get('id') ]
265 for currTableId in flowInfoNode.keys():
266 flowInfoNode[currTableId].sort()
268 # store info collected in flowInfoNodes
269 flowInfoNodes[bridgeOfId] = flowInfoNode
273 def grabTopologyJson(mdsalTreeType):
274 global jsonTopologyNodes
276 url = 'http://{}:{}/restconf/{}/network-topology:network-topology/'.format(options.odlIp, options.odlPort, mdsalTreeType)
279 if not CONST_NET_TOPOLOGY in data:
280 printError( '{}\n\nError: did not find {} in data'.format(data, CONST_NET_TOPOLOGY) )
283 data2 = data[CONST_NET_TOPOLOGY]
284 if not CONST_TOPOLOGY in data2:
285 printError( '{}\n\nError: did not find {} in data2'.format(data2, CONST_TOPOLOGY) )
288 jsonTopologyNodes = data2[CONST_TOPOLOGY]
292 def buildDpidCache():
293 global jsonTopologyNodes
294 global nodeIdToDpidCache
296 # only needed if not parsing operational tree
297 if getMdsalTreeType() == CONST_OPERATIONAL:
300 jsonTopologyNodesSave = jsonTopologyNodes
301 grabTopologyJson(CONST_OPERATIONAL)
302 jsonTopologyNodesLocal = jsonTopologyNodes
303 jsonTopologyNodes = jsonTopologyNodesSave
305 for nodeDict in jsonTopologyNodesLocal:
306 if nodeDict.get('topology-id') != 'ovsdb:1':
308 for node in nodeDict.get('node', []):
309 if node.get('node-id') is None or node.get('ovsdb:datapath-id') is None:
311 nodeIdToDpidCache[ node.get('node-id') ] = node.get('ovsdb:datapath-id')
315 def parseTopologyJson(mdsalTreeType):
316 for nodeDict in jsonTopologyNodes:
317 if not 'topology-id' in nodeDict:
319 prtLn('{} {} keys are: {}'.format(mdsalTreeType, nodeDict['topology-id'], nodeDict.keys()), 3)
320 if 'node' in nodeDict:
322 for currNode in nodeDict['node']:
323 parseTopologyJsonNode('', mdsalTreeType, nodeDict['topology-id'], nodeIndex, currNode)
326 if (mdsalTreeType == CONST_OPERATIONAL) and (nodeDict['topology-id'] == 'flow:1') and ('link' in nodeDict):
327 parseTopologyJsonFlowLink(nodeDict['link'])
333 def parseTopologyJsonNode(indent, mdsalTreeType, topologyId, nodeIndex, node):
334 if node.get('node-id') is None:
335 printError( 'Warning: unexpected node: {}\n'.format(node) )
337 prt('{} {} node[{}] {} '.format(indent + mdsalTreeType, topologyId, nodeIndex, node.get('node-id')), 2)
338 if 'ovsdb:bridge-name' in node:
340 parseTopologyJsonNodeBridge(indent + ' ', mdsalTreeType, topologyId, nodeIndex, node)
341 elif 'ovsdb:connection-info' in node:
343 parseTopologyJsonNodeOvsdb(indent + ' ', mdsalTreeType, topologyId, nodeIndex, node)
345 prtLn('keys: {}'.format(node.keys()), 2)
349 def parseTopologyJsonNodeOvsdb(indent, mdsalTreeType, topologyId, nodeIndex, node):
353 prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
355 connectionInfoRaw = node.get('ovsdb:connection-info')
357 if type(connectionInfoRaw) is dict:
358 connectionInfo['inetMgr'] = make_it_a_string(connectionInfoRaw.get('local-ip')) + ':' + make_it_a_string(connectionInfoRaw.get('local-port'))
359 connectionInfo['inetNode'] = make_it_a_string(connectionInfoRaw.get('remote-ip')) + ':' + make_it_a_string(connectionInfoRaw.get('remote-port'))
360 otherConfigsRaw = node.get('ovsdb:openvswitch-other-configs')
362 if type(otherConfigsRaw) is list:
363 for currOtherConfig in otherConfigsRaw:
364 if type(currOtherConfig) is dict and \
365 currOtherConfig.get('other-config-key') == 'local_ip':
366 otherLocalIp = currOtherConfig.get('other-config-value')
369 ovsdbNode = OvsdbNode(node.get('node-id'), connectionInfo.get('inetMgr'), connectionInfo.get('inetNode'), otherLocalIp, node.get('ovsdb:ovs-version'))
370 state.registerOvsdbNode(ovsdbNode)
371 prtLn('Added {}'.format(ovsdbNode), 1)
375 def parseTopologyJsonNodeBridge(indent, mdsalTreeType, topologyId, nodeIndex, node):
377 controllerTarget = None
378 controllerConnected = None
379 controllerEntries = node.get('ovsdb:controller-entry')
380 if type(controllerEntries) is list:
381 for currControllerEntry in controllerEntries:
382 if type(currControllerEntry) is dict:
383 controllerTarget = currControllerEntry.get('target')
384 controllerConnected = currControllerEntry.get('is-connected')
387 nodeId = node.get('node-id')
388 dpId = node.get('ovsdb:datapath-id', nodeIdToDpidCache.get(nodeId))
389 bridgeNode = BridgeNode(nodeId, dpId, node.get('ovsdb:bridge-name'), controllerTarget, controllerConnected)
394 if k == 'termination-point' and len(node[k]) > 0:
397 terminationPoint = parseTopologyJsonNodeBridgeTerminationPoint('%stermination-point[%d] :' % (indent, tpIndex), mdsalTreeType, topologyId, nodeIndex, node, tp)
400 if terminationPoint.ofPort == CONST_TP_OF_INTERNAL and \
401 (terminationPoint.name == 'br-ex' or terminationPoint.name == 'br-int'):
404 bridgeNode.addTerminationPoint(terminationPoint)
408 prtLn('{}{} : {}'.format(indent, k, node[k]), 2)
410 state.registerBridgeNode(bridgeNode)
411 prtLn('Added {}'.format(bridgeNode), 1)
416 def parseTopologyJsonNodeBridgeTerminationPoint(indent, mdsalTreeType, topologyId, nodeIndex, node, tp):
423 if (k == 'ovsdb:port-external-ids' or k == 'ovsdb:interface-external-ids') and len(tp[k]) > 0:
426 prtLn('{} {}[{}] {} : {}'.format(indent, k, extIdIndex, extId.get('external-id-key'), extId.get('external-id-value')), 2)
429 if extId.get('external-id-key') == 'attached-mac':
430 attachedMac = extId.get('external-id-value')
431 if extId.get('external-id-key') == 'iface-id':
432 ifaceId = extId.get('external-id-value')
434 prtLn('{} {} : {}'.format(indent, k, tp[k]), 2)
436 return TerminationPoint(tp.get('ovsdb:name'),
437 tp.get('ovsdb:ofport'),
438 tp.get('ovsdb:interface-type', '').split('-')[-1],
439 attachedMac, ifaceId)
443 def parseTopologyJsonFlowLink(link):
446 for currLinkDict in link:
448 linkId = currLinkDict.get('link-id')
449 linkDest = currLinkDict.get('destination', {}).get('dest-tp')
450 linkSrc = currLinkDict.get('source', {}).get('source-tp')
452 linkDestNode = currLinkDict.get('destination', {}).get('dest-node')
453 linkSrcNode = currLinkDict.get('source', {}).get('source-node')
454 prtLn('{} {} {} => {}:{} -> {}:{}'.format(spc, linkCount, linkId, linkSrcNode, linkSrc.split(':')[-1], linkDestNode, linkDest.split(':')[-1]), 3)
456 if linkId != linkSrc:
457 printError('Warning: ignoring link with unexpected id: %s != %s\n' % (linkId, linkSrc))
460 state.ofLinks[linkSrc] = linkDest
464 def showPrettyNamesMap():
466 if not options.useAlias or len(state.bridgeNodes) == 0:
469 prtLn('aliasMap:', 0)
471 for bridge in state.bridgeNodes.values():
472 resultMap[ bridge.alias ] = '{0: <25} {1: <7} {2}'.format(bridge.getOpenflowName(), bridge.name, bridge.dpId)
474 for resultMapKey in sorted(resultMap):
475 prtLn('{0}{1: <10} -> {2}'.format(spc, resultMapKey, resultMap[resultMapKey]), 0)
480 def showNodesPretty():
481 if len(state.ovsdbNodes) == 0:
482 showBridgeOnlyNodes()
485 aliasDict = { state.ovsdbNodes[nodeId].alias : nodeId for nodeId in state.ovsdbNodes.keys() }
486 aliasDictKeys = aliasDict.keys()
488 for ovsAlias in aliasDictKeys:
489 ovsdbNode = state.ovsdbNodes[ aliasDict[ovsAlias] ]
491 prt('ovsdbNode:{} mgr:{} version:{}'.format(ovsAlias, ovsdbNode.inetMgr, ovsdbNode.ovsVersion), 0)
492 if ovsdbNode.inetNode.split(':')[0] != ovsdbNode.otherLocalIp:
493 prt(' **localIp:{}'.format(ovsdbNode.otherLocalIp), 0)
495 showPrettyBridgeNodes(' ', getNodeBridgeIds(ovsdbNode.nodeId), ovsdbNode)
496 showBridgeOnlyNodes(True)
501 def showFlowInfoPretty():
505 if not options.showFlows:
508 if len(flowInfoNodes) == 0:
509 prtLn('no flowInfo found\n', 0)
512 # translate flowKeys (openflow:123124) into their alias format
513 # then sort it and translate back, so we list them in the order
514 flowInfoNodeKeysDict = {}
515 for flowInfoNodeKey in flowInfoNodes.keys():
516 flowInfoNodeKeysDict[ showPrettyName(flowInfoNodeKey) ] = flowInfoNodeKey
517 flowInfoNodeKeysKeys = flowInfoNodeKeysDict.keys()
518 flowInfoNodeKeysKeys.sort()
520 flowInfoNodesKeys = [ flowInfoNodeKeysDict[ x ] for x in flowInfoNodeKeysKeys ]
522 nodeIdToDpidCacheReverse = {dataPathIdToOfFormat(v): k for k, v in nodeIdToDpidCache.items()}
524 for flowInfoNodeKey in flowInfoNodesKeys:
525 if nodesVisited > 0: prtLn('', 0)
527 nodeName = showPrettyName(flowInfoNodeKey)
528 if nodeName == flowInfoNodeKey:
529 nodeName += ' ( {} )'.format( nodeIdToDpidCacheReverse.get(flowInfoNodeKey, 'node_not_in_topology') )
531 prtLn('{} tree flows at {}'.format(getMdsalTreeType(), nodeName), 0)
532 flowInfoNode = flowInfoNodes[flowInfoNodeKey]
533 flowInfoTables = flowInfoNode.keys()
534 flowInfoTables.sort()
535 for flowInfoTable in flowInfoTables:
536 for rule in flowInfoNode[flowInfoTable]:
537 prtLn('{}table {}: {}'.format(spc, flowInfoTable, rule), 0)
544 def getNodeBridgeIds(nodeIdFilter = None):
546 for bridge in state.bridgeNodes.values():
547 if nodeIdFilter is None or nodeIdFilter in bridge.nodeId:
548 resultMap[ bridge.alias ] = bridge.nodeId
549 resultMapKeys = resultMap.keys()
551 return [ resultMap[x] for x in resultMapKeys ]
555 def showPrettyBridgeNodes(indent, bridgeNodeIds, ovsdbNode = None):
556 if bridgeNodeIds is None:
559 for nodeId in bridgeNodeIds:
560 bridgeNode = state.bridgeNodes[nodeId]
561 prt('{}{}:{}'.format(indent, showPrettyName(nodeId), bridgeNode.name), 0)
563 if ovsdbNode is None or \
564 bridgeNode.controllerTarget is None or \
565 bridgeNode.controllerTarget == '' or \
566 ovsdbNode.inetMgr.split(':')[0] != bridgeNode.controllerTarget.split(':')[-2] or \
567 bridgeNode.controllerConnected != True:
568 prt(' controller:{}'.format(bridgeNode.controllerTarget), 0)
569 prt(' connected:{}'.format(bridgeNode.controllerConnected), 0)
571 showPrettyTerminationPoints(indent + ' ', bridgeNode.tps)
575 def showBridgeOnlyNodes(showOrphansOnly = False):
576 if len(state.bridgeNodes) == 0:
579 # group bridges by nodeId prefix
581 for bridge in state.bridgeNodes.values():
582 nodePrefix = bridge.nodeId.split('/bridge/')[0]
584 if showOrphansOnly and nodePrefix in state.ovsdbNodes:
587 if nodePrefix in resultMap:
588 resultMap[nodePrefix][bridge.alias] = bridge.nodeId
590 resultMap[nodePrefix] = { bridge.alias: bridge.nodeId }
591 resultMapKeys = resultMap.keys()
594 if len(resultMapKeys) == 0:
597 for nodePrefix in resultMapKeys:
598 nodePrefixEntriesKeys = resultMap[nodePrefix].keys()
599 nodePrefixEntriesKeys.sort()
600 # prtLn('Bridges in {}: {}'.format(nodePrefix, nodePrefixEntriesKeys), 0)
601 prtLn('Bridges in {}'.format(nodePrefix), 0)
602 nodeIds = [ resultMap[nodePrefix][nodePrefixEntry] for nodePrefixEntry in nodePrefixEntriesKeys ]
603 showPrettyBridgeNodes(' ', nodeIds)
609 def showPrettyTerminationPoints(indent, tps):
613 tpsDict[ tp.ofPort ] = tp
615 tpDictKeys = tpsDict.keys()
617 for tpKey in tpDictKeys:
619 prt('{}of:{} {}'.format(indent, tp.ofPort, tp.name), 0)
621 prt(' {}:{}'.format('mac', tp.mac), 0)
623 prt(' {}:{}'.format('ifaceId', tp.ifaceId), 0)
629 def dataPathIdToOfFormat(dpId):
630 return 'openflow:' + str( int('0x' + dpId.replace(':',''), 16) )
634 def showPrettyName(name):
635 if not options.useAlias:
638 # handle both openflow:138604958315853:2 and openflow:138604958315853 (aka dpid)
639 # also handle ovsdb://uuid/5c72ec51-1e71-4a04-ab0b-b044fb5f4dc0/bridge/br-int (aka nodeId)
641 nameSplit = name.split(':')
642 ofName = ':'.join(nameSplit[:2])
644 if len(nameSplit) > 2:
645 ofPart = ':' + ':'.join(nameSplit[2:])
647 for bridge in state.bridgeNodes.values():
648 if bridge.getOpenflowName() == ofName or bridge.nodeId == name:
649 return '{}{}'.format(bridge.alias, ofPart)
651 # not found, return paramIn
658 ofLinksKeys = state.ofLinks.keys()
660 ofLinksKeysVisited = set()
662 if len(ofLinksKeys) == 0:
663 # prtLn('no ofLinks found\n', 0)
666 prtLn('ofLinks (discover via lldp):', 0)
667 for ofLinkKey in ofLinksKeys:
668 if ofLinkKey in ofLinksKeysVisited:
670 if state.ofLinks.get( state.ofLinks[ofLinkKey] ) == ofLinkKey:
671 prtLn('{}{} <-> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
672 ofLinksKeysVisited.add(state.ofLinks[ofLinkKey])
674 prtLn('{}{} -> {}'.format(spc, showPrettyName(ofLinkKey), showPrettyName(state.ofLinks[ofLinkKey])), 0)
675 ofLinksKeysVisited.add(ofLinkKey)
683 parser = optparse.OptionParser(version="0.1")
684 parser.add_option("-d", "--debug", action="count", dest="debug", default=CONST_DEFAULT_DEBUG,
685 help="Verbosity. Can be provided multiple times for more debug.")
686 parser.add_option("-n", "--noalias", action="store_false", dest="useAlias", default=True,
687 help="Do not map nodeId of bridges to an alias")
688 parser.add_option("-i", "--ip", action="store", type="string", dest="odlIp", default="localhost",
689 help="opendaylights ip address")
690 parser.add_option("-t", "--port", action="store", type="string", dest="odlPort", default="8080",
691 help="opendaylights listening tcp port on restconf northbound")
692 parser.add_option("-u", "--user", action="store", type="string", dest="odlUsername", default="admin",
693 help="opendaylight restconf username")
694 parser.add_option("-p", "--password", action="store", type="string", dest="odlPassword", default="admin",
695 help="opendaylight restconf password")
696 parser.add_option("-c", "--config", action="store_true", dest="useConfigTree", default=False,
697 help="parse mdsal restconf config tree instead of operational tree")
698 parser.add_option("-f", "--hide-flows", action="store_false", dest="showFlows", default=True,
701 (options, args) = parser.parse_args(sys.argv)
702 prtLn('argv options:{} args:{}'.format(options, args), 2)
712 grabTopologyJson(getMdsalTreeType())
713 grabInventoryJson(getMdsalTreeType())
714 parseTopologyJson(getMdsalTreeType())
715 parseInventoryJson(getMdsalTreeType())
723 if __name__ == "__main__":