1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

Implement Arista table output plugin and default directive

This commit is contained in:
thatmattlove
2021-12-08 17:13:56 -07:00
parent 0ec3086c67
commit c8892f43ea
5 changed files with 318 additions and 3 deletions

View File

@@ -0,0 +1,165 @@
"""Default Arista Directives."""
# Project
from hyperglass.models.directive import Rule, Text, BuiltinDirective
__all__ = (
"AristaBGPRoute",
"AristaBGPASPath",
"AristaBGPCommunity",
"AristaPing",
"AristaTraceroute",
"AristaBGPRouteTable",
"AristaBGPASPathTable",
"AristaBGPCommunityTable",
)
AristaBGPRoute = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_route__",
name="BGP Route",
rules=[
Rule(
condition="0.0.0.0/0",
action="permit",
command="show ip bgp {target}",
),
Rule(
condition="::/0",
action="permit",
command="show ipv6 bgp {target}",
),
],
field=Text(description="IP Address, Prefix, or Hostname"),
table_output="__hyperglass_arista_eos_bgp_route_table__",
platforms=["arista_eos"],
)
AristaBGPASPath = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_aspath__",
name="BGP AS Path",
rules=[
Rule(
condition="*",
action="permit",
commands=[
"show ip bgp regexp {target}",
"show ipv6 bgp regexp {target}",
],
)
],
field=Text(description="AS Path Regular Expression"),
table_output="__hyperglass_arista_eos_bgp_aspath_table__",
platforms=["arista_eos"],
)
AristaBGPCommunity = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_community__",
name="BGP Community",
rules=[
Rule(
condition="*",
action="permit",
commands=[
"show ip bgp community {target}",
"show ipv6 bgp community {target}",
],
)
],
field=Text(description="BGP Community String"),
table_output="__hyperglass_arista_eos_bgp_community_table__",
platforms=["arista_eos"],
)
AristaPing = BuiltinDirective(
id="__hyperglass_arista_eos_ping__",
name="Ping",
rules=[
Rule(
condition="0.0.0.0/0",
action="permit",
command="ping ip {target} source {source4}",
),
Rule(
condition="::/0",
action="permit",
command="ping ipv6 {target} source {source6}",
),
],
field=Text(description="IP Address, Prefix, or Hostname"),
platforms=["arista_eos"],
)
AristaTraceroute = BuiltinDirective(
id="__hyperglass_arista_eos_traceroute__",
name="Traceroute",
rules=[
Rule(
condition="0.0.0.0/0",
action="permit",
command="traceroute ip {target} source {source4}",
),
Rule(
condition="::/0",
action="permit",
command="traceroute ipv6 {target} source {source6}",
),
],
field=Text(description="IP Address, Prefix, or Hostname"),
platforms=["arista_eos"],
)
# Table Output Directives
AristaBGPRouteTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_route_table__",
name="BGP Route",
rules=[
Rule(
condition="0.0.0.0/0",
action="permit",
command="show ip bgp {target} | json",
),
Rule(
condition="::/0",
action="permit",
command="show ipv6 bgp {target} | json",
),
],
field=Text(description="IP Address, Prefix, or Hostname"),
platforms=["arista_eos"],
)
AristaBGPASPathTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_aspath_table__",
name="BGP AS Path",
rules=[
Rule(
condition="*",
action="permit",
commands=[
"show ip bgp regexp {target} | json",
"show ipv6 bgp regexp {target} | json",
],
)
],
field=Text(description="AS Path Regular Expression"),
platforms=["arista_eos"],
)
AristaBGPCommunityTable = BuiltinDirective(
id="__hyperglass_arista_eos_bgp_community_table__",
name="BGP Community",
rules=[
Rule(
condition="*",
action="permit",
commands=[
"show ip bgp community {target} | json",
"show ipv6 bgp community {target} | json",
],
)
],
field=Text(description="BGP Community String"),
platforms=["arista_eos"],
)

View File

@@ -93,7 +93,7 @@ class AristaRouteEntry(_AristaBase):
bgp_route_paths: List[AristaRoutePath] = []
class AristaRoute(_AristaBase):
class AristaBGPTable(_AristaBase):
"""Validation model for Arista bgpRouteEntries data."""
router_id: str
@@ -114,7 +114,7 @@ class AristaRoute(_AristaBase):
return []
return [int(p) for p in as_path.split() if p.isdecimal()]
def serialize(self):
def bgp_table(self: "AristaBGPTable") -> "BGPRouteTable":
"""Convert the Arista-formatted fields to standard parsed data model."""
routes = []
count = 0
@@ -164,5 +164,5 @@ class AristaRoute(_AristaBase):
winning_weight=WINNING_WEIGHT,
)
log.debug("Serialized Arista response: {}", serialized)
log.debug("Serialized Arista response: {!r}", serialized)
return serialized

View File

@@ -2,9 +2,11 @@
# Local
from .remove_command import RemoveCommand
from .bgp_route_arista import BGPRoutePluginArista
from .bgp_route_juniper import BGPRoutePluginJuniper
__all__ = (
"RemoveCommand",
"BGPRoutePluginJuniper",
"BGPRoutePluginArista",
)

View File

@@ -0,0 +1,91 @@
"""Parse Arista JSON Response to Structured Data."""
# Standard Library
import json
import typing as t
# Third Party
from pydantic import PrivateAttr, ValidationError
# Project
from hyperglass.log import log
from hyperglass.exceptions.private import ParsingError
from hyperglass.models.parsing.arista_eos import AristaBGPTable
# Local
from .._output import OutputPlugin
if t.TYPE_CHECKING:
# Project
from hyperglass.models.data import OutputDataModel
from hyperglass.models.api.query import Query
# Local
from .._output import OutputType
def parse_arista(output: t.Sequence[str]) -> "OutputDataModel":
"""Parse a Arista BGP JSON response."""
result = None
for response in output:
try:
parsed: t.Dict = json.loads(response)
log.debug("Pre-parsed data:\n{}", parsed)
vrf = list(parsed["vrfs"].keys())[0]
routes = parsed["vrfs"][vrf]
validated = AristaBGPTable(**routes)
bgp_table = validated.bgp_table()
if result is None:
result = bgp_table
else:
result += bgp_table
except json.JSONDecodeError as err:
log.critical("Error decoding JSON: {}", str(err))
raise ParsingError("Error parsing response data")
except KeyError as err:
log.critical("'{}' was not found in the response", str(err))
raise ParsingError("Error parsing response data")
except IndexError as err:
log.critical(str(err))
raise ParsingError("Error parsing response data")
except ValidationError as err:
log.critical(str(err))
raise ParsingError(err.errors())
return result
class BGPRoutePluginArista(OutputPlugin):
"""Coerce a Arista route table in JSON format to a standard BGP Table structure."""
__hyperglass_builtin__: bool = PrivateAttr(True)
platforms: t.Sequence[str] = ("arista_eos",)
directives: t.Sequence[str] = (
"__hyperglass_arista_eos_bgp_route_table__",
"__hyperglass_arista_eos_bgp_aspath_table__",
"__hyperglass_arista_eos_bgp_community_table__",
)
def process(self, *, output: "OutputType", query: "Query") -> "OutputType":
"""Parse Arista response if data is a string (and is therefore unparsed)."""
should_process = all(
(
isinstance(output, (list, tuple)),
query.device.platform in self.platforms,
query.device.structured_output is True,
query.device.has_directives(*self.directives),
)
)
if should_process:
return parse_arista(output)
return output

View File

@@ -0,0 +1,57 @@
"""Arista BGP Route Parsing Tests."""
# flake8: noqa
# Standard Library
from pathlib import Path
# Third Party
import pytest
# Project
from hyperglass.models.config.devices import Device
from hyperglass.models.data.bgp_route import BGPRouteTable
# Local
from .._builtin.bgp_route_arista import BGPRoutePluginArista
DEPENDS_KWARGS = {
"depends": [
"hyperglass/models/tests/test_util.py::test_check_legacy_fields",
"hyperglass/external/tests/test_rpki.py::test_rpki",
],
"scope": "session",
}
SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "arista_route.json"
def _tester(sample: str):
plugin = BGPRoutePluginArista()
device = Device(
name="Test Device",
address="127.0.0.1",
group="Test Network",
credential={"username": "", "password": ""},
platform="arista",
structured_output=True,
directives=[],
attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"},
)
# Override has_directives method for testing.
device.has_directives = lambda *x: True
query = type("Query", (), {"device": device})
result = plugin.process(output=(sample,), query=query)
assert isinstance(result, BGPRouteTable), "Invalid parsed result"
assert hasattr(result, "count"), "BGP Table missing count"
assert result.count > 0, "BGP Table count is 0"
@pytest.mark.dependency(**DEPENDS_KWARGS)
def test_arista_route_sample():
with SAMPLE.open("r") as file:
sample = file.read()
return _tester(sample)