diff --git a/.gitignore b/.gitignore index f2b3193..d3ab421 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ __pycache__/ /env/ current.ospf + +/venv/ diff --git a/birdvisu/maps_new/__init__.py b/birdvisu/maps_new/__init__.py index 602b4be..3b82517 100644 --- a/birdvisu/maps_new/__init__.py +++ b/birdvisu/maps_new/__init__.py @@ -144,7 +144,7 @@ class AnnotatedTopology: # TODO: maybe class to handle sets of annotators? def annotate_topology(topology, annotators: Sequence[Callable[[AnnotatedTopology], AnnotatedTopology]], - initial_annotation: AnnotatedTopology = None, + initial_annotation: AnnotatedTopology | None = None, ) -> AnnotatedTopology: """Runs all the annotators and assigns all the tags. diff --git a/birdvisu/maps_new/providers.py b/birdvisu/maps_new/providers.py index 4a55e65..41e3e32 100644 --- a/birdvisu/maps_new/providers.py +++ b/birdvisu/maps_new/providers.py @@ -1,6 +1,6 @@ """Common TopologyProviders""" -from birdvisu import ospffile +from birdvisu import ospffile, ospfsock from birdvisu.maps_new import TopologyProvider, Router, Network, Link, Topology import subprocess import re @@ -16,7 +16,7 @@ class OspfDataTopologyProvider(TopologyProvider): # 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 = {} + self.network_renames : dict[str,str] = {} # We only care for areas: # TODO: Should we support global visualisation configuration? for directive, details in parsed: @@ -125,3 +125,15 @@ class RunningBirdTopologyProvider(TopologyProvider): 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() diff --git a/birdvisu/ospfsock.py b/birdvisu/ospfsock.py new file mode 100644 index 0000000..b034d06 --- /dev/null +++ b/birdvisu/ospfsock.py @@ -0,0 +1,82 @@ +"""Simple utility to interface with BIRD using the control socket.""" + +from dataclasses import dataclass,field +import socket as sk +from string import digits + +class BirdError(Exception): + def __init__(self, code, where, text): + self.code = code + self.start = where[0] + self.end = where[1] + self.text = text + +@dataclass +class BirdResponse: + text: str = '' + codes: list[tuple[tuple[int, int], str]] = field(default_factory=list) # List of line ranges (inclusive) and the respective code. + + def raise_exceptions(self) -> None: + # If the response contains any errors, raise them as BirdErrors + # TODO Py3.11: use ExceptionGroups? + # Until then, let's just report the first one? + assert len(self.codes) >= 1, "Not a valid response." + for where, code in self.codes: + if code.startswith('9'): + # Extract text + start = where[0] - 1 + end = where[1] + print(start,end) + errorlines = self.text.splitlines(keepends=True)[start:end] + exc = BirdError(code, where, ''.join(errorlines)) + raise exc + +class BirdSocketConnection: + def __init__(self, sockpath='/run/bird/bird.ctl'): + self.socket = sk.socket(sk.AF_UNIX, sk.SOCK_STREAM) + self.socket.setblocking(False) + self.socket.connect(sockpath) + # Bird announces itself by printing version. We save the response just in case. + self.bird_prologue = self._parse_response() + + def request(self, req: str) -> BirdResponse: + if not req.endswith('\n'): + req = req + '\n' + binreq = req.encode() + self.socket.send(binreq) + return self._parse_response() + + def _parse_response(self) -> BirdResponse: + # We even read from the socket here in order to have the whole protocol + # described at one place. Therefore other parts of the code do not need + # to know anything about the protocol (maybe apart from the meaning of + # status codes, if they are interested…) + result = BirdResponse() + code : str | None = None # string because of leading zeroes, and it has no numeric interpretation anyway + start = None + cont = True + f = self.socket.makefile('r') + for i, line in enumerate(f, start=1): + assert cont, "WTF bad format (should not continue)." + if line.startswith(tuple(digits)): + if start is not None: + end = i-1 + result.codes.append(((start, end), code)) + code = line[:4] + start = i + cont = line[4] == '-' + text = line[5:] + else: + assert cont == True + text = line[1:] + result.text += text # Invariant: LF as line terminator + if not cont: + # Finalization. We do not have i after the end of the cycle, + # but this is the last iteration. + end = i + result.codes.append(((start, end), code)) + break + result.raise_exceptions() + return result + + diff --git a/poor_mans_visualisation.py b/poor_mans_visualisation.py index c5747fc..7a6d2db 100755 --- a/poor_mans_visualisation.py +++ b/poor_mans_visualisation.py @@ -14,7 +14,7 @@ with open(ref_topo_file) as ref_file: ref_topo = providers.OspfFileTopologyProvider(ref_file).get_topology() try: - cur_topo = providers.RunningBirdTopologyProvider().get_topology() + cur_topo = providers.BirdSocketTopologyProvider().get_topology() except OSError: # HACK! import traceback as tb @@ -95,6 +95,16 @@ for tagsrc in [ scene.addItem(taglist[-1]) view = QtWidgets.QGraphicsView(scene) -view.show() +#view.show() + + +main_window = QtWidgets.QMainWindow() +main_window.setCentralWidget(view) + +menu = main_window.menuBar().addMenu('Hello') +act = QtGui.QAction('Hi') +#act.setToolTip('Howdy') +menu.addAction(act) +main_window.show() app.exec()