You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
530 lines
22 KiB
Python
530 lines
22 KiB
Python
from abc import ABC, abstractmethod
|
|
from collections import defaultdict
|
|
from typing import Final
|
|
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):
|
|
"""Allows obtaining topology description from somewhere
|
|
|
|
We only require a function to get a topology. However, the topology
|
|
retrieval may require further specification, e.g. for which area the
|
|
topology should be returned. Handling of this is an implementation detail
|
|
of the specific provider."""
|
|
|
|
@abstractmethod
|
|
def get_topology(self) -> TopologyV3: ...
|
|
|
|
class OspfFileParseError(Exception): pass
|
|
|
|
class OspfFileTopologyParser:
|
|
"""This class parses an ospffile-formated data into a Topology. However,
|
|
this is not a full-fledged provider, because it does not deal with
|
|
retrieval of the file itself.
|
|
|
|
Since the ospf dump is intended for a knowledgeable human, there are a few
|
|
caveats with parsing it:
|
|
- The dump may describe more than one area
|
|
- It is not clear whether we are dealing with OSPFv2 or OSPFv3.
|
|
- The precise format is not documented. We tried simulating various
|
|
network conditions to see as many different examples as possible and
|
|
looked briefly into ``proto/ospf/ospf.c`` in BIRD's source code, but
|
|
still are not sure.
|
|
|
|
Therefore either the initialiser is told, which version we are dealing
|
|
with, or we try to guess, based on network directive. That seems to be the
|
|
only distinguishing part."""
|
|
|
|
# We aim to have most of the code common, so only a few functions are
|
|
# specialised for each OSPF version.
|
|
|
|
# Convention: when we pass the resulting topology as parameter, it is
|
|
# always the first parameter after self.
|
|
|
|
def __init__(self, version=None, area=None):
|
|
if version not in [2,3,None]: raise ValueError("Unknown OSPF version")
|
|
if isinstance(area, int) or area is None:
|
|
self.area = area
|
|
else:
|
|
self.area = self.parse_fake_ip(area)
|
|
self.version = version
|
|
self.guessing = version is None
|
|
|
|
@staticmethod
|
|
def parse_fake_ip(s:str):
|
|
# Only import here, so that if I accidentally write IPv4Address
|
|
# elsewhere, it fails.
|
|
from ipaddress import IPv4Address
|
|
return int(IPv4Address(s))
|
|
|
|
def parse(self, data: str, freeze=True) -> TopologyV3:
|
|
"""Parses the ospffile (duh!)
|
|
|
|
Expected format::
|
|
BIRD whatever greeting ...
|
|
area ...
|
|
router ...
|
|
neigh ...
|
|
...
|
|
...
|
|
network ...
|
|
address ...
|
|
...
|
|
...
|
|
|
|
We consider layers of the tree: the layer with ``area`` is the top
|
|
level, the one with ``router``s and ``network``s is the level-2, and
|
|
the specifics are level-3. The hopefully nice thing about ospffile is
|
|
that the directed edges only go from l2 to l3 directives.
|
|
|
|
Unfortunately, when adding an edge, we must make sure that a correct
|
|
vertex is referenced. That is not a problem for anything external
|
|
(external networks, stubnets, xrouters, xnetworks, …) nor for routers,
|
|
since for all of this a complete :class:VertexID can be created.
|
|
However, transit networks in OSPFv2 may be distinguished by the DR,
|
|
which is not contained in l3 directives. In that case, we postpone
|
|
adding the edge and only add it when all network vertices are present
|
|
in the topology, so that we can take the DR RID from that.
|
|
|
|
Even more unfortunate consequence of that is that the ospffile does
|
|
*not* contain enough information for us to pick the correct network
|
|
vertex in case of network split (since we only know IP range which is
|
|
shared). While the topology will be inaccurate, we can still detect
|
|
that a split has occured, so the user can be made aware of this
|
|
problem. (Also, the forwarding itself is as confused as us in this
|
|
case.)
|
|
|
|
:param freeze: Whether to freeze the resulting topology."""
|
|
syntree = ospffile.loads(data)
|
|
result = TopologyV3()
|
|
|
|
# Check for correct areas: either the specified area must be present,
|
|
# or there must only be a single area in the data.
|
|
found_area = None
|
|
|
|
for line, children in syntree:
|
|
# Let the following line be an idiom for parsing ospffile:
|
|
tag, *details = line.split()
|
|
# Possible values: BIRD (from greeting), area.
|
|
if tag == 'BIRD': continue
|
|
if tag != 'area': raise OspfFileParseError(f'Unknown top-level tag: {tag}')
|
|
area = self.parse_fake_ip(details[0])
|
|
if self.area is not None and area != self.area: continue
|
|
if self.area is None and found_area is not None:
|
|
raise OspfFileParseError('Too many areas provided')
|
|
found_area = area
|
|
|
|
# We think we have correct area. parse it.
|
|
self.parse_area(result, children)
|
|
|
|
# no break here, because we might encounter another area, in which
|
|
# case we raise an error.
|
|
|
|
# Final check: we should have found at least one vertex, else this was
|
|
# not a good file (likely cause: the specified area was not present)
|
|
if len(result.vertices) == 0:
|
|
if self.area is not None and found_area is not None:
|
|
raise OspfFileParseError('Parsed empty topology, refusing to continue. (Maybe bad area?)')
|
|
raise OspfFileParseError('Parsed empty topology, refusing to continue.')
|
|
|
|
if freeze: result.freeze()
|
|
return result
|
|
|
|
def parse_area(self, result, syntree):
|
|
self.future_transit_networks_edges: list[tuple[VertexID, str, int, int]] = [] # source, target, cost, count
|
|
for line, children in syntree:
|
|
tag, *details = line.split()
|
|
if tag == 'router':
|
|
vertex = self.add_vertex_for_router(result, details)
|
|
self.parse_vertex(result, children, vertex)
|
|
elif tag == 'network':
|
|
# This is trickier, since the details for the vertex may be
|
|
# hidden in the children. Therefore, we offload this to a
|
|
# dedicated function.
|
|
self.parse_l2_network(result, line, children)
|
|
else:
|
|
raise OspfFileParseError(f'Unknown tag in area: {tag}')
|
|
# Process postponed edges. At this point, all the vertices should
|
|
# already be created, so the only issue is resolving the correct
|
|
# VertexIDs for the networks.
|
|
# Since these are networks, the process is generally different for each version of OSPF
|
|
if len(self.future_transit_networks_edges) > 0:
|
|
if self.version is None:
|
|
# TODO: logger?
|
|
print('WARNING: At this point the OSPF version should have been guessed!')
|
|
# We need some sort of directive for this. We make one up.
|
|
directive = 'network ' + self.future_transit_networks_edges[0][1]
|
|
self.guess_version(directive)
|
|
if self.version == 2: return self.add_transit_network_edges_ospfv2(result)
|
|
if self.version == 3: return self.add_transit_network_edges_ospfv3(result)
|
|
raise RuntimeError('Bug? Unreachable point of code.')
|
|
|
|
def add_transit_network_edges_ospfv2(self, result):
|
|
for src, tgt, cost, count in self.future_transit_networks_edges:
|
|
# tgt may under some circumstances be a DR RID in brackets. We have
|
|
# not seen that, so not implementing.
|
|
if tgt.startswith(r'['): raise NotImplementedError('A network is identified by DR, not by range.')
|
|
tgt_addr = IPv4Network(tgt)
|
|
# We do not have a VertexFinder available at the moment. TODO?
|
|
candidates = list(filter(lambda v: v.type == VertexType.TransitNet and v.id.address == tgt_addr, result.vertices.values()))
|
|
found = 0
|
|
for cand in candidates:
|
|
# We match by the network being connected back to the router
|
|
for edge in cand.outgoing_edges:
|
|
if edge.target == src:
|
|
if found == 0:
|
|
# This is the first occurence, so use this
|
|
edge = Edge(source=src, target=cand.id, cost=cost, count=count)
|
|
result.add_edge(edge)
|
|
found += 1
|
|
if found == 0:
|
|
raise OspfFileParseError(f'Could not find a network {tgt_addr} to connect {src.router_id} to.')
|
|
if found > 1:
|
|
print(f'WARNING: Multiple candidates for connecting {src.router_id} to {tgt_addr}')
|
|
|
|
def add_transit_network_edges_ospfv3(self, result):
|
|
for src, tgt, cost, count in self.future_transit_networks_edges:
|
|
# tgt is a DR RID + iface ID in brackets. This pair should uniquely
|
|
# determine the network. (We are dealing with a single topology, so
|
|
# it must match.)
|
|
dr_id, discr = tgt.lstrip(r'[').rstrip(']').split('-')
|
|
dr_id = self.parse_fake_ip(dr_id)
|
|
candidates = list(filter(lambda vid: vid.dr_id == dr_id and vid.discriminator == discr, result.vertices))
|
|
if len(candidates) != 1: raise OspfFileParseError(f'Multiple or no candidates for uniquely determined network {tgt}')
|
|
tgtid = candidates[0]
|
|
edge = Edge(source=src, target=tgtid, cost=cost, count=count)
|
|
result.add_edge(edge)
|
|
|
|
|
|
def parse_l2_network(self, result, directive, children):
|
|
# We need to split the children into at least two groups: those
|
|
# describing incident edges and those specifying network details
|
|
detail_lines = []
|
|
edge_lines = []
|
|
for line, chld in children:
|
|
tag, *rest = line.split()
|
|
if tag in ['distance', 'unreachable']: continue
|
|
if tag in ['dr', 'address']: detail_lines.append((line, chld))
|
|
elif tag in ['router']: edge_lines.append((line, chld))
|
|
else: raise OspfFileParseError(f'Unknown tag for network: {tag}')
|
|
vtx = self.get_vertex_for_l2_network(directive, detail_lines)
|
|
result.add_vertex(vtx)
|
|
own_id = vtx.id
|
|
|
|
# Add the edges
|
|
# The neighbours may only be routers for transit networks, so we can
|
|
# add them right away.
|
|
# In order to count matching lines, we note the edges and create them in bulk.
|
|
future_edges: dict[VertexID, int] = defaultdict(lambda: 0)
|
|
for line, chld in edge_lines:
|
|
if chld != []: raise OspfFileParseError('Unsupported level-4 directive')
|
|
# TODO: If BIRD ever starts supporting RFC 8042 (OSPF Two-Part
|
|
# Metric), this is likely to also include the metric. At that
|
|
# moment this will crash and only then will we implement that
|
|
# feature. (We could guess the format right away, but that would
|
|
# only be a guess.)
|
|
tag, router = line.split()
|
|
rid = self.parse_fake_ip(router)
|
|
peer_id = VertexID(family=None, address=None, is_router=True, router_id=rid, dr_id=None, discriminator=None)
|
|
future_edges[peer_id] += 1
|
|
for peer, count in future_edges.items():
|
|
edge = Edge(source=own_id, target=peer, cost=0, count=count)
|
|
result.add_edge(edge)
|
|
|
|
def parse_vertex(self, result, syntree, vertex):
|
|
own_id = vertex.id
|
|
# We know that syntree only contains irrelevant details
|
|
# (distance/unreachable) and edges to other vertices. The only problem
|
|
# is resolving transit networks, since syntree does not contain DRs
|
|
# when dealing with OSPFv2.
|
|
T = VertexType
|
|
neigh_types: Final[dict[str, tuple[VertexType, bool]]] = {
|
|
# We map not only the type of vertex, but also whether the edge is virtual (vlinks only)
|
|
'vlink': (T.Router, True),
|
|
'xnetwork': (T.ExtraAreaNet, False),
|
|
'xrouter': (T.ExtraAreaRouter, False),
|
|
'stubnet': (T.StubNet, False),
|
|
'external': (T.ExternalNet, False),
|
|
'router': (T.Router, False),
|
|
'network': (T.TransitNet, False),
|
|
}
|
|
# As mentioned, we can create most of the edges right away, but not
|
|
# transit networks. (Stubnets are fine, since we know our RID.)
|
|
# Same as for networks: we must create the edges in bulk at the end,
|
|
# since there may be multiedges.
|
|
future_edges: dict[tuple[VertexID, int, MetricType, bool], int] = defaultdict(lambda: 0)
|
|
future_transit_networks: dict[tuple[str, int], int] = defaultdict(lambda: 0)
|
|
for line, chld in syntree:
|
|
tag, *det = line.split()
|
|
if tag in ['distance', 'unreachable']: continue
|
|
if tag not in neigh_types:
|
|
raise OspfFileParseError(f'Unknown tag {tag} for router {vertex.id.router_id}')
|
|
if chld != []: raise OspfFileParseError('Unsupported level-4 directive')
|
|
# All of the lines seem to follow the same format
|
|
tgt, mtype, cost = det
|
|
try:
|
|
mtype = {'metric': MetricType.Type1, 'metric2': MetricType.Type2}[mtype]
|
|
except KeyError as e:
|
|
raise OspfFileParseError(f'Unknown metric type {e.args}') from e
|
|
cost = int(cost)
|
|
ntype, isvlink = neigh_types[tag]
|
|
if ntype in [T.Router, T.ExtraAreaRouter]:
|
|
rid = self.parse_fake_ip(tgt)
|
|
target_id = VertexID(family=None, address=None, is_router=True, router_id=rid, dr_id=None, discriminator=None)
|
|
if target_id not in result.vertices:
|
|
vtx = Vertex(id=target_id, topology=None, type=ntype)
|
|
result.add_vertex(vtx)
|
|
elif result.vertices[target_id].type != ntype:
|
|
# FIXME: should check using add_vertex_for_router?
|
|
raise OspfFileParseError(f'The router {target_id.router_id} is both in area and external to it.')
|
|
future_edges[(target_id, cost, mtype, isvlink)] += 1
|
|
continue
|
|
if ntype in [T.ExtraAreaNet, T.ExternalNet, T.StubNet]:
|
|
rid = own_id.router_id if ntype == T.StubNet else None
|
|
addr = ip_network(tgt)
|
|
if isinstance(addr, IPv4Network):
|
|
family = AF_INET
|
|
elif isinstance(addr, IPv6Network):
|
|
family = AF_INET6
|
|
else:
|
|
raise OspfFileParseError(f'Unknown family of address {addr} in edge from {own_id.router_id}')
|
|
target_id = VertexID(family=family, address=addr, is_router=False, router_id=rid, dr_id=None, discriminator=None)
|
|
if target_id not in result.vertices:
|
|
vtx = Vertex(id=target_id, topology=None, type=ntype)
|
|
result.add_vertex(vtx)
|
|
future_edges[(target_id, cost, mtype, isvlink)] += 1
|
|
continue
|
|
if ntype == T.TransitNet: # Always true at this point, just for formatting
|
|
# Deal with this later.
|
|
future_transit_networks[(tgt, cost)] += 1
|
|
continue
|
|
# Bulk edge creation:
|
|
for details, count in future_edges.items():
|
|
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.items():
|
|
tgt, cost = details
|
|
self.future_transit_networks_edges.append((own_id, tgt, cost, count))
|
|
|
|
def get_vertex_for_l2_network(self, directive, details):
|
|
if self.version is None: self.guess_version(directive)
|
|
if self.version == 2: return self.get_vertex_for_l2_network_ospfv2(directive, details)
|
|
if self.version == 3: return self.get_vertex_for_l2_network_ospfv3(directive, details)
|
|
|
|
def guess_version(self, directive):
|
|
"""Version guesser, usable for both l2 and l3 directives."""
|
|
if self.version is not None: raise RuntimeError('Bug? Trying to guess known OSPF version.')
|
|
if not self.guessing: raise RuntimeError('Bug? Inconsistency about guessing in OspfFileTopologyParser.')
|
|
tag, ident, *rest_maybe = directive.split()
|
|
if tag != 'network': raise RuntimeError('Bug? Guessing from a non-network. (No tasseography here.)')
|
|
# For OSPFv2, networks can be identified by DR RID on l3, but it will
|
|
# not contain iface ID. (Ref: BIRD source @ ecbae010 proto/ospf/ospf.c:1084)
|
|
self.version = 3 if r'-' in ident else 2
|
|
|
|
def get_vertex_for_l2_network_ospfv3(self, directive, details):
|
|
_tag, ident = directive.split()
|
|
# In OSPFv3, the ID is always '[DRID-iface]'
|
|
dr, discr = ident.lstrip(r'[').rstrip(r']').split(r'-')
|
|
dr = self.parse_fake_ip(dr)
|
|
addresses = []
|
|
for line, chld in details:
|
|
if chld != []: raise OspfFileParseError('Unsupported level-4 directive')
|
|
tag, addr = line.split()
|
|
if tag != 'address':
|
|
raise OspfFileParseError(
|
|
'Bad ospffile: mixing OSPFv2 directive in OSPFv3 file.'
|
|
+ (' Maybe bad guess?' if self.guessing else ''))
|
|
addresses.append(ip_network(addr))
|
|
if len(addresses) == 0:
|
|
final_addr = None
|
|
family = None
|
|
elif len(addresses) == 1:
|
|
final_addr = addresses[0]
|
|
if isinstance(final_addr, IPv4Network):
|
|
family = AF_INET
|
|
elif isinstance(final_addr, IPv6Network):
|
|
family = AF_INET6
|
|
else:
|
|
raise OspfFileParseError(f'Unknown family of address {final_addr} in network {directive}')
|
|
else: # Multiple addresses
|
|
# mypy 1.3.0 wtf.
|
|
final_addr = tuple(addresses)
|
|
if all(isinstance(a, IPv4Network) for a in final_addr):
|
|
family = AF_INET
|
|
elif all(isinstance(a, IPv6Network) for a in final_addr):
|
|
family = AF_INET6
|
|
else:
|
|
raise OspfFileParseError(f'Multiple or unknown families for network {directive}')
|
|
|
|
vtxid = VertexID(family=family, is_router=False, address=final_addr, router_id=None, dr_id = dr, discriminator=discr)
|
|
vtx = Vertex(id=vtxid, topology=None, type=VertexType.TransitNet)
|
|
return vtx
|
|
|
|
def get_vertex_for_l2_network_ospfv2(self, directive, details):
|
|
_tag, ident = directive.split()
|
|
network = IPv4Network(ident)
|
|
dr = None
|
|
for line, chld in details:
|
|
if chld != []: raise OspfFileParseError('Unsupported level-4 directive')
|
|
tag, drid = line.split()
|
|
if tag != 'dr':
|
|
raise OspfFileParseError(
|
|
'Bad ospffile: mixing OSPFv3 directive in OSPFv2 file.'
|
|
+ (' Maybe bad guess?' if self.guessing else ''))
|
|
if dr is not None:
|
|
raise OspfFileParseError(f'Multiple DRs for network {directive}')
|
|
dr = self.parse_fake_ip(drid)
|
|
|
|
vtxid = VertexID(family=AF_INET, is_router=False, address=network, router_id=None, dr_id=dr, discriminator=None)
|
|
vtx = Vertex(id=vtxid, topology=None, type=VertexType.TransitNet)
|
|
return vtx
|
|
|
|
def add_vertex_for_router(self, result, details, extraarea=False):
|
|
# FIXME: Should be reused for l3 directive, but not possible atm
|
|
type = VertexType.Router if not extraarea else VertexType.ExtraAreaRouter
|
|
router_id = self.parse_fake_ip(details[0])
|
|
vtxid = VertexID(family=None, is_router=True, address=None, router_id=router_id, dr_id=None, discriminator=None)
|
|
vtx = Vertex(id=vtxid, topology=None, type=type)
|
|
# If the router is already present, check consistency (It is not wrong
|
|
# to add it multiple times, it should be correctly substituted.)
|
|
if vtxid in result.vertices:
|
|
if vtx.type != result.vertices[vtxid].type:
|
|
raise OspfFileParseError(f'Inconsistent type for router {vtxid.router_id}')
|
|
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 stil
|
|
re-raise, so the error message is sane.)"""
|
|
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\n':
|
|
protocols = self.find_runing_ospf(sock)
|
|
raise BirdMultipleInstancesError(e, protocols) from e
|
|
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.text.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
|