mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add structured data support for arista_eos
This commit is contained in:
@@ -19,7 +19,7 @@ TARGET_FORMAT_SPACE = ("huawei", "huawei_vrpv8")
|
|||||||
|
|
||||||
TARGET_JUNIPER_ASPATH = ("juniper", "juniper_junos")
|
TARGET_JUNIPER_ASPATH = ("juniper", "juniper_junos")
|
||||||
|
|
||||||
SUPPORTED_STRUCTURED_OUTPUT = ("juniper",)
|
SUPPORTED_STRUCTURED_OUTPUT = ("juniper", "arista_eos")
|
||||||
|
|
||||||
STATUS_CODE_MAP = {"warning": 400, "error": 400, "danger": 500}
|
STATUS_CODE_MAP = {"warning": 400, "error": 400, "danger": 500}
|
||||||
|
|
||||||
|
@@ -47,6 +47,38 @@ class _VPNIPv6(CommandSet):
|
|||||||
traceroute: StrictStr = "traceroute vrf {vrf} ipv6 {target} source {source}"
|
traceroute: StrictStr = "traceroute vrf {vrf} ipv6 {target} source {source}"
|
||||||
|
|
||||||
|
|
||||||
|
_structured = CommandGroup(
|
||||||
|
ipv4_default=CommandSet(
|
||||||
|
bgp_route="show ip bgp {target} | json",
|
||||||
|
bgp_aspath="show ip bgp regexp {target} | json",
|
||||||
|
bgp_community="show ip bgp community {target} | json",
|
||||||
|
ping="ping ip {target} source {source}",
|
||||||
|
traceroute="traceroute ip {target} source {source}",
|
||||||
|
),
|
||||||
|
ipv6_default=CommandSet(
|
||||||
|
bgp_route="show ipv6 bgp {target} | json",
|
||||||
|
bgp_aspath="show ipv6 bgp regexp {target} | json",
|
||||||
|
bgp_community="show ipv6 bgp community {target} | json",
|
||||||
|
ping="ping ipv6 {target} source {source}",
|
||||||
|
traceroute="traceroute ipv6 {target} source {source}",
|
||||||
|
),
|
||||||
|
ipv4_vpn=CommandSet(
|
||||||
|
bgp_route="show ip bgp {target} vrf {vrf} | json",
|
||||||
|
bgp_aspath="show ip bgp regexp {target} vrf {vrf} | json",
|
||||||
|
bgp_community="show ip bgp community {target} vrf {vrf} | json",
|
||||||
|
ping="ping vrf {vrf} ip {target} source {source}",
|
||||||
|
traceroute="traceroute vrf {vrf} ip {target} source {source}",
|
||||||
|
),
|
||||||
|
ipv6_vpn=CommandSet(
|
||||||
|
bgp_route="show ipv6 bgp {target} vrf {vrf} | json",
|
||||||
|
bgp_aspath="show ipv6 bgp regexp {target} vrf {vrf} | json",
|
||||||
|
bgp_community="show ipv6 bgp community {target} vrf {vrf} | json",
|
||||||
|
ping="ping vrf {vrf} ipv6 {target} source {source}",
|
||||||
|
traceroute="traceroute vrf {vrf} ipv6 {target} source {source}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AristaEOSCommands(CommandGroup):
|
class AristaEOSCommands(CommandGroup):
|
||||||
"""Validation model for default arista_eos commands."""
|
"""Validation model for default arista_eos commands."""
|
||||||
|
|
||||||
@@ -54,3 +86,8 @@ class AristaEOSCommands(CommandGroup):
|
|||||||
ipv6_default: _IPv6 = _IPv6()
|
ipv6_default: _IPv6 = _IPv6()
|
||||||
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
ipv4_vpn: _VPNIPv4 = _VPNIPv4()
|
||||||
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
ipv6_vpn: _VPNIPv6 = _VPNIPv6()
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize command group, ensure structured fields are not overridden."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.structured = _structured
|
||||||
|
155
hyperglass/models/parsing/arista_eos.py
Normal file
155
hyperglass/models/parsing/arista_eos.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""Data Models for Parsing Arista JSON Response."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.log import log
|
||||||
|
|
||||||
|
# Local
|
||||||
|
from ..main import HyperglassModel
|
||||||
|
from .serialized import ParsedRoutes
|
||||||
|
|
||||||
|
RPKI_STATE_MAP = {
|
||||||
|
"invalid": 0,
|
||||||
|
"valid": 1,
|
||||||
|
"notFound": 2,
|
||||||
|
"notValidated": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
WINNING_WEIGHT = "high"
|
||||||
|
|
||||||
|
|
||||||
|
def _alias_generator(field: str) -> str:
|
||||||
|
caps = "".join(x for x in field.title() if x.isalnum())
|
||||||
|
return caps[0].lower() + caps[1:]
|
||||||
|
|
||||||
|
|
||||||
|
class _AristaBase(HyperglassModel):
|
||||||
|
"""Base Model for Arista validation."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "ignore"
|
||||||
|
alias_generator = _alias_generator
|
||||||
|
|
||||||
|
|
||||||
|
class AristaAsPathEntry(_AristaBase):
|
||||||
|
"""Validation model for Arista asPathEntry."""
|
||||||
|
|
||||||
|
as_path_type: str = "External"
|
||||||
|
as_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class AristaPeerEntry(_AristaBase):
|
||||||
|
"""Validation model for Arista peerEntry."""
|
||||||
|
|
||||||
|
peer_router_id: str
|
||||||
|
peer_addr: str
|
||||||
|
|
||||||
|
|
||||||
|
class AristaRouteType(_AristaBase):
|
||||||
|
"""Validation model for Arista routeType."""
|
||||||
|
|
||||||
|
origin: str
|
||||||
|
suppressed: bool
|
||||||
|
valid: bool
|
||||||
|
active: bool
|
||||||
|
origin_validity: Optional[str] = "notVerified"
|
||||||
|
|
||||||
|
|
||||||
|
class AristaRouteDetail(_AristaBase):
|
||||||
|
"""Validation for Arista routeDetail."""
|
||||||
|
|
||||||
|
origin: str
|
||||||
|
label_stack: List = []
|
||||||
|
ext_community_list: List[str] = []
|
||||||
|
ext_community_list_raw: List[str] = []
|
||||||
|
community_list: List[str] = []
|
||||||
|
large_community_list: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class AristaRoutePath(_AristaBase):
|
||||||
|
"""Validation model for Arista bgpRoutePaths."""
|
||||||
|
|
||||||
|
as_path_entry: AristaAsPathEntry
|
||||||
|
med: int
|
||||||
|
local_preference: int
|
||||||
|
weight: int
|
||||||
|
peer_entry: AristaPeerEntry
|
||||||
|
reason_not_bestpath: str
|
||||||
|
timestamp: int = int(datetime.utcnow().timestamp())
|
||||||
|
next_hop: str
|
||||||
|
route_type: AristaRouteType
|
||||||
|
route_detail: Optional[AristaRouteDetail]
|
||||||
|
|
||||||
|
|
||||||
|
class AristaRouteEntry(_AristaBase):
|
||||||
|
"""Validation model for Arista bgpRouteEntries."""
|
||||||
|
|
||||||
|
total_paths: int = 0
|
||||||
|
bgp_advertised_peer_groups: Dict = {}
|
||||||
|
mask_length: int
|
||||||
|
bgp_route_paths: List[AristaRoutePath] = []
|
||||||
|
|
||||||
|
|
||||||
|
class AristaRoute(_AristaBase):
|
||||||
|
"""Validation model for Arista bgpRouteEntries data."""
|
||||||
|
|
||||||
|
router_id: str
|
||||||
|
vrf: str
|
||||||
|
bgp_route_entries: Dict[str, AristaRouteEntry]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_route_age(timestamp: int) -> int:
|
||||||
|
now = datetime.utcnow()
|
||||||
|
now_timestamp = int(now.timestamp())
|
||||||
|
return now_timestamp - timestamp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_as_path(as_path: str) -> List[str]:
|
||||||
|
return [int(p) for p in as_path.split() if p.isdecimal()]
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
"""Convert the Arista-formatted fields to standard parsed data model."""
|
||||||
|
routes = []
|
||||||
|
count = 0
|
||||||
|
for prefix, entries in self.bgp_route_entries.items():
|
||||||
|
|
||||||
|
count += entries.total_paths
|
||||||
|
|
||||||
|
for route in entries.bgp_route_paths:
|
||||||
|
|
||||||
|
as_path = self._get_as_path(route.as_path_entry.as_path)
|
||||||
|
rpki_state = RPKI_STATE_MAP.get(route.route_type.origin_validity, 3)
|
||||||
|
|
||||||
|
# BGP AS Path and BGP Community queries do not include the routeDetail
|
||||||
|
# block. Therefore, we must verify it exists before including its data.
|
||||||
|
communities = []
|
||||||
|
if route.route_detail is not None:
|
||||||
|
communities = route.route_detail.community_list
|
||||||
|
|
||||||
|
routes.append(
|
||||||
|
{
|
||||||
|
"prefix": prefix,
|
||||||
|
"active": route.route_type.active,
|
||||||
|
"age": self._get_route_age(route.timestamp),
|
||||||
|
"weight": route.weight,
|
||||||
|
"med": route.med,
|
||||||
|
"local_preference": route.local_preference,
|
||||||
|
"as_path": as_path,
|
||||||
|
"communities": communities,
|
||||||
|
"next_hop": route.next_hop,
|
||||||
|
"source_as": as_path[0],
|
||||||
|
"source_rid": route.peer_entry.peer_router_id,
|
||||||
|
"peer_rid": route.peer_entry.peer_router_id,
|
||||||
|
"rpki_state": rpki_state,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized = ParsedRoutes(
|
||||||
|
vrf=self.vrf, count=count, routes=routes, winning_weight=WINNING_WEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug("Serialized Arista response: {}", serialized)
|
||||||
|
return serialized
|
65
hyperglass/models/parsing/arista_route.json
Normal file
65
hyperglass/models/parsing/arista_route.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"vrfs": {
|
||||||
|
"default": {
|
||||||
|
"routerId": "198.18.0.1",
|
||||||
|
"vrf": "default",
|
||||||
|
"bgpRouteEntries": {
|
||||||
|
"198.18.2.0/24": {
|
||||||
|
"totalPaths": 1,
|
||||||
|
"bgpAdvertisedPeerGroups": {},
|
||||||
|
"maskLength": 24,
|
||||||
|
"bgpRoutePaths": [
|
||||||
|
{
|
||||||
|
"asPathEntry": {
|
||||||
|
"asPathType": "External",
|
||||||
|
"asPath": "65002 65003"
|
||||||
|
},
|
||||||
|
"med": 8675309,
|
||||||
|
"localPreference": 100,
|
||||||
|
"weight": 0,
|
||||||
|
"peerEntry": {
|
||||||
|
"peerRouterId": "198.18.0.2",
|
||||||
|
"peerAddr": "198.18.0.2"
|
||||||
|
},
|
||||||
|
"reasonNotBestpath": "noReason",
|
||||||
|
"timestamp": 1612996830,
|
||||||
|
"nextHop": "198.18.0.2",
|
||||||
|
"routeType": {
|
||||||
|
"atomicAggregator": false,
|
||||||
|
"origin": "Igp",
|
||||||
|
"suppressed": false,
|
||||||
|
"queued": false,
|
||||||
|
"valid": true,
|
||||||
|
"ecmpContributor": false,
|
||||||
|
"luRoute": false,
|
||||||
|
"active": true,
|
||||||
|
"stale": false,
|
||||||
|
"ecmp": false,
|
||||||
|
"backup": false,
|
||||||
|
"ecmpHead": false,
|
||||||
|
"ucmp": false
|
||||||
|
},
|
||||||
|
"routeDetail": {
|
||||||
|
"origin": "Igp",
|
||||||
|
"labelStack": [],
|
||||||
|
"isLeaked": false,
|
||||||
|
"tunnelRibEligible": false,
|
||||||
|
"extCommunityList": [],
|
||||||
|
"extCommunityListRaw": [],
|
||||||
|
"communityList": ["65002:1"],
|
||||||
|
"rxSafi": "Unicast",
|
||||||
|
"bgpContributors": [],
|
||||||
|
"recvdFromRRClient": false,
|
||||||
|
"igpMetric": 1,
|
||||||
|
"largeCommunityList": [],
|
||||||
|
"domainPath": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"address": "198.18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"asn": "65001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
hyperglass/parsing/arista.py
Normal file
53
hyperglass/parsing/arista.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Parse Arista JSON Response to Structured Data."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import json
|
||||||
|
from typing import Dict, Sequence
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.log import log
|
||||||
|
from hyperglass.exceptions import ParsingError
|
||||||
|
from hyperglass.models.parsing.arista_eos import AristaRoute
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arista(output: Sequence[str]) -> Dict: # noqa: C901
|
||||||
|
"""Parse a Arista BGP JSON response."""
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for i, response in enumerate(output):
|
||||||
|
|
||||||
|
try:
|
||||||
|
data: Dict = json.loads(response)
|
||||||
|
|
||||||
|
log.debug("Pre-parsed data: {}", data)
|
||||||
|
|
||||||
|
vrf = list(data["vrfs"].keys())[0]
|
||||||
|
routes = data["vrfs"][vrf]
|
||||||
|
|
||||||
|
log.debug("Pre-validated data: {}", routes)
|
||||||
|
|
||||||
|
validated = AristaRoute(**routes)
|
||||||
|
serialized = validated.serialize().export_dict()
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
data.update(serialized)
|
||||||
|
else:
|
||||||
|
data["routes"].extend(serialized["routes"])
|
||||||
|
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
log.critical("Error decoding JSON: {}", str(err))
|
||||||
|
raise ParsingError("Error parsing response data")
|
||||||
|
|
||||||
|
except (KeyError, IndexError) as err:
|
||||||
|
log.critical("{} was not found in the response", str(err))
|
||||||
|
raise ParsingError("Error parsing response data")
|
||||||
|
|
||||||
|
except ValidationError as err:
|
||||||
|
log.critical(str(err))
|
||||||
|
raise ParsingError(err.errors())
|
||||||
|
|
||||||
|
log.debug("Serialzed: {}", data)
|
||||||
|
return data
|
@@ -1,6 +1,7 @@
|
|||||||
"""Map NOS and Commands to Parsing Functions."""
|
"""Map NOS and Commands to Parsing Functions."""
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
from .arista import parse_arista
|
||||||
from .juniper import parse_juniper
|
from .juniper import parse_juniper
|
||||||
from .mikrotik import parse_mikrotik
|
from .mikrotik import parse_mikrotik
|
||||||
|
|
||||||
@@ -10,6 +11,11 @@ structured_parsers = {
|
|||||||
"bgp_aspath": parse_juniper,
|
"bgp_aspath": parse_juniper,
|
||||||
"bgp_community": parse_juniper,
|
"bgp_community": parse_juniper,
|
||||||
},
|
},
|
||||||
|
"arista_eos": {
|
||||||
|
"bgp_route": parse_arista,
|
||||||
|
"bgp_aspath": parse_arista,
|
||||||
|
"bgp_community": parse_arista,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
scrape_parsers = {
|
scrape_parsers = {
|
||||||
|
25
hyperglass/parsing/test_arista.py
Normal file
25
hyperglass/parsing/test_arista.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Test Arista JSON Parsing."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.log import log
|
||||||
|
|
||||||
|
# Local
|
||||||
|
from .arista import parse_arista
|
||||||
|
|
||||||
|
SAMPLE_FILE = Path(__file__).parent.parent / "models" / "parsing" / "arista_route.json"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
sample = sys.argv[1]
|
||||||
|
else:
|
||||||
|
with SAMPLE_FILE.open("r") as file:
|
||||||
|
sample = file.read()
|
||||||
|
|
||||||
|
parsed = parse_arista([sample])
|
||||||
|
log.info(json.dumps(parsed, indent=2))
|
||||||
|
sys.exit(0)
|
Reference in New Issue
Block a user