Add TopologyProviders for ospffile format
parent
c13192316b
commit
e60413bdf1
@ -0,0 +1,126 @@
|
|||||||
|
"""Common TopologyProviders"""
|
||||||
|
|
||||||
|
from birdvisu import ospffile
|
||||||
|
from birdvisu.maps_new import TopologyProvider, Router, Network, Link, Topology
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
class OspfDataTopologyProvider(TopologyProvider):
|
||||||
|
"""Common provider for parsing BIRD's OSPF data.
|
||||||
|
|
||||||
|
At the moment it can only process static data."""
|
||||||
|
def __init__(self, data: str):
|
||||||
|
parsed = ospffile.loads(data)
|
||||||
|
self.topology = Topology()
|
||||||
|
# Networks will need changed names, since 'network' directives do not
|
||||||
|
# feel trustworthy (I don't really understand them, but I think they
|
||||||
|
# depend on designated routers and interface numbers, changes of which
|
||||||
|
# should not cause us to think that is a different network)
|
||||||
|
self.network_renames = {}
|
||||||
|
# We only care for areas:
|
||||||
|
# TODO: Should we support global visualisation configuration?
|
||||||
|
for directive, details in parsed:
|
||||||
|
if directive.startswith('area'):
|
||||||
|
self.add_area((directive, details))
|
||||||
|
def add_area(self, ospf_area):
|
||||||
|
_area, topo = ospf_area # Ignoring the area
|
||||||
|
for obj in topo:
|
||||||
|
directive, details = obj
|
||||||
|
if directive.startswith('router'):
|
||||||
|
self.add_router(obj)
|
||||||
|
elif directive.startswith('network'):
|
||||||
|
self.add_network(obj)
|
||||||
|
self.find_links()
|
||||||
|
# assert self.topology.is_valid()
|
||||||
|
|
||||||
|
def add_router(self, tree):
|
||||||
|
ident, details = tree
|
||||||
|
assert ident not in self.topology.routers
|
||||||
|
self.topology.routers[ident] = Router(
|
||||||
|
ident = ident,
|
||||||
|
links = [], # Will be filled by find_links
|
||||||
|
details = tree,
|
||||||
|
)
|
||||||
|
# Stubnets and external routes are only mentioned here, so add them now
|
||||||
|
for det in details:
|
||||||
|
n, _nd = det
|
||||||
|
if n.startswith(('stubnet', 'external')):
|
||||||
|
net_id = re.match(r'((stubnet|external) [^ ]+)', n).group(1)
|
||||||
|
if net_id not in self.topology.networks:
|
||||||
|
self.topology.networks[net_id] = Network(
|
||||||
|
ident = net_id,
|
||||||
|
links = [],
|
||||||
|
details = det,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_network(self, tree):
|
||||||
|
bird_name, details = tree
|
||||||
|
assert bird_name not in self.network_renames
|
||||||
|
details.append((bird_name, [])) # Keep old name for reference
|
||||||
|
ident_candidates = []
|
||||||
|
for d, dd in details:
|
||||||
|
if d.startswith('address'):
|
||||||
|
ident_candidates.append(d.replace('address', 'network'))
|
||||||
|
assert len(ident_candidates) > 0, "Network without address?"
|
||||||
|
ident = sorted(ident_candidates)[0]
|
||||||
|
self.network_renames[bird_name] = ident
|
||||||
|
assert ident not in self.topology.networks, "Multiple networks with same address?"
|
||||||
|
self.topology.networks[ident] = Network(
|
||||||
|
ident = ident,
|
||||||
|
links = [],
|
||||||
|
details = tree,
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_links(self):
|
||||||
|
for r in self.topology.routers.values():
|
||||||
|
det = r.details[1]
|
||||||
|
for n, nd in det:
|
||||||
|
if n.startswith(('network', 'stubnet', 'external')):
|
||||||
|
net_id = re.match(r'((network|stubnet|external) [^ ]+)', n).group(1)
|
||||||
|
if net_id in self.network_renames:
|
||||||
|
net_id = self.network_renames[net_id]
|
||||||
|
metric = int(re.search(r'metric ([0-9]+)', n).group(1))
|
||||||
|
ident = (r.ident, net_id)
|
||||||
|
# I really hope that one router has at most one link to each network (incl. external)
|
||||||
|
assert ident not in self.topology.links
|
||||||
|
link = Link(
|
||||||
|
router = self.topology.routers[r.ident],
|
||||||
|
network = self.topology.networks[net_id],
|
||||||
|
metric = metric,
|
||||||
|
)
|
||||||
|
self.topology.links[ident] = link
|
||||||
|
link.router.links.append(link)
|
||||||
|
link.network.links.append(link)
|
||||||
|
|
||||||
|
def get_topology(self):
|
||||||
|
return self.topology
|
||||||
|
|
||||||
|
class OspfFileTopologyProvider(TopologyProvider):
|
||||||
|
def __init__(self, ospf_file):
|
||||||
|
ospf_data = ospf_file.read()
|
||||||
|
self.ospfdataprovider = OspfDataTopologyProvider(ospf_data)
|
||||||
|
def get_topology(self):
|
||||||
|
return self.ospfdataprovider.get_topology()
|
||||||
|
|
||||||
|
|
||||||
|
class RunningBirdTopologyProvider(TopologyProvider):
|
||||||
|
def __init__(self):
|
||||||
|
self.load_topology()
|
||||||
|
|
||||||
|
def load_topology(self):
|
||||||
|
bird = subprocess.Popen(['birdcl', 'show', 'ospf', 'state'],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
# Probably not needed, but will work and is tested from interactive python shell
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
stdout, stderr = bird.communicate()
|
||||||
|
if bird.returncode != 0 or stderr != '':
|
||||||
|
raise RuntimeError(f'BIRD failed: returncode={bird.returncode}, stderr={stderr}')
|
||||||
|
# FIXME: this probably causes memory leaks, since TopologyCombiner will
|
||||||
|
# keep complete history from all runs that have been processed. This
|
||||||
|
# needs to be solved at TopologyCombiner level or above, since we need
|
||||||
|
# to be able to remove old data from the topology.
|
||||||
|
self.topology = OspfDataTopologyProvider(stdout).get_topology()
|
||||||
|
|
||||||
|
def get_topology(self):
|
||||||
|
return self.topology
|
Loading…
Reference in New Issue