diff --git a/birdvisu/providers.py b/birdvisu/providers.py index 6060185..ec96706 100644 --- a/birdvisu/providers.py +++ b/birdvisu/providers.py @@ -5,6 +5,8 @@ from enum import Enum from .topo_v3 import TopologyV3, VertexID, Edge, Vertex, VertexType, MetricType from ipaddress import IPv4Network, IPv6Network, ip_network from socket import AF_INET, AF_INET6 +from .ospfsock import BirdSocketConnection, BirdError +from subprocess import Popen, PIPE from . import ospffile class TopologyProvider(ABC): @@ -305,7 +307,7 @@ class OspfFileTopologyParser: peer_id, cost, mtype, isvirt = details edge = Edge(source=own_id, target=peer_id, cost=cost, count=count, metric_type=mtype, virtual=isvirt) result.add_edge(edge) - for details, count in future_transit_networks: + for details, count in future_transit_networks.items(): tgt, cost = details self.future_transit_networks_edges.append((own_id, tgt, cost, count)) @@ -396,3 +398,131 @@ class OspfFileTopologyParser: result.add_vertex(vtx) return vtx +class OspfDataTopologyProvider(TopologyProvider): + """Returns a topology from a static data. + + Very basic, but may be used as a tool when the topology is received from a + source for which there is no provider.""" + def __init__(self, data: str, version=None, area=None, freeze=True): + # No need to wait, parse the topology right away + parser = OspfFileTopologyParser(version=version, area=area) + self.topology = parser.parse(data, freeze=freeze) + def get_topology(self): + return self.topology + +class OspfFileTopologyProvider(TopologyProvider): + """Returns a topology from a given file. + + It can cope with the file being changed, we only read the topology in + get_topology.""" + def __init__(self, filename, version=None, area=None): + self.filename = filename + self.area = area + self.version = version + + def get_topology(self): + with open(self.filename, 'r') as f: + data = f.read() + parser = OspfFileTopologyParser(version=self.version, area=self.area) + # No more data in the file, so we can freeze now. + return parser.parse(data, freeze=True) + +class ProcessTopologyProvider(TopologyProvider): + """Retrieves a topology from a process which outputs an ospffile with the + topology. + + The process is invoked in each call of get_topology and is expected to end + sucessfully. + + This is not suitable to retrieve data from BIRD, use + :class:BirdSocketTopologyProvider instead. For example, this can not cope + with multiple instances of OSPF running in BIRD.""" + def __init__(self, command: str | list[str], version=None, area=None, freeze=True): + self.command = command.split() if isinstance(command, str) else command + self.version = version + self.area = area + self.freeze = freeze + + def get_topology(self): + process = Popen(self.command, stdout=PIPE, stderr=PIPE, stdin=PIPE, text=True) + stdout, stderr = process.communicate() + if process.returncode != 0 or stderr != '': + raise RuntimeError(f'The command failed. Return code: {process.returncode}, stderr: {process.stderr}') + # stdout contains the data. + parser = OspfFileTopologyParser(version=self.version, area=self.area) + return parser.parse(stdout, freeze=self.freeze) + +class BirdMultipleInstancesError(BirdError): + """BIRD has multiple known OSPF's running. + + This exception is intended to be created from existing BirdError. (We do + not re-raise, because we know what has happened.)""" + def __init__(self, orig_exc: BirdError, protocols: list[str]): + for attr in ['code', 'start', 'end', 'text']: + val = getattr(orig_exc, attr) + setattr(self, attr, val) + self.protocols = protocols + +class BirdSocketTopologyProvider(TopologyProvider): + """Connects to a running BIRD and retrieves current topology. + + This allows to select instance of OSPF. If there are more instances, we + raise a :class:BirdMultipleInstancesError with list of the possible + instances. + + We do not keep the socket open, so if BIRD restarts or anything similar + happens, we survive. + + Unfortunately, BIRD does not expose the version of the instance, so it is + either guessed or you must provide it.""" + def __init__(self, socket='/run/bird/bird.ctl', all=False, instance=None, + area=None, version=None, freeze=True): + self.socket_filename = socket + self.all = all + self.instance = instance + self.area = area + self.version = version + self.freeze = freeze + + def get_topology(self): + sock = BirdSocketConnection(self.socket_filename) + request = 'show ospf state' + if self.all: + request += ' all' + if self.instance is not None: + request += ' ' + self.instance + try: + response = sock.request(request) + except BirdError as e: + if e.text == 'There are multiple OSPF protocols running': + protocols = self.find_runing_ospf(sock) + raise BirdMultipleInstancesError(e, protocols) + raise + parser = OspfFileTopologyParser(version=self.version, area=self.area) + return parser.parse(response.text, freeze=self.freeze) + + def find_runing_ospf(self, bird): + resp = bird.request('show protocols') + # There are three sections: the header, the table itself and a 0000 at + # the end. + assert len(resp.codes) == 3 + assert resp.codes[0] == ((1,1), '2002') + assert resp.codes[1][1] == '1002' + lines = resp.splitlines() + header = lines[0] + fields = header.split() + assert fields[0] == 'Name' + assert fields[1] == 'Proto' + assert fields[3] == 'State' + # The output looks familiar, so we parse it. + + start, end = resp.codes[1][0] + # The numbers are line numbers, so we need to subtract one from start + # to get index. End points after the end, that is correct. + table = lines[start-1:end] + result = [] + for line in table: + name, proto, _table, state, *_rest = line.split() + if proto == 'OSPF' and state == 'up': + result.append(name) + return result