Remove dead code

topo-mov
LEdoian 1 year ago
parent db8b1414f0
commit 30ac0231e4

@ -1,87 +0,0 @@
from birdvisu.ospffile import load
import re
class Topology:
"""Basically a graph"""
def __init__(self):
self.routers = [] # Tuples like in ospffile for router section
self.networks = [] # Ditto, but the key is the address, not the ad-hoc string from bird
self.network_addrs = {} # Keeps mapping from network to address
self.links = [] # Tuples (router, network_addr, cost)
# FIXME: we are ugly, so everything are strings and I don't care as of now.
def add_area(self, dirtuple):
# FIXME: Do not use BIRD's strings, create an abstraction above them.
_area, topo = dirtuple # Ignoring the area
for obj in topo:
directive, details = obj
if directive.startswith('router'):
self.routers.append(obj)
elif directive.startswith('network'):
net_addr = None
for d, _ in details:
if d.startswith('address'):
net_addr = d
break
assert net_addr is not None
fixed_network = (net_addr, details + [directive])
self.networks.append(fixed_network)
self.network_addrs[directive] = net_addr
# Fix the topology: find other networks (stubnets, external, ...) and links:
known = set()
for r, rd in self.routers:
for n, nd in rd:
if n.startswith(('network', 'stubnet', 'external')):
net_id = re.match(r'((network|stubnet|external) [^ ]+)', n).group(1)
if n.startswith(('stubnet', 'external')) and net_id not in known:
known.add(net_id)
self.networks.append((net_id, []))
# Add dummy mapping
self.network_addrs[net_id] = net_id
metric = int(re.search(r'metric ([0-9]+)', n).group(1))
net_addr = self.network_addrs[net_id]
self.links.append((r, net_addr, metric))
@classmethod
def from_ospffile(cls, f):
# FIXME: We should create own classes from the OSPF file as soon as
# possible to avoid changing whole codebase when BIRD's format changes.
result = cls()
parsed = load(f)
for directive, details in parsed:
if directive.startswith('area'):
result.add_area((directive, details))
return result
class TopologyDifference:
def __init__(self, actual, reference):
self.actual = actual
self.reference = reference
def compare(self):
# FIXME: This also relies on BIRD's current format.
just_ident = lambda x: x[0]
act_routers = set(map(just_ident, self.actual.routers))
ref_routers = set(map(just_ident, self.reference.routers))
act_networks = set(map(just_ident, self.actual.networks))
ref_networks = set(map(just_ident, self.reference.networks))
act_links = set(self.actual.links)
ref_links = set(self.reference.links)
# *_missing: in reference, not in actual network.
# *_extra: in actual network, not in reference.
# *_discrepancies: different settings (currently only metrics)
self.routers_missing = ref_routers - act_routers
self.routers_extra = act_routers - ref_routers
self.networks_missing = ref_networks - act_networks
self.networks_extra = act_networks - ref_networks
# FIXME: be more clever. Discrepancy is not missing and extra…
self.links_missing = ref_links - act_links
self.links_extra = act_links - ref_links
self.links_discrepancies = act_links ^ ref_links

@ -1,199 +1,3 @@
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
from typing import Sequence, Callable
# The topologies provided by TopologyProviders are independent and do not have
# common routers, networks and links. We therefore join them on identifiers,
# which we expect to be canonical, somehow (i.e. the names BIRD has used)
@dataclass
class Router:
ident: str
links: list['Link']
# The full details in ospffile format. This allows extracting original
# details (e.g. visualisation details) from the router objects.
# FIXME: Topology handling does not need this, ideally this would be in
# some separate part of code. But everything else takes data from a
# CombinedTopology object, so we would still need a way of resolving this,
# and adding a separate object would complicate the code even more.
details: tuple[str,list] | None = None
# source objects for this Router in case of combined topology. Key is some
# source identifier
# FIXME: This allows for different identifiers in sources, which should be
# forbidden
# XXX: This is here, because Python mandates so: "TypeError: non-default
# argument 'links' follows default argument". I would rather have it right
# after `.ident`, but to no avail.
sources: dict[str, 'Router'] = field(default_factory=dict)
@property
def all_details(self):
if self.details is not None:
yield self.details
for src in self.sources.values():
for x in src.all_details: yield x
@dataclass
class Network:
ident: str
links: list['Link']
details: tuple[str,list] | None = None
sources: dict[str, 'Network'] = field(default_factory=dict)
@property
def all_details(self):
if self.details is not None:
yield self.details
for src in self.sources.values():
for x in src.all_details: yield x
@dataclass
class Link:
"""A connection from a Router to Network"""
router: Router
network: Network
metric: int
sources: dict[str, 'Link'] = field(default_factory=dict)
# Are there any other details that help describe a link?
# This could be just a (named) tuple, dataclass is only used for consistency.
@dataclass
class Topology:
# Keys: idents
routers: dict[str, Router] = field(default_factory=dict)
networks: dict[str, Network] = field(default_factory=dict)
links: dict[tuple[str,str], Link] = field(default_factory=dict) # (router.ident, network.ident) -> Link
# TODO: function for validating the topology, namely:
# - Routers and networks all know their links
# - Objects from this topology are referenced, not their sources nor other objects
# - Keys in dictionaries are same as identifiers of the values
# - maybe sth else…
class TopologyProvider(ABC):
"""Allows obtaining topology description from somewhere (usually OSPF
files)"""
@abstractmethod
def get_topology(self) -> Topology: ...
class TopologyCombiner:
"""Takes multiple topologies (e.g. reference and actual one) and creates a
super-topology out of that.
The rest of code relies mainly on such super-topologies"""
def __init__(self):
self.topology = Topology()
def add_topology(self, source_name: str, topo: Topology) -> None:
# We handle Routers and Networks pretty much the same way
for which_dict, which_class in [
('routers', Router),
('networks', Network),
]:
own_dict = getattr(self.topology, which_dict)
their_dict = getattr(topo, which_dict)
for nid, n in their_dict.items():
assert nid == n.ident
if nid not in own_dict:
# FIXME: should only add complete objects. This will require
# different approach.
own_dict[nid] = which_class(
ident = nid,
links = [],
)
assert source_name not in own_dict[nid].sources
own_dict[nid].sources[source_name] = n
# TODO: Can we avoid code duplication?
for lkey, l in topo.links.items():
assert lkey == (l.router.ident, l.network.ident)
if lkey not in self.topology.links:
new_link = Link(
# Use own objects, not their children
router = self.topology.routers[l.router.ident],
network = self.topology.networks[l.network.ident],
metric = l.metric,
)
self.topology.links[lkey] = new_link
# We add the link to our nodes
# TODO: This should ensure that only one link exists between
# this Router and Network, but currently it is based mostly on
# belief.
self.topology.routers[l.router.ident].links.append(new_link)
self.topology.networks[l.network.ident].links.append(new_link)
own_link = self.topology.links[lkey]
assert source_name not in own_link.sources
own_link.sources[source_name] = l
if own_link.metric != l.metric:
own_link.metric = -1 # Discrepant metrics!
def get_complete_topology(self) -> Topology:
return self.topology
@dataclass
class AnnotatedTopology:
topology: Topology
# Router.ident -> some tags. What they are is up to users /
# annotators. Similarly for other fields.
router_annotations: dict[str, list[object]]
network_annotations: dict[str, list[object]]
link_annotations: dict[tuple[str, str], list[object]]
# FIXME: this should rather be immutable, to allow saving different annotations.
# TODO: maybe class to handle sets of annotators?
def annotate_topology(topology,
annotators: Sequence[Callable[[AnnotatedTopology], AnnotatedTopology]],
initial_annotation: AnnotatedTopology | None = None,
) -> AnnotatedTopology:
"""Runs all the annotators and assigns all the tags.
The tags will change how the topology will be visualized. Examples:
- Which parts of network were expected and are missing
- Which paths are used in a routing tree of particular router
- Where should be nodes placed
The annotators are run in order, therefore later annotators may depend on
pre-existing tags. This allows pipelining, for example to first analyse the
networks, then set styles, then layout the graph. An AnnotatedTopology
should therefore contain everything the visualisation (or other user)
needs.
Initial topology contains all the dictionaries with all keys and empty lists.
Annotators are not allowed to remove tags (by policy, code unfortunately
currently enables that.)."""
# This is maybe too general…
# The use of lists leaves namespacing to users, but can hinder performance
# in case of many tags.
# Removal of annotators is problematic, since tags are not tied to them.
# Unless users can distinguish tag ownership somehow (i.e. using own types
# for the tags), the only option is to recalculate the unannotated topology
# with a different set of annotators.
# TODO: We are going to abuse the fact that the annotators run in order and
# the tags are in lists to avoid searching through the whole list of tags
# when we depend on previous annotator, its output is at the end. This
# should be more explicit (maybe an AnnotationPipeline object?), since this
# can easily be broken by misordering the annotators.
# - Idea: allow using `annotate_topology` itself as annotator. This way, a
# single call can hold the pipeline segment and can be managed
# separately (and possibly in more readable manner)
if initial_annotation is None:
initial_annotation = AnnotatedTopology(
topology = topology,
router_annotations = {k: [] for k in topology.routers.keys()},
network_annotations = {k: [] for k in topology.networks.keys()},
link_annotations = {k: [] for k in topology.links.keys()},
)
assert initial_annotation.topology is topology
# FIXME: probably shouldn't rely on topology having correct keys.
assert all(k in initial_annotation.router_annotations for k in topology.routers.keys())
assert all(k in initial_annotation.network_annotations for k in topology.networks.keys())
assert all(k in initial_annotation.link_annotations for k in topology.links.keys())
for annotator in annotators:
new_annotation = annotator(initial_annotation)
assert isinstance(new_annotation, AnnotatedTopology)
initial_annotation = new_annotation
return initial_annotation
# - Which parts of network were expected and are missing
# - Which paths are used in a routing tree of particular router
# - Where should be nodes placed

@ -1,139 +0,0 @@
"""Common TopologyProviders"""
from birdvisu import ospffile, ospfsock
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 : dict[str,str] = {}
# 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]
# FIXME: Hacking metric and metric2 together is a bad idea.
metric = int(re.search(r'metric2? ([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
class BirdSocketTopologyProvider(TopologyProvider):
"""This one is dynamic, provides new topology every time."""
def __init__(self, socket_filename='/run/bird/bird.ctl'):
self.socket_filename = socket_filename
def get_topology(self):
birdsock = ospfsock.BirdSocketConnection()
resp = birdsock.request('show ospf state')
# TODO: Naming things this is not exactly parser…
parser = OspfDataTopologyProvider(resp.text)
return parser.get_topology()
Loading…
Cancel
Save