|
|
|
@ -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
|
|
|
|
|