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:
165
hyperglass/defaults/directives/arista_eos.py
Normal file
165
hyperglass/defaults/directives/arista_eos.py
Normal 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"],
|
||||
)
|
@@ -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
|
||||
|
@@ -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",
|
||||
)
|
||||
|
91
hyperglass/plugins/_builtin/bgp_route_arista.py
Normal file
91
hyperglass/plugins/_builtin/bgp_route_arista.py
Normal 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
|
57
hyperglass/plugins/tests/test_bgp_route_arista.py
Normal file
57
hyperglass/plugins/tests/test_bgp_route_arista.py
Normal 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)
|
Reference in New Issue
Block a user