From acf58f6dec6236b2c58164acab63f30a918e4947 Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Tue, 24 Sep 2024 06:56:02 -0400 Subject: [PATCH 1/2] issue_819 Added TC for Route Entries --- anta/tests/routing/generic.py | 94 +++++++++++++++- examples/tests.yaml | 7 ++ .../units/anta_tests/routing/test_generic.py | 101 +++++++++++++++++- 3 files changed, 198 insertions(+), 4 deletions(-) diff --git a/anta/tests/routing/generic.py b/anta/tests/routing/generic.py index cd9cf0d24..c6b050e12 100644 --- a/anta/tests/routing/generic.py +++ b/anta/tests/routing/generic.py @@ -8,12 +8,13 @@ from __future__ import annotations from functools import cache -from ipaddress import IPv4Address, IPv4Interface -from typing import ClassVar, Literal +from ipaddress import IPv4Address, IPv4Interface, IPv4Network +from typing import Any, ClassVar, Literal -from pydantic import model_validator +from pydantic import BaseModel, model_validator from anta.models import AntaCommand, AntaTemplate, AntaTest +from anta.tools import get_item, get_value class VerifyRoutingProtocolModel(AntaTest): @@ -178,3 +179,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}") diff --git a/examples/tests.yaml b/examples/tests.yaml index 954b5b736..1709b161a 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -494,6 +494,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: diff --git a/tests/units/anta_tests/routing/test_generic.py b/tests/units/anta_tests/routing/test_generic.py index 0ac43f3c5..d34ab4d5f 100644 --- a/tests/units/anta_tests/routing/test_generic.py +++ b/tests/units/anta_tests/routing/test_generic.py @@ -7,7 +7,7 @@ from typing import Any -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]] = [ @@ -317,4 +317,103 @@ "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"], "collect": "not-valid"}, "expected": {"result": "error", "messages": ["Inputs are not valid"]}, }, + { + "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.'}}" + ], + }, + }, ] From c51d7fd8cd19b86c68140baddb75a4e8e003803f Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Fri, 27 Sep 2024 04:31:13 -0400 Subject: [PATCH 2/2] issue_819 removed redundant unit test added during conflicts resolve --- tests/units/anta_tests/routing/test_generic.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/units/anta_tests/routing/test_generic.py b/tests/units/anta_tests/routing/test_generic.py index d2b6fe114..d4cade10c 100644 --- a/tests/units/anta_tests/routing/test_generic.py +++ b/tests/units/anta_tests/routing/test_generic.py @@ -304,13 +304,6 @@ "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": "collect-input-error", - "test": VerifyRoutingTableEntry, - "eos_data": {}, - "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"], "collect": "not-valid"}, - "expected": {"result": "error", "messages": ["Inputs are not valid"]}, - }, { "name": "success", "test": VerifyRouteEntry,