Repository: aristanetworks/anta (python)

Issue:
feat(anta.tests): Add a knob to VerifyBGPExchangedRoutes to check if the route is valid only
Today we are checking that each provided prefix is active and valid but that might be too strict in ECMP scenarios where the prefix would be valid and ecmp contributor.

Pull request description:
feat(anta.tests): Add a knob to VerifyBGPExchangedRoutes to check if the route is valid only 
# Description

We are checking that each provided prefix is active and valid but that might be too strict in ECMP scenarios where the prefix would be valid and ecmp contributor.
Added knob to check if the route is valid only.

Fixes #1172 

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have run pre-commit for code linting and typing (`pre-commit run`)
- [ ] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes (`tox -e testenv`)


Patch:
diff --git a/anta/custom_types.py b/anta/custom_types.py
index bacb641d..9e5c085f 100644
--- a/anta/custom_types.py
+++ b/anta/custom_types.py
@@ -419,3 +419,4 @@ ReloadCause = Annotated[
     Literal["System reloaded due to Zero Touch Provisioning", "Reload requested by the user.", "Reload requested after FPGA upgrade", "USER", "FPGA", "ZTP"],
     BeforeValidator(convert_reload_cause),
 ]
+BgpCommunity = Literal["standard", "extended", "large"]
diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py
index 09def7fe..840edfcd 100644
--- a/anta/input_models/routing/bgp.py
+++ b/anta/input_models/routing/bgp.py
@@ -12,7 +12,7 @@ from warnings import warn
 from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator
 from pydantic_extra_types.mac_address import MacAddress
 
-from anta.custom_types import Afi, BgpDropStats, BgpUpdateError, MultiProtocolCaps, RedistributedAfiSafi, RedistributedProtocol, Safi, Vni
+from anta.custom_types import Afi, BgpCommunity, BgpDropStats, BgpUpdateError, MultiProtocolCaps, RedistributedAfiSafi, RedistributedProtocol, Safi, Vni
 
 if TYPE_CHECKING:
     import sys
@@ -196,6 +196,10 @@ class BgpPeer(BaseModel):
     """The Time-To-Live (TTL). Required field in the `VerifyBGPPeerTtlMultiHops` test."""
     max_ttl_hops: int | None = Field(default=None, ge=1, le=255)
     """The Max TTL hops. Required field in the `VerifyBGPPeerTtlMultiHops` test."""
+    advertised_communities: list[BgpCommunity] = Field(default=["standard", "extended", "large"])
+    """List of advertised communities to be verified.
+
+    Optional field in the `VerifyBGPAdvCommunities` test. If not provided, the test will verify that all communities are advertised."""
 
     def __str__(self) -> str:
         """Return a human-readable string representation of the BgpPeer for reporting."""


Test patch:
diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py
index da4d757c..3ffae5ba 100644
--- a/anta/tests/routing/bgp.py
+++ b/anta/tests/routing/bgp.py
@@ -444,16 +444,17 @@ class VerifyBGPExchangedRoutes(AntaTest):
 
       For each advertised and received route:
         - Confirms that the route exists in the BGP route table.
-        - Verifies that the route is in an 'active' and 'valid' state.
+        - If `check_active` input flag is True, verifies that the route is 'valid' and 'active'.
+        - If `check_active` input flag is False, verifies that the route is 'valid'.
 
     Expected Results
     ----------------
     * Success: If all of the following conditions are met:
         - All specified advertised/received routes are found in the BGP route table.
-        - All routes are in both 'active' and 'valid' states.
+        - All routes are 'active' and 'valid' or 'valid' only per the `check_active` input flag.
     * Failure: If any of the following occur:
         - An advertised/received route is not found in the BGP route table.
-        - Any route is not in an 'active' or 'valid' state.
+        - Any route is not 'active' and 'valid' or 'valid' only per `check_active` input flag.
 
     Examples
     --------
@@ -461,6 +462,7 @@ class VerifyBGPExchangedRoutes(AntaTest):
     anta.tests.routing:
       bgp:
         - VerifyBGPExchangedRoutes:
+            check_active: True
             bgp_peers:
               - peer_address: 172.30.255.5
                 vrf: default
@@ -485,6 +487,8 @@ class VerifyBGPExchangedRoutes(AntaTest):
     class Input(AntaTest.Input):
         """Input model for the VerifyBGPExchangedRoutes test."""
 
+        check_active: bool = True
+        """Flag to check if the provided prefixes must be active and valid. If False, checks if the prefixes are valid only. """
         bgp_peers: list[BgpPeer]
         """List of BGP IPv4 peers."""
         BgpNeighbor: ClassVar[type[BgpNeighbor]] = BgpNeighbor
@@ -503,7 +507,7 @@ class VerifyBGPExchangedRoutes(AntaTest):
         """Render the template for each BGP peer in the input list."""
         return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]
 
-    def _validate_bgp_route_paths(self, peer: str, route_type: str, route: str, entries: dict[str, Any]) -> str | None:
+    def _validate_bgp_route_paths(self, peer: str, route_type: str, route: str, entries: dict[str, Any], *, active_flag: bool = True) -> str | None:
         """Validate the BGP route paths."""
         # Check if the route is found
         if route in entries:
@@ -511,8 +515,11 @@ class VerifyBGPExchangedRoutes(AntaTest):
             route_paths = entries[route]["bgpRoutePaths"][0]["routeType"]
             is_active = route_paths["active"]
             is_valid = route_paths["valid"]
-            if not is_active or not is_valid:
-                return f"{peer} {route_type} route: {route} - Valid: {is_valid} Active: {is_active}"
+            if active_flag:
+                if not is_active or not is_valid:
+                    return f"{peer} {route_type} route: {route} - Valid: {is_valid} Active: {is_active}"
+            elif not is_valid:
+                return f"{peer} {route_type} route: {route} - Valid: {is_valid}"
             return None
 
         return f"{peer} {route_type} route: {route} - Not found"
@@ -544,8 +551,8 @@ class VerifyBGPExchangedRoutes(AntaTest):
 
                 entries = command_output[route_type]
                 for route in routes:
-                    # Check if the route is found. If yes then checks the route is active and valid
-                    failure_msg = self._validate_bgp_route_paths(str(peer), route_type, str(route), entries)
+                    # Check if the route is found. If yes then checks the route is active/valid
+                    failure_msg = self._validate_bgp_route_paths(str(peer), route_type, str(route), entries, active_flag=self.inputs.check_active)
                     if failure_msg:
                         self.result.is_failure(failure_msg)
 
@@ -923,24 +930,21 @@ class VerifyEVPNType2Route(AntaTest):
 
 
 class VerifyBGPAdvCommunities(AntaTest):
-    """Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s).
+    """Verifies the advertised communities for BGP IPv4 peer(s).
 
     This test performs the following checks for each specified peer:
 
       1. Verifies that the peer is found in its VRF in the BGP configuration.
-      2. Validates that all required community types are advertised:
-        - Standard communities
-        - Extended communities
-        - Large communities
+      2. Validates that given community types are advertised. If not provided, validates that all communities (standard, extended, large) are advertised.
 
     Expected Results
     ----------------
     * Success: If all of the following conditions are met:
         - All specified peers are found in the BGP configuration.
-        - Each peer advertises standard, extended and large communities.
+        - Each peer advertises the given community types.
     * Failure: If any of the following occur:
         - A specified peer is not found in the BGP configuration.
-        - A peer does not advertise standard, extended or large communities.
+        - A peer does not advertise any of the given community types.
 
     Examples
     --------
@@ -953,6 +957,7 @@ class VerifyBGPAdvCommunities(AntaTest):
                 vrf: default
               - peer_address: 172.30.11.21
                 vrf: default
+                advertised_communities: ["standard", "extended"]
     ```
     """
 
@@ -983,7 +988,7 @@ class VerifyBGPAdvCommunities(AntaTest):
                 continue
 
             # Check BGP peer advertised communities
-            if not all(get_value(peer_data, f"advertisedCommunities.{community}") is True for community in ["standard", "extended", "large"]):
+            if not all(get_value(peer_data, f"advertisedCommunities.{community}") is True for community in peer.advertised_communities):
                 self.result.is_failure(f"{peer} - {format_data(peer_data['advertisedCommunities'])}")
 
 
diff --git a/examples/tests.yaml b/examples/tests.yaml
index a0cb8c0e..e747fdd6 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -387,14 +387,16 @@ anta.tests.ptp:
       # Verifies the PTP interfaces state.
 anta.tests.routing.bgp:
   - VerifyBGPAdvCommunities:
-      # Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s).
+      # Verifies the advertised communities for BGP IPv4 peer(s).
       bgp_peers:
         - peer_address: 172.30.11.17
           vrf: default
         - peer_address: 172.30.11.21
           vrf: default
+          advertised_communities: ["standard", "extended"]
   - VerifyBGPExchangedRoutes:
       # Verifies the advertised and received routes of BGP IPv4 peer(s).
+      check_active: True
       bgp_peers:
         - peer_address: 172.30.255.5
           vrf: default
diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py
index e26d452a..2e4d8559 100644
--- a/tests/units/anta_tests/routing/test_bgp.py
+++ b/tests/units/anta_tests/routing/test_bgp.py
@@ -1323,6 +1323,80 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {"result": "success"},
     },
+    {
+        "name": "success-check-active-false",
+        "test": VerifyBGPExchangedRoutes,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "192.0.254.3/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": True,
+                                            "active": True,
+                                        },
+                                    }
+                                ]
+                            },
+                            "192.0.254.5/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": True,
+                                            "active": True,
+                                        },
+                                    }
+                                ]
+                            },
+                        },
+                    }
+                }
+            },
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "192.0.254.3/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": True,
+                                            "active": True,
+                                        },
+                                    }
+                                ],
+                            },
+                            "192.0.255.4/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": True,
+                                            "active": True,
+                                        },
+                                    }
+                                ],
+                            },
+                        },
+                    }
+                }
+            },
+        ],
+        "inputs": {
+            "check_active": False,
+            "bgp_peers": [
+                {
+                    "peer_address": "172.30.11.1",
+                    "vrf": "default",
+                    "advertised_routes": ["192.0.254.5/32", "192.0.254.3/32"],
+                    "received_routes": ["192.0.254.3/32", "192.0.255.4/32"],
+                },
+            ],
+        },
+        "expected": {"result": "success"},
+    },
     {
         "name": "success-advertised-route-validation-only",
         "test": VerifyBGPExchangedRoutes,
@@ -1815,6 +1889,87 @@ DATA: list[dict[str, Any]] = [
             ],
         },
     },
+    {
+        "name": "failure-check-active-false",
+        "test": VerifyBGPExchangedRoutes,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "192.0.254.3/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": False,
+                                            "active": False,
+                                        },
+                                    }
+                                ]
+                            },
+                            "192.0.254.5/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": False,
+                                            "active": True,
+                                        },
+                                    }
+                                ]
+                            },
+                        },
+                    }
+                }
+            },
+            {
+                "vrfs": {
+                    "default": {
+                        "bgpRouteEntries": {
+                            "192.0.254.3/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": True,
+                                            "active": True,
+                                        },
+                                    }
+                                ],
+                            },
+                            "192.0.255.4/32": {
+                                "bgpRoutePaths": [
+                                    {
+                                        "routeType": {
+                                            "valid": False,
+                                            "active": True,
+                                        },
+                                    }
+                                ],
+                            },
+                        },
+                    }
+                }
+            },
+        ],
+        "inputs": {
+            "check_active": False,
+            "bgp_peers": [
+                {
+                    "peer_address": "172.30.11.1",
+                    "vrf": "default",
+                    "advertised_routes": ["192.0.254.5/32", "192.0.254.3/32"],
+                    "received_routes": ["192.0.254.3/32", "192.0.255.4/32"],
+                },
+            ],
+        },
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.5/32 - Valid: False",
+                "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.3/32 - Valid: False",
+                "Peer: 172.30.11.1 VRF: default Received route: 192.0.255.4/32 - Valid: False",
+            ],
+        },
+    },
     {
         "name": "success",
         "test": VerifyBGPPeerMPCaps,
@@ -3231,6 +3386,47 @@ DATA: list[dict[str, Any]] = [
         },
         "expected": {"result": "success"},
     },
+    {
+        "name": "success-specified-communities",
+        "test": VerifyBGPAdvCommunities,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "peerList": [
+                            {
+                                "peerAddress": "172.30.11.1",
+                                "advertisedCommunities": {
+                                    "standard": True,
+                                    "extended": True,
+                                    "large": False,
+                                },
+                            }
+                        ]
+                    },
+                    "MGMT": {
+                        "peerList": [
+                            {
+                                "peerAddress": "172.30.11.10",
+                                "advertisedCommunities": {
+                                    "standard": False,
+                                    "extended": True,
+                                    "large": False,
+                                },
+                            }
+                        ]
+                    },
+                }
+            }
+        ],
+        "inputs": {
+            "bgp_peers": [
+                {"peer_address": "172.30.11.1", "advertised_communities": ["standard", "extended"]},
+                {"peer_address": "172.30.11.10", "vrf": "MGMT", "advertised_communities": ["extended"]},
+            ]
+        },
+        "expected": {"result": "success"},
+    },
     {
         "name": "failure-no-peer",
         "test": VerifyBGPAdvCommunities,
@@ -3337,6 +3533,52 @@ DATA: list[dict[str, Any]] = [
             ],
         },
     },
+    {
+        "name": "failure-specified-communities",
+        "test": VerifyBGPAdvCommunities,
+        "eos_data": [
+            {
+                "vrfs": {
+                    "default": {
+                        "peerList": [
+                            {
+                                "peerAddress": "172.30.11.1",
+                                "advertisedCommunities": {
+                                    "standard": False,
+                                    "extended": False,
+                                    "large": False,
+                                },
+                            }
+                        ]
+                    },
+                    "MGMT": {
+                        "peerList": [
+                            {
+                                "peerAddress": "172.30.11.10",
+                                "advertisedCommunities": {
+                                    "standard": False,
+                                    "extended": True,
+                                    "large": False,
+                                },
+                            }
+                        ]
+                    },
+                }
+            }
+        ],
+        "inputs": {
+            "bgp_peers": [
+                {"peer_address": "172.30.11.1", "advertised_communities": ["standard", "extended"]},
+                {"peer_address": "172.30.11.10", "vrf": "MGMT", "advertised_communities": ["extended"]},
+            ]
+        },
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "Peer: 172.30.11.1 VRF: default - Standard: False, Extended: False, Large: False",
+            ],
+        },
+    },
     {
         "name": "success",
         "test": VerifyBGPTimers,