1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-04-17 21:38:27 +00:00
thatmattlove-hyperglass/hyperglass/models/parsing/frr.py
Tan Siewert 79d235c2ff
frr: return AS0 if AS_PATH length is zero
Local prefixes usually do not have an AS_PATH:
```
vtysh -c "show bgp ipv4 unicast 10.10.10.10 json" | jq '.paths[].aspath'
{
  "string": "Local",
  "segments": [],
  "length": 0
}
```

Set AS0 as a temporary solution when AS_PATH length is zero. This
avoids parser failures for local prefixes, though ideally we should
use the device's actual ASN (see TODO in code).

Signed-off-by: Tan Siewert <tan@siewert.io>
2025-10-04 21:35:30 +02:00

130 lines
3.7 KiB
Python

"""Data Models for Parsing FRRouting JSON Response."""
# Standard Library
import typing as t
from datetime import datetime
# Third Party
from pydantic import ConfigDict, model_validator
# Project
from hyperglass.log import log
from hyperglass.models.data import BGPRouteTable
# Local
from ..main import HyperglassModel
FRRPeerType = t.Literal["internal", "external", "confed-internal", "confed-external"]
def _alias_generator(field):
components = field.split("_")
return components[0] + "".join(x.title() for x in components[1:])
class _FRRBase(HyperglassModel):
model_config = ConfigDict(alias_generator=_alias_generator, extra="ignore")
class FRRNextHop(_FRRBase):
"""FRR Next Hop Model."""
ip: str
afi: str
metric: int = 0
accessible: bool
used: bool = False
class FRRPeer(_FRRBase):
"""FRR Peer Model."""
peer_id: str
router_id: str
type: FRRPeerType
class FRRPath(_FRRBase):
"""FRR Path Model."""
aspath: t.List[int]
aggregator_as: int = 0
aggregator_id: str = ""
loc_prf: int = 100 # 100 is the default value for local preference
metric: int = 0
med: int = 0
weight: int = 0
valid: bool
last_update: int
bestpath: bool
community: t.List[str]
nexthops: t.List[FRRNextHop]
peer: FRRPeer
@model_validator(mode="before")
def validate_path(cls, values):
"""Extract meaningful data from FRR response."""
new = values.copy()
# Local prefixes (i.e. those in the same ASN) usually have
# no AS_PATH.
# Set AS_PATH to AS0 for now as we cannot ensure that the
# ASN for the prefix is the primary ASN.
if values["aspath"]["length"] != 0:
new["aspath"] = values["aspath"]["segments"][0]["list"]
else:
# TODO: Get an ASN that is reasonable (e.g. primary ASN)
new["aspath"] = [0]
community = values.get("community", {"list": []})
new["community"] = community["list"]
new["lastUpdate"] = values["lastUpdate"]["epoch"]
bestpath = values.get("bestpath", {})
new["bestpath"] = bestpath.get("overall", False)
return new
class FRRBGPTable(_FRRBase):
"""FRR Route Model."""
prefix: str
paths: t.List[FRRPath] = []
def bgp_table(self):
"""Convert the FRR-specific fields to standard parsed data model."""
# TODO: somehow, get the actual VRF
vrf = "default"
routes = []
for route in self.paths:
now = datetime.utcnow().timestamp()
then = datetime.utcfromtimestamp(route.last_update).timestamp()
age = int(now - then)
routes.append(
{
"prefix": self.prefix,
"active": route.bestpath,
"age": age,
"weight": route.weight,
"med": route.med,
"local_preference": route.loc_prf,
"as_path": route.aspath,
"communities": route.community,
"next_hop": route.nexthops[0].ip,
"source_as": route.aggregator_as,
"source_rid": route.aggregator_id,
"peer_rid": route.peer.peer_id,
# TODO: somehow, get the actual RPKI state
# This depends on whether or not the RPKI module is enabled in FRR
"rpki_state": 3,
}
)
serialized = BGPRouteTable(
vrf=vrf,
count=len(routes),
routes=routes,
winning_weight="high",
)
log.bind(platform="frr", response=repr(serialized)).debug("Serialized response")
return serialized