Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anta): Added the test case to verify the BGP route origin #813

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion anta/tests/routing/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from __future__ import annotations

from ipaddress import IPv4Address, IPv4Network, IPv6Address
from typing import TYPE_CHECKING, Any, ClassVar
from typing import TYPE_CHECKING, Any, ClassVar, Literal

from pydantic import BaseModel, Field, PositiveInt, model_validator
from pydantic.v1.utils import deep_update
Expand All @@ -27,6 +27,10 @@
from typing_extensions import Self


# pylint: disable=C0302
# TODO: Refactor to reduce the number of lines in this module later
gmuloc marked this conversation as resolved.
Show resolved Hide resolved


def _add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None:
"""Add a BGP failure entry to the given `failures` dictionary.

Expand Down Expand Up @@ -1628,3 +1632,93 @@ def test(self) -> None:
self.result.is_success()
else:
self.result.is_failure(f"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\n{failures}")


class VerifyBGPRouteOrigin(AntaTest):
"""Verifies BGP route origin for the provided IPv4 Network(s).

Expected Results
----------------
* Success: The test will pass if the BGP route's origin matches expected origin type and next-hop address.
* Failure: The test will fail if the BGP route's origin does not matches with expected origin type, next-hop address or BGP route entry(s) not found.

Examples
--------
```yaml
anta.tests.routing:
bgp:
- VerifyBGPRouteOrigin:
route_entries:
- prefix: 10.100.0.128/31
vrf: default
route_paths:
- nexthop: 10.100.0.10
origin: Igp
- nexthop: 10.100.4.5
origin: Incomplete
```
"""

name = "VerifyBGPRouteOrigin"
description = "Verifies BGP route origin for the provided IPv4 Network(s)."
categories: ClassVar[list[str]] = ["bgp"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip bgp {prefix} vrf {vrf}", revision=3)]

class Input(AntaTest.Input):
"""Input model for the VerifyBGPRouteOrigin test."""

route_entries: list[BgpRoute]
"""List of BGP route(s)"""

class BgpRoute(BaseModel):
"""Model for a BGP route(s)."""

prefix: IPv4Network
"""IPv4 network address"""
vrf: str = "default"
"""Optional VRF. If not provided, it defaults to `default`."""
route_paths: list[dict[str, IPv4Address | Literal["Igp", "Egp", "Incomplete"]]]
"""A list of dictionaries represents a BGP path.
- `nexthop`: The next-hop IP address for the path.
- `origin`: The origin of the route."""
gmuloc marked this conversation as resolved.
Show resolved Hide resolved

def render(self, template: AntaTemplate) -> list[AntaCommand]:
"""Render the template for each BGP peer in the input list."""
return [template.render(prefix=route.prefix, vrf=route.vrf) for route in self.inputs.route_entries]

@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyBGPRouteOrigin."""
failures: dict[Any, Any] = {}

for command, input_entry in zip(self.instance_commands, self.inputs.route_entries):
prefix = str(command.params.prefix)
vrf = command.params.vrf
paths = input_entry.route_paths

# Verify if a BGP peer is configured with the provided vrf
if not (bgp_routes := get_value(command.json_output, f"vrfs..{vrf}..bgpRouteEntries..{prefix}..bgpRoutePaths", separator="..")):
failures[prefix] = {vrf: "Not configured"}
continue

# Iterating over each nexthop.
failure: dict[Any, Any] = {}
for path in paths:
nexthop = str(path["nexthop"])
origin = path["origin"]
if not (route_path := get_item(bgp_routes, "nextHop", nexthop)):
failure[nexthop] = "Path not found."
continue

if (actual_origin := route_path.get("routeDetail", {}).get("origin", "Not Found")) != origin:
failure[nexthop] = f"Expected `{origin}` as the origin, but found `{actual_origin}` instead."

# Updating failures.
if failure:
failures[prefix] = failure
gmuloc marked this conversation as resolved.
Show resolved Hide resolved

# Check if any failures
if not failures:
self.result.is_success()
else:
self.result.is_failure(f"Following BGP route entry(s) or nexthop path(s) not found or origin type is not correct:\n{failures}")
16 changes: 16 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,22 @@ anta.tests.routing:
vrf: default
maximum_routes: 12000
warning_limit: 10000
- VerifyBGPRouteOrigin:
route_entries:
- prefix: 10.100.0.128/31
vrf: default
route_paths:
- nexthop: 10.100.0.10
origin: Igp
- nexthop: 10.100.4.5
origin: Incomplete
- prefix: 10.100.0.130/31
vrf: default
route_paths:
- nexthop: 10.100.0.8
origin: Igp
- nexthop: 10.100.0.10
origin: Igp
ospf:
- VerifyOSPFNeighborState:
- VerifyOSPFNeighborCount:
Expand Down
241 changes: 241 additions & 0 deletions tests/units/anta_tests/routing/test_bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
VerifyBGPPeersHealth,
VerifyBGPPeerUpdateErrors,
VerifyBgpRouteMaps,
VerifyBGPRouteOrigin,
VerifyBGPSpecificPeers,
VerifyBGPTimers,
VerifyEVPNType2Route,
Expand Down Expand Up @@ -4836,4 +4837,244 @@
],
},
},
{
"name": "success",
"test": VerifyBGPRouteOrigin,
"eos_data": [
{
"vrfs": {
"default": {
"bgpRouteEntries": {
"10.100.0.128/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.10",
"routeDetail": {
"origin": "Igp",
},
},
{
"nextHop": "10.100.4.5",
"routeDetail": {
"origin": "Incomplete",
},
},
],
}
}
}
}
},
{
"vrfs": {
"MGMT": {
"bgpRouteEntries": {
"10.100.0.130/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.8",
"routeDetail": {
"origin": "Igp",
},
},
{
"nextHop": "10.100.0.10",
"routeDetail": {
"origin": "Igp",
},
},
],
}
}
}
}
},
],
"inputs": {
"route_entries": [
{
"prefix": "10.100.0.128/31",
"vrf": "default",
"route_paths": [{"nexthop": "10.100.0.10", "origin": "Igp"}, {"nexthop": "10.100.4.5", "origin": "Incomplete"}],
},
{
"prefix": "10.100.0.130/31",
"vrf": "MGMT",
"route_paths": [{"nexthop": "10.100.0.8", "origin": "Igp"}, {"nexthop": "10.100.0.10", "origin": "Igp"}],
},
]
},
"expected": {"result": "success"},
},
{
"name": "failure-origin-not-correct",
"test": VerifyBGPRouteOrigin,
"eos_data": [
{
"vrfs": {
"default": {
"bgpRouteEntries": {
"10.100.0.128/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.10",
"routeDetail": {
"origin": "Igp",
},
},
{
"nextHop": "10.100.4.5",
"routeDetail": {
"origin": "Incomplete",
},
},
],
}
}
}
}
},
{
"vrfs": {
"MGMT": {
"bgpRouteEntries": {
"10.100.0.130/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.8",
"routeDetail": {
"origin": "Igp",
},
},
{
"nextHop": "10.100.0.10",
"routeDetail": {
"origin": "Igp",
},
},
],
}
}
}
}
},
],
"inputs": {
"route_entries": [
{
"prefix": "10.100.0.128/31",
"vrf": "default",
"route_paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
},
{
"prefix": "10.100.0.130/31",
"vrf": "MGMT",
"route_paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
},
]
},
"expected": {
"result": "failure",
"messages": [
"Following BGP route entry(s) or nexthop path(s) not found or origin type is not correct:\n"
"{'10.100.0.128/31': {'10.100.0.10': 'Expected `Incomplete` as the origin, but found `Igp` instead.', "
"'10.100.4.5': 'Expected `Igp` as the origin, but found `Incomplete` instead.'}, "
"'10.100.0.130/31': {'10.100.0.8': 'Expected `Incomplete` as the origin, but found `Igp` instead.', "
"'10.100.0.10': 'Expected `Incomplete` as the origin, but found `Igp` instead.'}}"
],
},
},
{
"name": "failure-path-not-found",
"test": VerifyBGPRouteOrigin,
"eos_data": [
{
"vrfs": {
"default": {
"bgpRouteEntries": {
"10.100.0.128/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.15",
"routeDetail": {
"origin": "Igp",
},
},
],
}
}
}
}
},
{
"vrfs": {
"MGMT": {
"bgpRouteEntries": {
"10.100.0.130/31": {
"bgpRoutePaths": [
{
"nextHop": "10.100.0.15",
"routeDetail": {
"origin": "Igp",
},
},
],
}
}
}
}
},
],
"inputs": {
"route_entries": [
{
"prefix": "10.100.0.128/31",
"vrf": "default",
"route_paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
},
{
"prefix": "10.100.0.130/31",
"vrf": "MGMT",
"route_paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
},
]
},
"expected": {
"result": "failure",
"messages": [
"Following BGP route entry(s) or nexthop path(s) not found or origin type is not correct:\n"
"{'10.100.0.128/31': {'10.100.0.10': 'Path not found.', '10.100.4.5': 'Path not found.'}, "
"'10.100.0.130/31': {'10.100.0.8': 'Path not found.', '10.100.0.10': 'Path not found.'}}"
],
},
},
{
"name": "failure-route-not-found",
"test": VerifyBGPRouteOrigin,
"eos_data": [
{"vrfs": {"default": {"bgpRouteEntries": {}}}},
{"vrfs": {"MGMT": {"bgpRouteEntries": {}}}},
],
"inputs": {
"route_entries": [
{
"prefix": "10.100.0.128/31",
"vrf": "default",
"route_paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}],
},
{
"prefix": "10.100.0.130/31",
"vrf": "MGMT",
"route_paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}],
},
]
},
"expected": {
"result": "failure",
"messages": [
"Following BGP route entry(s) or nexthop path(s) not found or origin type is not correct:\n"
"{'10.100.0.128/31': {'default': 'Not configured'}, '10.100.0.130/31': {'MGMT': 'Not configured'}}"
],
},
},
]
Loading