156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""BGP event location resolver.
|
|
|
|
A BGP event (announcement / withdrawal / RIB entry) is geographically tied to
|
|
the route collector that observed it. This module defines the pipeline that
|
|
turns an event payload into renderable coordinates.
|
|
|
|
Current resolver chain:
|
|
|
|
SourceCoordinates → event payload itself carries lat/lon (rare; some
|
|
enriched feeds do).
|
|
InheritFromCollector → look up the owning collector via
|
|
:func:`resolve_bgp_collector_location`.
|
|
|
|
Future plug-ins (no consumer changes required, just append to the list):
|
|
|
|
ASNFacilityResolver — origin/peer ASN → peeringdb facility.
|
|
PrefixGeoResolver — prefix → IP range geo lookup (iptoasn / opengeofeed).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.services.bgp_collector_locations import (
|
|
get_bgp_collector_location_dict,
|
|
)
|
|
from app.services.location import (
|
|
InheritFromAnotherEntityResolver,
|
|
LocationCandidate,
|
|
LocationPipeline,
|
|
LocationQuery,
|
|
ResolutionResult,
|
|
SourceCoordinatesResolver,
|
|
coerce_str,
|
|
)
|
|
|
|
|
|
def _inherit_from_owning_collector(
|
|
query: LocationQuery,
|
|
) -> LocationCandidate | None:
|
|
"""Look up the event's owning collector by exact name in the DB-backed cache."""
|
|
extra = query.extra or {}
|
|
collector_name = coerce_str(extra.get("collector"))
|
|
if not collector_name:
|
|
return None
|
|
legacy = get_bgp_collector_location_dict(collector_name)
|
|
if not legacy:
|
|
return None
|
|
latitude = legacy.get("latitude")
|
|
longitude = legacy.get("longitude")
|
|
if latitude in (None, 0.0) or longitude in (None, 0.0):
|
|
return None
|
|
return LocationCandidate(
|
|
latitude=float(latitude),
|
|
longitude=float(longitude),
|
|
display_name=legacy.get("matched_location_name") or collector_name,
|
|
precision=legacy.get("precision") or "city",
|
|
confidence=float(legacy.get("confidence") or 0.85),
|
|
query=f"inherit_from_collector::{collector_name}",
|
|
source="inherited_from_collector",
|
|
source_note=(
|
|
f"Inherited from owning collector {collector_name}"
|
|
),
|
|
matched_fields=("collector",),
|
|
needs_confirmation=bool(legacy.get("needs_confirmation")),
|
|
city=legacy.get("city"),
|
|
region=None,
|
|
country=legacy.get("country"),
|
|
matched_location_name=legacy.get("matched_location_name"),
|
|
location_verified_at=legacy.get("verified_at"),
|
|
suggested_registry_entry=None,
|
|
)
|
|
|
|
|
|
BGP_EVENT_PIPELINE = LocationPipeline(
|
|
[
|
|
SourceCoordinatesResolver(),
|
|
InheritFromAnotherEntityResolver(
|
|
source_lookup=_inherit_from_owning_collector,
|
|
name="inherited_from_collector",
|
|
),
|
|
# Plug new resolvers (peeringdb / ASN facility / prefix-geo) here.
|
|
],
|
|
failure_reason=(
|
|
"Could not resolve BGP event coordinates: no source coords, owning"
|
|
" collector unknown, and no fallback resolver matched."
|
|
),
|
|
)
|
|
|
|
|
|
def resolve_bgp_event_location(
|
|
*,
|
|
collector: str,
|
|
source_latitude: float | None = None,
|
|
source_longitude: float | None = None,
|
|
site: str | None = None,
|
|
operator: str | None = None,
|
|
peer_asn: int | None = None,
|
|
origin_asn: int | None = None,
|
|
prefix: str | None = None,
|
|
) -> ResolutionResult:
|
|
"""Resolve a BGP event to its renderable coordinates.
|
|
|
|
The ``peer_asn`` / ``origin_asn`` / ``prefix`` arguments are accepted
|
|
today so future resolvers (ASN→facility, prefix→geo) can consume them
|
|
without callers needing to change.
|
|
"""
|
|
query = LocationQuery(
|
|
name=collector or None,
|
|
aliases=tuple(filter(None, (collector,))),
|
|
source_latitude=source_latitude,
|
|
source_longitude=source_longitude,
|
|
extra={
|
|
"collector": collector or "",
|
|
"site": coerce_str(site),
|
|
"operator": coerce_str(operator),
|
|
"peer_asn": peer_asn,
|
|
"origin_asn": origin_asn,
|
|
"prefix": coerce_str(prefix),
|
|
},
|
|
)
|
|
return BGP_EVENT_PIPELINE.resolve_best(query)
|
|
|
|
|
|
def resolve_bgp_event_geo_dict(
|
|
collector: str,
|
|
*,
|
|
source_latitude: float | None = None,
|
|
source_longitude: float | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Convenience wrapper returning the legacy ``collector_geo`` dict shape.
|
|
|
|
Preserves ``city``/``country``/``latitude``/``longitude`` keys (consumed
|
|
by existing detectors / enrichment / DB serialization) and adds
|
|
``precision``/``source``/``needs_confirmation`` for richer downstream use.
|
|
"""
|
|
result = resolve_bgp_event_location(
|
|
collector=collector,
|
|
source_latitude=source_latitude,
|
|
source_longitude=source_longitude,
|
|
)
|
|
candidate = result.location
|
|
if candidate is None:
|
|
return {}
|
|
return {
|
|
"city": candidate.city,
|
|
"country": candidate.country,
|
|
"latitude": candidate.latitude,
|
|
"longitude": candidate.longitude,
|
|
"precision": candidate.precision,
|
|
"source": candidate.source,
|
|
"needs_confirmation": candidate.needs_confirmation,
|
|
"matched_location_name": candidate.matched_location_name,
|
|
"confidence": candidate.confidence,
|
|
}
|