diff --git a/birdvisu/maps_new/providers.py b/birdvisu/maps_new/providers.py index 4a55e65..7bda803 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 @@ -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 index 9873b20..b929f97 100644 --- a/birdvisu/ospfsock.py +++ b/birdvisu/ospfsock.py @@ -1,18 +1,43 @@ """Simple utility to interface with BIRD using the control socket.""" -from dataclasses import dataclass +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], int]] = [] # 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'): @@ -26,14 +51,32 @@ class BirdSocketConnection: # 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…) - MAXLINELENGTH = 1000 # Pretty please… - READSIZE = MAXLINELENGTH * 2 # Ensures that after the read at least one complete line has been read. - lines = [] - data = b'' - # There is no way to read line-wise, so we just read everything and split afterwards. - # This is probably very horrible. - try: - while True: - data += self.socket. - + 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 +