Build complete spanning tree for data links and use it to orient tree

Rather than obeying the "parent" field, which leads to wrong-way up
graphs when building from a non-root site - instead build a complete
cost-based spanning tree from the specified root node. Then use
the generated tree - falling back on parent if nothing is found -
to populate the tree in order.

Tested on Herbert's network, correctly generates topology from several
different locations.

Still to come: adding a mechanism for providing parenting overloads
to allow for cases where a long path is actually optimal, but we
have no way of knowing that.
This commit is contained in:
Herbert Wolverson 2023-04-06 16:22:01 +00:00
parent d2aa804ace
commit 2d3874e812

View File

@ -76,19 +76,36 @@ def buildFlatGraph():
net.createNetworkJson() net.createNetworkJson()
net.createShapedDevices() net.createShapedDevices()
def buildFullGraph(): def linkSiteTarget(link, direction):
# Attempts to build a full network graph, incorporating as much of the UISP # Helper function to extract the site ID from a data link. Returns
# hierarchy as possible. # None if not present.
from integrationCommon import NetworkGraph, NetworkNode, NodeType if link[direction]['site'] is not None:
from ispConfig import generatedPNUploadMbps, generatedPNDownloadMbps return link[direction]['site']['identification']['id']
# Load network sites return None
print("Loading Data from UISP")
sites = uispRequest("sites")
devices = uispRequest("devices?withInterfaces=true&authorized=true")
dataLinks = uispRequest("data-links?siteLinksOnly=true")
# Do we already have a integrationUISPbandwidths.csv file? def findSiteLinks(dataLinks, siteId):
# Searches the Data Links for any links to/from the specified site.
# Returns a list of site IDs that are linked to the specified site.
links = []
for dl in dataLinks:
fromSiteId = linkSiteTarget(dl, "from")
if fromSiteId is not None and fromSiteId == siteId:
# We have a link originating in this site.
target = linkSiteTarget(dl, "to")
if target is not None:
links.append(target)
toSiteId = linkSiteTarget(dl, "to")
if toSiteId is not None and toSiteId == siteId:
# We have a link originating in this site.
target = linkSiteTarget(dl, "from")
if target is not None:
links.append(target)
return links
def buildSiteBandwidths():
# Builds a dictionary of site bandwidths from the integrationUISPbandwidths.csv file.
siteBandwidth = {} siteBandwidth = {}
if os.path.isfile("integrationUISPbandwidths.csv"): if os.path.isfile("integrationUISPbandwidths.csv"):
with open('integrationUISPbandwidths.csv') as csv_file: with open('integrationUISPbandwidths.csv') as csv_file:
@ -99,8 +116,10 @@ def buildFullGraph():
download = int(download) download = int(download)
upload = int(upload) upload = int(upload)
siteBandwidth[name] = {"download": download, "upload": upload} siteBandwidth[name] = {"download": download, "upload": upload}
return siteBandwidth
# Find AP capacities from UISP def findApCapacities(devices, siteBandwidth):
# Searches the UISP devices for APs and adds their capacities to the siteBandwidth dictionary.
for device in devices: for device in devices:
if device['identification']['role'] == "ap": if device['identification']['role'] == "ap":
name = device['identification']['name'] name = device['identification']['name']
@ -111,7 +130,7 @@ def buildFullGraph():
siteBandwidth[device['identification']['name']] = { siteBandwidth[device['identification']['name']] = {
"download": download, "upload": upload} "download": download, "upload": upload}
# Find Site Capacities by AirFiber capacities def findAirfibers(devices, generatedPNDownloadMbps, generatedPNUploadMbps):
foundAirFibersBySite = {} foundAirFibersBySite = {}
for device in devices: for device in devices:
if device['identification']['site']['type'] == 'site': if device['identification']['site']['type'] == 'site':
@ -134,6 +153,99 @@ def buildFullGraph():
foundAirFibersBySite[device['identification']['site']['id']]['upload'] = upload foundAirFibersBySite[device['identification']['site']['id']]['upload'] = upload
else: else:
foundAirFibersBySite[device['identification']['site']['id']] = {'download': download, 'upload': upload} foundAirFibersBySite[device['identification']['site']['id']] = {'download': download, 'upload': upload}
return foundAirFibersBySite
def buildSiteList(sites, dataLinks):
# Builds a list of sites, including their IDs, names, and connections.
# Connections are determined by the dataLinks list.
siteList = []
for site in sites:
newSite = {
'id': site['identification']['id'],
'name': site['identification']['name'],
'connections': findSiteLinks(dataLinks, site['identification']['id']),
'cost': 10000,
'parent': "",
'type': type,
}
siteList.append(newSite)
return siteList
def findInSiteList(siteList, name):
# Searches the siteList for a site with the specified name.
for site in siteList:
if site['name'] == name:
return site
return None
def findInSiteListById(siteList, id):
# Searches the siteList for a site with the specified name.
for site in siteList:
if site['id'] == id:
return site
return None
def debugSpaces(n):
# Helper function to print n spaces.
spaces = ""
for i in range(int(n)):
spaces = spaces + " "
return spaces
def walkGraphOutwards(siteList, root):
def walkGraph(node, parent, cost, backPath):
site = findInSiteListById(siteList, node)
if cost < site['cost']:
# It's cheaper to get here this way, so update the cost and parent.
site['cost'] = cost
site['parent'] = parent['id']
#print(debugSpaces(cost/10) + parent['name'] + "->" + site['name'] + " -> New cost: " + str(cost))
for connection in site['connections']:
if not connection in backPath:
#target = findInSiteListById(siteList, connection)
#print(debugSpaces((cost+10)/10) + site['name'] + " -> " + target['name'] + " (" + str(target['cost']) + ")")
newBackPath = backPath.copy()
newBackPath.append(site['id'])
walkGraph(connection, site, cost+10, newBackPath)
for connection in root['connections']:
# Force the parent since we're at the top
site = findInSiteListById(siteList, connection)
site['parent'] = root['id']
walkGraph(connection, root, 20, [root['id']])
def buildFullGraph():
# Attempts to build a full network graph, incorporating as much of the UISP
# hierarchy as possible.
from integrationCommon import NetworkGraph, NetworkNode, NodeType
from ispConfig import generatedPNUploadMbps, generatedPNDownloadMbps
# Load network sites
print("Loading Data from UISP")
sites = uispRequest("sites")
devices = uispRequest("devices?withInterfaces=true&authorized=true")
dataLinks = uispRequest("data-links?siteLinksOnly=true")
# Build Site Capacities
siteBandwidth = buildSiteBandwidths()
findApCapacities(devices, siteBandwidth)
foundAirFibersBySite = findAirfibers(devices, generatedPNDownloadMbps, generatedPNUploadMbps)
# Create a list of just network sites
siteList = buildSiteList(sites, dataLinks)
rootSite = findInSiteList(siteList, uispSite)
if rootSite is None:
print("ERROR: Unable to find root site in UISP")
return
walkGraphOutwards(siteList, rootSite)
# Debug code: dump the list of site parents
# for s in siteList:
# if s['parent'] == "":
# p = "None"
# else:
# p = findInSiteListById(siteList, s['parent'])['name']
# print(s['name'] + " (" + str(s['cost']) + ") <-- " + p)
print("Building Topology") print("Building Topology")
net = NetworkGraph() net = NetworkGraph()
@ -146,10 +258,12 @@ def buildFullGraph():
upload = generatedPNUploadMbps upload = generatedPNUploadMbps
address = "" address = ""
customerName = "" customerName = ""
if site['identification']['parent'] is None: parent = findInSiteListById(siteList, id)['parent']
parent = "" if parent == "":
else: if site['identification']['parent'] is None:
parent = site['identification']['parent']['id'] parent = ""
else:
parent = site['identification']['parent']['id']
match type: match type:
case "site": case "site":
nodeType = NodeType.site nodeType = NodeType.site