diff --git a/anta/custom_types.py b/anta/custom_types.py index c29811826..297f1f5a8 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -208,3 +208,33 @@ def validate_regex(value: str) -> str: SnmpErrorCounter = Literal[ "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" ] + +IPv4RouteType = Literal[ + "connected", + "static", + "kernel", + "OSPF", + "OSPF inter area", + "OSPF external type 1", + "OSPF external type 2", + "OSPF NSSA external type 1", + "OSPF NSSA external type2", + "Other BGP Routes", + "iBGP", + "eBGP", + "RIP", + "IS-IS level 1", + "IS-IS level 2", + "OSPFv3", + "BGP Aggregate", + "OSPF Summary", + "Nexthop Group Static Route", + "VXLAN Control Service", + "Martian", + "DHCP client installed default route", + "Dynamic Policy Route", + "VRF Leaked", + "gRIBI", + "Route Cache Route", + "CBF Leaked Route", +] diff --git a/anta/input_models/routing/generic.py b/anta/input_models/routing/generic.py new file mode 100644 index 000000000..41c78a132 --- /dev/null +++ b/anta/input_models/routing/generic.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023-2024 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. +"""Module containing input models for generic routing tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Network + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import IPv4RouteType + + +class IPv4Routes(BaseModel): + """Model for a list of IPV4 route entries.""" + + model_config = ConfigDict(extra="forbid") + prefix: IPv4Network + """The IPV4 network to validate the route type.""" + vrf: str = "default" + """VRF context. Defaults to `default` VRF.""" + route_type: IPv4RouteType + """List of IPV4 Route type to validate the valid rout type.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the IPv4RouteType for reporting.""" + return f"Prefix: {self.prefix} VRF: {self.vrf}" diff --git a/anta/tests/routing/generic.py b/anta/tests/routing/generic.py index fb9e3175d..7b916a3bc 100644 --- a/anta/tests/routing/generic.py +++ b/anta/tests/routing/generic.py @@ -14,7 +14,9 @@ from pydantic import model_validator from anta.custom_types import PositiveInteger +from anta.input_models.routing.generic import IPv4Routes from anta.models import AntaCommand, AntaTemplate, AntaTest +from anta.tools import get_value if TYPE_CHECKING: import sys @@ -181,3 +183,76 @@ 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 VerifyIPv4RouteType(AntaTest): + """Verifies the route-type of the IPv4 prefixes. + + This test performs the following checks for each IPv4 route: + 1. Verifies that the specified VRF is configured. + 2. Verifies that the specified IPv4 route is exists in the configuration. + 3. Verifies that the the specified IPv4 route is of the expected type. + + Expected Results + ---------------- + * Success: If all of the following conditions are met: + - All the specified VRFs are configured. + - All the specified IPv4 routes are found. + - All the specified IPv4 routes are of the expected type. + * Failure: If any of the following occur: + - A specified VRF is not configured. + - A specified IPv4 route is not found. + - Any specified IPv4 route is not of the expected type. + + Examples + -------- + ```yaml + anta.tests.routing: + generic: + - VerifyIPv4RouteType: + routes_entries: + - prefix: 10.10.0.1/32 + vrf: default + route_type: eBGP + - prefix: 10.100.0.12/31 + vrf: default + route_type: connected + - prefix: 10.100.1.5/32 + vrf: default + route_type: iBGP + ``` + """ + + categories: ClassVar[list[str]] = ["routing"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route vrf all", revision=4)] + + class Input(AntaTest.Input): + """Input model for the VerifyIPv4RouteType test.""" + + routes_entries: list[IPv4Routes] + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyIPv4RouteType.""" + self.result.is_success() + output = self.instance_commands[0].json_output + + # Iterating over the all routes entries mentioned in the inputs. + for entry in self.inputs.routes_entries: + prefix = str(entry.prefix) + vrf = entry.vrf + expected_route_type = entry.route_type + + # Verifying that on device, expected VRF is configured. + if (routes_details := get_value(output, f"vrfs.{vrf}.routes")) is None: + self.result.is_failure(f"{entry} - VRF not configured") + continue + + # Verifying that the expected IPv4 route is present or not on the device + if (route_data := routes_details.get(prefix)) is None: + self.result.is_failure(f"{entry} - Route not found") + continue + + # Verifying that the specified IPv4 routes are of the expected type. + if expected_route_type != (actual_route_type := route_data.get("routeType")): + self.result.is_failure(f"{entry} - Incorrect route type - Expected: {expected_route_type} Actual: {actual_route_type}") diff --git a/docs/api/tests.routing.generic.md b/docs/api/tests.routing.generic.md index 1c4c39d8f..bbc89040a 100644 --- a/docs/api/tests.routing.generic.md +++ b/docs/api/tests.routing.generic.md @@ -7,7 +7,10 @@ anta_title: ANTA catalog for generic routing tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.routing.generic + options: show_root_heading: false show_root_toc_entry: false @@ -18,3 +21,16 @@ anta_title: ANTA catalog for generic routing tests filters: - "!test" - "!render" + +# Input models + +::: anta.input_models.routing.generic + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + merge_init_into_class: false + anta_hide_test_module_description: true + show_labels: true + filters: ["!^__str__"] diff --git a/examples/tests.yaml b/examples/tests.yaml index 86ed117a3..58d34c42a 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -503,6 +503,18 @@ anta.tests.routing.bgp: - address: aac1.ab5d.b41e vni: 10010 anta.tests.routing.generic: + - VerifyIPv4RouteType: + # Verifies the route-type of the IPv4 prefixes. + routes_entries: + - prefix: 10.10.0.1/32 + vrf: default + route_type: eBGP + - prefix: 10.100.0.12/31 + vrf: default + route_type: connected + - prefix: 10.100.1.5/32 + vrf: default + route_type: iBGP - VerifyRoutingProtocolModel: # Verifies the configured routing protocol model. model: multi-agent diff --git a/tests/units/anta_tests/routing/test_generic.py b/tests/units/anta_tests/routing/test_generic.py index 20f83b92b..4e9d654dc 100644 --- a/tests/units/anta_tests/routing/test_generic.py +++ b/tests/units/anta_tests/routing/test_generic.py @@ -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 VerifyIPv4RouteType, VerifyRoutingProtocolModel, VerifyRoutingTableEntry, VerifyRoutingTableSize from tests.units.anta_tests import test DATA: list[dict[str, Any]] = [ @@ -304,6 +304,50 @@ "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-valid-route-type", + "test": VerifyIPv4RouteType, + "eos_data": [ + { + "vrfs": { + "default": {"routes": {"10.10.0.1/32": {"routeType": "eBGP"}, "10.100.0.12/31": {"routeType": "connected"}}}, + "MGMT": {"routes": {"10.100.1.5/32": {"routeType": "iBGP"}}}, + } + } + ], + "inputs": { + "routes_entries": [ + {"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "eBGP"}, + {"vrf": "default", "prefix": "10.100.0.12/31", "route_type": "connected"}, + {"vrf": "MGMT", "prefix": "10.100.1.5/32", "route_type": "iBGP"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-route-not-found", + "test": VerifyIPv4RouteType, + "eos_data": [{"vrfs": {"default": {"routes": {}}}}], + "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "eBGP"}]}, + "expected": {"result": "failure", "messages": ["Prefix: 10.10.0.1/32 VRF: default - Route not found"]}, + }, + { + "name": "failure-invalid-route-type", + "test": VerifyIPv4RouteType, + "eos_data": [{"vrfs": {"default": {"routes": {"10.10.0.1/32": {"routeType": "eBGP"}}}}}], + "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "iBGP"}]}, + "expected": { + "result": "failure", + "messages": ["Prefix: 10.10.0.1/32 VRF: default - Incorrect route type - Expected: iBGP Actual: eBGP"], + }, + }, + { + "name": "failure-vrf-not-configured", + "test": VerifyIPv4RouteType, + "eos_data": [{"vrfs": {}}], + "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "eBGP"}]}, + "expected": {"result": "failure", "messages": ["Prefix: 10.10.0.1/32 VRF: default - VRF not configured"]}, + }, ]