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 multiple routes with only specific nodes as next-hops #835

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
94 changes: 91 additions & 3 deletions anta/tests/routing/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
from __future__ import annotations

from functools import cache
from ipaddress import IPv4Address, IPv4Interface
from typing import TYPE_CHECKING, ClassVar, Literal
from ipaddress import IPv4Address, IPv4Interface, IPv4Network
from typing import TYPE_CHECKING, Any, ClassVar, Literal

from pydantic import model_validator
from pydantic import BaseModel, model_validator

from anta.custom_types import PositiveInteger
from anta.models import AntaCommand, AntaTemplate, AntaTest
from anta.tools import get_item, get_value

if TYPE_CHECKING:
import sys
Expand Down Expand Up @@ -187,3 +188,90 @@ def test(self) -> None:
self.result.is_success()
else:
self.result.is_failure(f"The following route(s) are missing from the routing table of VRF {self.inputs.vrf}: {missing_routes}")


class VerifyRouteEntry(AntaTest):
"""Verifies the route entries of given IPv4 network(s).

Supports `strict: True` to verify that only the specified nexthops by which routes are learned, requiring an exact match.

Expected Results
----------------
* Success: The test will pass if the route entry with given nexthop(s) present for given network(s).
* Failure: The test will fail if the routes not found or route entry with given nexthop(s) not present for given network(s).

Examples
--------
```yaml
anta.tests.routing:
generic:
- VerifyRouteEntry:
route_entries:
- prefix: 10.10.0.1/32
vrf: default
nexthops:
- 10.100.0.8
- 10.100.0.10
```
"""

name = "VerifyRouteEntry"
description = "Verifies the route entry(s) for the provided IPv4 Network(s)."
categories: ClassVar[list[str]] = ["routing"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip route {prefix}", revision=4)]

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

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

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

prefix: IPv4Network
"""IPv4 network address"""
vrf: str = "default"
"""Optional VRF. If not provided, it defaults to `default`."""
nexthops: list[IPv4Address]
"""A list of the next-hop IP address for the path."""
strict: bool = False
"""If True, requires exact matching of provided nexthop(s). Defaults to False."""

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

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

for command, input_entry in zip(self.instance_commands, self.inputs.route_entries):
prefix = str(input_entry.prefix)
vrf = input_entry.vrf
nexthops = input_entry.nexthops
strict = input_entry.strict

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

# Verify the nexthop addresses.
actual_nexthops = [route.get("nexthopAddr") for route in routes]

if strict and len(nexthops) != len(actual_nexthops):
exp_nexthops = ", ".join([str(nexthop) for nexthop in nexthops])
failures[prefix] = {vrf: f"Expected only `{exp_nexthops}` nexthops should be listed but found `{', '.join(actual_nexthops)}` instead."}

else:
nexthop_not_ok = [str(nexthop) for nexthop in nexthops if not get_item(routes, "nexthopAddr", str(nexthop))]
if nexthop_not_ok:
failures[prefix] = {vrf: nexthop_not_ok}

# Check if any failures
if not failures:
self.result.is_success()
else:
self.result.is_failure(f"Following route entry(s) or nexthop path(s) not found or not correct:\n{failures}")
7 changes: 7 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,13 @@ anta.tests.routing:
routes:
- 10.1.0.1
- 10.1.0.2
- VerifyRouteEntry:
route_entries:
- prefix: 10.10.0.1/32
vrf: default
nexthops:
- 10.100.0.8
- 10.100.0.10
bgp:
- VerifyBGPPeerCount:
address_families:
Expand Down
101 changes: 100 additions & 1 deletion tests/units/anta_tests/routing/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
from pydantic import ValidationError

from anta.tests.routing.generic import VerifyRoutingProtocolModel, VerifyRoutingTableEntry, VerifyRoutingTableSize
from anta.tests.routing.generic import VerifyRouteEntry, VerifyRoutingProtocolModel, VerifyRoutingTableEntry, VerifyRoutingTableSize
from tests.units.anta_tests import test

DATA: list[dict[str, Any]] = [
Expand Down Expand Up @@ -304,6 +304,105 @@
"inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"], "collect": "all"},
"expected": {"result": "failure", "messages": ["The following route(s) are missing from the routing table of VRF default: ['10.1.0.2']"]},
},
{
"name": "success",
"test": VerifyRouteEntry,
"eos_data": [
{
"vrfs": {
"default": {
"routes": {
"10.10.0.1/32": {
"vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}],
}
}
}
}
},
{
"vrfs": {
"MGMT": {
"routes": {
"10.100.0.128/31": {
"vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}],
}
}
}
}
},
],
"inputs": {
"route_entries": [
{"prefix": "10.10.0.1/32", "vrf": "default", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]},
{"prefix": "10.100.0.128/31", "vrf": "MGMT", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]},
]
},
"expected": {"result": "success"},
},
{
"name": "failure-not-configured",
"test": VerifyRouteEntry,
"eos_data": [
{"vrfs": {"default": {"routes": {}}}},
{"vrfs": {"MGMT": {"routes": {}}}},
],
"inputs": {
"route_entries": [
{"prefix": "10.10.0.1/32", "vrf": "default", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]},
{"prefix": "10.100.0.128/31", "vrf": "MGMT", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]},
]
},
"expected": {
"result": "failure",
"messages": [
"Following route entry(s) or nexthop path(s) not found or not correct:\n"
"{'10.10.0.1/32': {'default': 'Not configured'}, '10.100.0.128/31': {'MGMT': 'Not configured'}}"
],
},
},
{
"name": "failure-strict-failed",
"test": VerifyRouteEntry,
"eos_data": [
{
"vrfs": {
"default": {
"routes": {
"10.10.0.1/32": {
"vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}],
}
}
}
}
},
{
"vrfs": {
"MGMT": {
"routes": {
"10.100.0.128/31": {
"vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}],
}
}
}
}
},
],
"inputs": {
"route_entries": [
{"prefix": "10.10.0.1/32", "vrf": "default", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10", "10.100.0.11"]},
{"prefix": "10.100.0.128/31", "vrf": "MGMT", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10", "10.100.0.11"]},
]
},
"expected": {
"result": "failure",
"messages": [
"Following route entry(s) or nexthop path(s) not found or not correct:\n"
"{'10.10.0.1/32': {'default': 'Expected only `10.100.0.8, 10.100.0.10, 10.100.0.11` nexthops should be listed but "
"found `10.100.0.8, 10.100.0.10` instead.'}, '10.100.0.128/31': {'MGMT': 'Expected only `10.100.0.8, 10.100.0.10, "
"10.100.0.11` nexthops should be listed but found `10.100.0.8, 10.100.0.10` instead.'}}"
],
},
},
]


Expand Down
Loading