diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py index 57c821740..45d25868c 100644 --- a/anta/input_models/routing/bgp.py +++ b/anta/input_models/routing/bgp.py @@ -6,7 +6,7 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv4Network, IPv6Address -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from warnings import warn from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator @@ -207,3 +207,50 @@ class VxlanEndpoint(BaseModel): def __str__(self) -> str: """Return a human-readable string representation of the VxlanEndpoint for reporting.""" return f"Address: {self.address} VNI: {self.vni}" + + +class BgpRoute(BaseModel): + """Model representing BGP routes. + + Only IPv4 prefixes are supported for now. + """ + + model_config = ConfigDict(extra="forbid") + prefix: IPv4Network + """The IPv4 network address.""" + vrf: str = "default" + """Optional VRF for the BGP peer. Defaults to `default`.""" + paths: list[RoutePath] | None = None + """A list of paths for the BGP route. Required field in the `VerifyBGPRouteOrigin` test.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the BgpRoute for reporting. + + Examples + -------- + - Prefix: 192.168.66.100/24 VRF: default + """ + return f"Prefix: {self.prefix} VRF: {self.vrf}" + + +class RoutePath(BaseModel): + """Model representing a BGP route path.""" + + model_config = ConfigDict(extra="forbid") + nexthop: IPv4Address + """The next-hop IPv4 address for the path.""" + origin: Literal["Igp", "Egp", "Incomplete"] + """The origin type of the BGP route path: + - 'Igp': Indicates the route originated from an interior gateway protocol (IGP). + - 'Egp': Indicates the route originated from an exterior gateway protocol (EGP). + - 'Incomplete': Indicates the origin is unknown or learned by other means. + """ + + def __str__(self) -> str: + """Return a human-readable string representation of the RoutePath for reporting. + + Examples + -------- + - Nexthop: 192.168.66.101 Origin: Igp + """ + return f"Nexthop: {self.nexthop}" diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index 2a140ddb2..081e23445 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -11,7 +11,7 @@ from pydantic import field_validator -from anta.input_models.routing.bgp import BgpAddressFamily, BgpAfi, BgpNeighbor, BgpPeer, VxlanEndpoint +from anta.input_models.routing.bgp import BgpAddressFamily, BgpAfi, BgpNeighbor, BgpPeer, BgpRoute, VxlanEndpoint from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import format_data, get_item, get_value @@ -1272,3 +1272,73 @@ def test(self) -> None: # Verify warning limit if given. if warning_limit and (actual_warning_limit := peer_data.get("totalRoutesWarnLimit", "Not Found")) != warning_limit: self.result.is_failure(f"{peer} - Maximum route warning limit mismatch - Expected: {warning_limit}, Actual: {actual_warning_limit}") + + +class VerifyBGPRouteOrigin(AntaTest): + """Verifies BGP route origin. + + This test performs the following checks for each specified bgp route entry: + 1. Checks whether the specified BGP route entry exists. + 2. Confirms that each path for the route entry exists and corresponds to the next-hop address. + 3. Verifies that the origin type of the BGP route matches the expected type. + + Expected Results + ---------------- + * Success: The test will pass if: + - The BGP route entries exist for specified prefixes. + - Every path exists and corresponds to the specified next-hop address. + - The origin type of the BGP route matches the expected type. + * Failure: The test will fail if: + - The BGP route entries does not exist for specified prefixes. + - Any Path does not exists and corresponds to the specified next-hop address. + - The origin type does not match the configured value. + + Examples + -------- + ```yaml + anta.tests.routing: + bgp: + - VerifyBGPRouteOrigin: + route_entries: + - prefix: 10.100.0.128/31 + vrf: default + paths: + - nexthop: 10.100.0.10 + origin: Igp + - nexthop: 10.100.4.5 + origin: Incomplete + ``` + """ + + categories: ClassVar[list[str]] = ["bgp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip bgp detail vrf all", revision=3)] + + class Input(AntaTest.Input): + """Input model for the VerifyBGPRouteOrigin test.""" + + route_entries: list[BgpRoute] + """List of BGP route(s)""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyBGPRouteOrigin.""" + self.result.is_success() + + for route in self.inputs.route_entries: + # Verify if a BGP routes are present with the provided vrf + if not ( + bgp_routes := get_value(self.instance_commands[0].json_output, f"vrfs..{route.vrf}..bgpRouteEntries..{route.prefix}..bgpRoutePaths", separator="..") + ): + self.result.is_failure(f"{route} - routes not found") + continue + + # Iterating over each path. + for path in route.paths: + nexthop = str(path.nexthop) + origin = path.origin + if not (route_path := get_item(bgp_routes, "nextHop", nexthop)): + self.result.is_failure(f"{route} {path} - path not found") + continue + + if (actual_origin := route_path.get("routeType", {}).get("origin", "Not Found")) != origin: + self.result.is_failure(f"{route} {path} - Origin mismatch - Expected: {origin} Actual: {actual_origin}") diff --git a/examples/tests.yaml b/examples/tests.yaml index e22acf49a..b43b03f69 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -463,6 +463,16 @@ anta.tests.routing.bgp: safi: "unicast" vrf: "DEV" check_tcp_queues: false + - VerifyBGPRouteOrigin: + # Verifies BGP route origin. + route_entries: + - prefix: 10.100.0.128/31 + vrf: default + paths: + - nexthop: 10.100.0.10 + origin: Igp + - nexthop: 10.100.4.5 + origin: Incomplete - VerifyBGPSpecificPeers: # Verifies the health of specific BGP peer(s) for given address families. address_families: diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py index 59a67191c..f5654f929 100644 --- a/tests/units/anta_tests/routing/test_bgp.py +++ b/tests/units/anta_tests/routing/test_bgp.py @@ -24,6 +24,7 @@ VerifyBGPPeersHealth, VerifyBGPPeerUpdateErrors, VerifyBgpRouteMaps, + VerifyBGPRouteOrigin, VerifyBGPSpecificPeers, VerifyBGPTimers, VerifyEVPNType2Route, @@ -3847,4 +3848,228 @@ def test_check_bgp_neighbor_capability(input_dict: dict[str, bool], expected: bo ], }, }, + { + "name": "success", + "test": VerifyBGPRouteOrigin, + "eos_data": [ + { + "vrfs": { + "default": { + "bgpRouteEntries": { + "10.100.0.128/31": { + "bgpRoutePaths": [ + { + "nextHop": "10.100.0.10", + "routeType": { + "origin": "Igp", + }, + }, + { + "nextHop": "10.100.4.5", + "routeType": { + "origin": "Incomplete", + }, + }, + ], + } + } + }, + "MGMT": { + "bgpRouteEntries": { + "10.100.0.130/31": { + "bgpRoutePaths": [ + { + "nextHop": "10.100.0.8", + "routeType": { + "origin": "Igp", + }, + }, + { + "nextHop": "10.100.0.10", + "routeType": { + "origin": "Igp", + }, + }, + ], + } + } + }, + } + }, + ], + "inputs": { + "route_entries": [ + { + "prefix": "10.100.0.128/31", + "vrf": "default", + "paths": [{"nexthop": "10.100.0.10", "origin": "Igp"}, {"nexthop": "10.100.4.5", "origin": "Incomplete"}], + }, + { + "prefix": "10.100.0.130/31", + "vrf": "MGMT", + "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", + "routeType": { + "origin": "Igp", + }, + }, + { + "nextHop": "10.100.4.5", + "routeType": { + "origin": "Incomplete", + }, + }, + ], + } + } + }, + "MGMT": { + "bgpRouteEntries": { + "10.100.0.130/31": { + "bgpRoutePaths": [ + { + "nextHop": "10.100.0.8", + "routeType": { + "origin": "Igp", + }, + }, + { + "nextHop": "10.100.0.10", + "routeType": { + "origin": "Igp", + }, + }, + ], + } + } + }, + } + }, + ], + "inputs": { + "route_entries": [ + { + "prefix": "10.100.0.128/31", + "vrf": "default", + "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}], + }, + { + "prefix": "10.100.0.130/31", + "vrf": "MGMT", + "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}], + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Prefix: 10.100.0.128/31 VRF: default Nexthop: 10.100.0.10 - Origin mismatch - Expected: Incomplete Actual: Igp", + "Prefix: 10.100.0.128/31 VRF: default Nexthop: 10.100.4.5 - Origin mismatch - Expected: Igp Actual: Incomplete", + "Prefix: 10.100.0.130/31 VRF: MGMT Nexthop: 10.100.0.8 - Origin mismatch - Expected: Incomplete Actual: Igp", + "Prefix: 10.100.0.130/31 VRF: MGMT Nexthop: 10.100.0.10 - Origin mismatch - Expected: Incomplete Actual: Igp", + ], + }, + }, + { + "name": "failure-path-not-found", + "test": VerifyBGPRouteOrigin, + "eos_data": [ + { + "vrfs": { + "default": { + "bgpRouteEntries": { + "10.100.0.128/31": { + "bgpRoutePaths": [ + { + "nextHop": "10.100.0.15", + "routeType": { + "origin": "Igp", + }, + }, + ], + } + } + }, + "MGMT": { + "bgpRouteEntries": { + "10.100.0.130/31": { + "bgpRoutePaths": [ + { + "nextHop": "10.100.0.15", + "routeType": { + "origin": "Igp", + }, + }, + ], + } + } + }, + } + }, + ], + "inputs": { + "route_entries": [ + { + "prefix": "10.100.0.128/31", + "vrf": "default", + "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}], + }, + { + "prefix": "10.100.0.130/31", + "vrf": "MGMT", + "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}], + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Prefix: 10.100.0.128/31 VRF: default Nexthop: 10.100.0.10 - path not found", + "Prefix: 10.100.0.128/31 VRF: default Nexthop: 10.100.4.5 - path not found", + "Prefix: 10.100.0.130/31 VRF: MGMT Nexthop: 10.100.0.8 - path not found", + "Prefix: 10.100.0.130/31 VRF: MGMT Nexthop: 10.100.0.10 - path not found", + ], + }, + }, + { + "name": "failure-route-not-found", + "test": VerifyBGPRouteOrigin, + "eos_data": [ + {"vrfs": {"default": {"bgpRouteEntries": {}}, "MGMT": {"bgpRouteEntries": {}}}}, + ], + "inputs": { + "route_entries": [ + { + "prefix": "10.100.0.128/31", + "vrf": "default", + "paths": [{"nexthop": "10.100.0.10", "origin": "Incomplete"}, {"nexthop": "10.100.4.5", "origin": "Igp"}], + }, + { + "prefix": "10.100.0.130/31", + "vrf": "MGMT", + "paths": [{"nexthop": "10.100.0.8", "origin": "Incomplete"}, {"nexthop": "10.100.0.10", "origin": "Incomplete"}], + }, + ] + }, + "expected": { + "result": "failure", + "messages": ["Prefix: 10.100.0.128/31 VRF: default - routes not found", "Prefix: 10.100.0.130/31 VRF: MGMT - routes not found"], + }, + }, ]