Reimplemented TopologyProviders to generate TopologyV3

If this works (it might), we may be able to drop original topology
implementation (which is horrible)
styling
LEdoian 1 year ago
parent b9ae64f643
commit f7f064147c

@ -5,6 +5,8 @@ from enum import Enum
from .topo_v3 import TopologyV3, VertexID, Edge, Vertex, VertexType, MetricType from .topo_v3 import TopologyV3, VertexID, Edge, Vertex, VertexType, MetricType
from ipaddress import IPv4Network, IPv6Network, ip_network from ipaddress import IPv4Network, IPv6Network, ip_network
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from .ospfsock import BirdSocketConnection, BirdError
from subprocess import Popen, PIPE
from . import ospffile from . import ospffile
class TopologyProvider(ABC): class TopologyProvider(ABC):
@ -305,7 +307,7 @@ class OspfFileTopologyParser:
peer_id, cost, mtype, isvirt = details peer_id, cost, mtype, isvirt = details
edge = Edge(source=own_id, target=peer_id, cost=cost, count=count, metric_type=mtype, virtual=isvirt) edge = Edge(source=own_id, target=peer_id, cost=cost, count=count, metric_type=mtype, virtual=isvirt)
result.add_edge(edge) result.add_edge(edge)
for details, count in future_transit_networks: for details, count in future_transit_networks.items():
tgt, cost = details tgt, cost = details
self.future_transit_networks_edges.append((own_id, tgt, cost, count)) self.future_transit_networks_edges.append((own_id, tgt, cost, count))
@ -396,3 +398,131 @@ class OspfFileTopologyParser:
result.add_vertex(vtx) result.add_vertex(vtx)
return 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

Loading…
Cancel
Save