From 1acbdcb7c7d7e230559ceb9932ed7ab99deee31c Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:38:16 +0530 Subject: [PATCH 1/4] fix(anta.tests): BFD test module: AntaTest.Input subclasses using common input models should have validators for their required fields. (#999) * issue_997: BFD tests with proper validators * improved the test coverage * updated validator function * addressed review commenrs: updated validator msgs --- anta/tests/bfd.py | 34 +++++++++++++- tests/units/input_models/test_bfd.py | 68 ++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/units/input_models/test_bfd.py diff --git a/anta/tests/bfd.py b/anta/tests/bfd.py index 861a6a2e4..2361a4221 100644 --- a/anta/tests/bfd.py +++ b/anta/tests/bfd.py @@ -8,9 +8,9 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, ClassVar, TypeVar -from pydantic import Field +from pydantic import Field, field_validator from anta.input_models.bfd import BFDPeer from anta.models import AntaCommand, AntaTest @@ -19,6 +19,9 @@ if TYPE_CHECKING: from anta.models import AntaTemplate +# Using a TypeVar for the BFDPeer model since mypy thinks it's a ClassVar and not a valid type when used in field validators +T = TypeVar("T", bound=BFDPeer) + class VerifyBFDSpecificPeers(AntaTest): """Verifies the state of IPv4 BFD peer sessions. @@ -143,6 +146,23 @@ class Input(AntaTest.Input): BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer """To maintain backward compatibility""" + @field_validator("bfd_peers") + @classmethod + def validate_bfd_peers(cls, bfd_peers: list[T]) -> list[T]: + """Validate that 'tx_interval', 'rx_interval' and 'multiplier' fields are provided in each BFD peer.""" + for peer in bfd_peers: + missing_fileds = [] + if peer.tx_interval is None: + missing_fileds.append("tx_interval") + if peer.rx_interval is None: + missing_fileds.append("rx_interval") + if peer.multiplier is None: + missing_fileds.append("multiplier") + if missing_fileds: + msg = f"{peer} {', '.join(missing_fileds)} field(s) are missing in the input." + raise ValueError(msg) + return bfd_peers + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBFDPeersIntervals.""" @@ -308,6 +328,16 @@ class Input(AntaTest.Input): BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer """To maintain backward compatibility""" + @field_validator("bfd_peers") + @classmethod + def validate_bfd_peers(cls, bfd_peers: list[T]) -> list[T]: + """Validate that 'protocols' field is provided in each BFD peer.""" + for peer in bfd_peers: + if peer.protocols is None: + msg = f"{peer} 'protocols' field missing in the input." + raise ValueError(msg) + return bfd_peers + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBFDPeersRegProtocols.""" diff --git a/tests/units/input_models/test_bfd.py b/tests/units/input_models/test_bfd.py new file mode 100644 index 000000000..e179f39fe --- /dev/null +++ b/tests/units/input_models/test_bfd.py @@ -0,0 +1,68 @@ +# Copyright (c) 2023-2025 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. +"""Tests for anta.input_models.bfd.py.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from pydantic import ValidationError + +from anta.tests.bfd import VerifyBFDPeersIntervals, VerifyBFDPeersRegProtocols + +if TYPE_CHECKING: + from anta.input_models.bfd import BFDPeer + + +class TestVerifyBFDPeersIntervalsInput: + """Test anta.tests.bfd.VerifyBFDPeersIntervals.Input.""" + + @pytest.mark.parametrize( + ("bfd_peers"), + [ + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default", "tx_interval": 1200, "rx_interval": 1200, "multiplier": 3}], id="valid"), + ], + ) + def test_valid(self, bfd_peers: list[BFDPeer]) -> None: + """Test VerifyBFDPeersIntervals.Input valid inputs.""" + VerifyBFDPeersIntervals.Input(bfd_peers=bfd_peers) + + @pytest.mark.parametrize( + ("bfd_peers"), + [ + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default", "tx_interval": 1200}], id="invalid-tx-interval"), + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default", "rx_interval": 1200}], id="invalid-rx-interval"), + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default", "tx_interval": 1200, "rx_interval": 1200}], id="invalid-multiplier"), + ], + ) + def test_invalid(self, bfd_peers: list[BFDPeer]) -> None: + """Test VerifyBFDPeersIntervals.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifyBFDPeersIntervals.Input(bfd_peers=bfd_peers) + + +class TestVerifyBFDPeersRegProtocolsInput: + """Test anta.tests.bfd.VerifyBFDPeersRegProtocols.Input.""" + + @pytest.mark.parametrize( + ("bfd_peers"), + [ + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default", "protocols": ["bgp"]}], id="valid"), + ], + ) + def test_valid(self, bfd_peers: list[BFDPeer]) -> None: + """Test VerifyBFDPeersRegProtocols.Input valid inputs.""" + VerifyBFDPeersRegProtocols.Input(bfd_peers=bfd_peers) + + @pytest.mark.parametrize( + ("bfd_peers"), + [ + pytest.param([{"peer_address": "10.0.0.1", "vrf": "default"}], id="invalid"), + ], + ) + def test_invalid(self, bfd_peers: list[BFDPeer]) -> None: + """Test VerifyBFDPeersRegProtocols.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifyBFDPeersRegProtocols.Input(bfd_peers=bfd_peers) From 56acfbceca41658960edf191aed11efe61134ae8 Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:15:45 +0530 Subject: [PATCH 2/4] feat(anta.tests): Added tests parallel of VerifyBGPSpecificPeers or VerifyBGPPeersHealth tailored for RIBD (#1003) * Added TC for VerifyBGPSpecificPeers or VerifyBGPPeersHealth tailored for RIBD * fixing the pipeline failures * Addressed review comments: updated docstring and info message for ribd tests * Update BGP info message --------- Co-authored-by: Carl Baillargeon --- anta/tests/routing/bgp.py | 142 ++++++++ docs/api/tests.routing.bgp.md | 9 +- examples/tests.yaml | 15 + tests/units/anta_tests/routing/test_bgp.py | 372 +++++++++++++++++++++ 4 files changed, 536 insertions(+), 2 deletions(-) diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index a2863c6e0..e17974226 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -1333,3 +1333,145 @@ def test(self) -> None: # Verify warning limit if provided. By default, EOS does not have a warning limit and `totalRoutesWarnLimit` is not present in the output. if warning_limit is not None and (actual_warning_limit := peer_data.get("totalRoutesWarnLimit", 0)) != warning_limit: self.result.is_failure(f"{peer} - Maximum routes warning limit mismatch - Expected: {warning_limit}, Actual: {actual_warning_limit}") + + +class VerifyBGPPeerSessionRibd(AntaTest): + """Verifies the session state of BGP IPv4 peer(s). + + Compatible with EOS operating in `ribd` routing protocol model. + + 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. Checks that the BGP session is in the `Established` state. + 3. Ensures that both input and output TCP message queues are empty. + Can be disabled by setting `check_tcp_queues` global flag to `False`. + + Expected Results + ---------------- + * Success: If all of the following conditions are met: + - All specified peers are found in the BGP configuration. + - All peers sessions state are `Established`. + - All peers have empty TCP message queues if `check_tcp_queues` is `True` (default). + * Failure: If any of the following occur: + - A specified peer is not found in the BGP configuration. + - A peer's session state is not `Established`. + - A peer has non-empty TCP message queues (input or output) when `check_tcp_queues` is `True`. + + Examples + -------- + ```yaml + anta.tests.routing: + bgp: + - VerifyBGPPeerSessionRibd: + check_tcp_queues: false + bgp_peers: + - peer_address: 10.1.0.1 + vrf: default + - peer_address: 10.1.0.2 + vrf: default + - peer_address: 10.1.255.2 + vrf: DEV + - peer_address: 10.1.255.4 + vrf: DEV + ``` + """ + + categories: ClassVar[list[str]] = ["bgp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip bgp neighbors vrf all", revision=2)] + + class Input(AntaTest.Input): + """Input model for the VerifyBGPPeerSessionRibd test.""" + + check_tcp_queues: bool = True + """Flag to check if the TCP session queues are empty for all BGP peers. Defaults to `True`.""" + bgp_peers: list[BgpPeer] + """List of BGP IPv4 peers.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyBGPPeerSessionRibd.""" + self.result.is_success() + + output = self.instance_commands[0].json_output + + for peer in self.inputs.bgp_peers: + peer_address = str(peer.peer_address) + peers = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) + + # Check if the peer is found + if (peer_data := get_item(peers, "peerAddress", peer_address)) is None: + self.result.is_failure(f"{peer} - Not found") + continue + + # Check if the BGP session is established + if peer_data["state"] != "Established": + self.result.is_failure(f"{peer} - Session state is not established - State: {peer_data['state']}") + continue + + # Check the TCP session message queues + if self.inputs.check_tcp_queues: + inq_stat = peer_data["peerTcpInfo"]["inputQueueLength"] + outq_stat = peer_data["peerTcpInfo"]["outputQueueLength"] + if inq_stat != 0 or outq_stat != 0: + self.result.is_failure(f"{peer} - Session has non-empty message queues - InQ: {inq_stat}, OutQ: {outq_stat}") + + +class VerifyBGPPeersHealthRibd(AntaTest): + """Verifies the health of all the BGP IPv4 peer(s). + + Compatible with EOS operating in `ribd` routing protocol model. + + This test performs the following checks for all BGP IPv4 peers: + + 1. Verifies that the BGP session is in the `Established` state. + 2. Checks that both input and output TCP message queues are empty. + Can be disabled by setting `check_tcp_queues` global flag to `False`. + + Expected Results + ---------------- + * Success: If all checks pass for all BGP IPv4 peers. + * Failure: If any of the following occur: + - Any BGP session is not in the `Established` state. + - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default). + + Examples + -------- + ```yaml + anta.tests.routing: + bgp: + - VerifyBGPPeersHealthRibd: + check_tcp_queues: True + ``` + """ + + categories: ClassVar[list[str]] = ["bgp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip bgp neighbors vrf all", revision=2)] + + class Input(AntaTest.Input): + """Input model for the VerifyBGPPeersHealthRibd test.""" + + check_tcp_queues: bool = True + """Flag to check if the TCP session queues are empty for all BGP peers. Defaults to `True`.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyBGPPeersHealthRibd.""" + self.result.is_success() + + output = self.instance_commands[0].json_output + + for vrf, vrf_data in output["vrfs"].items(): + peer_list = vrf_data.get("peerList", []) + + for peer in peer_list: + # Check if the BGP session is established + if peer["state"] != "Established": + self.result.is_failure(f"Peer: {peer['peerAddress']} VRF: {vrf} - Session state is not established - State: {peer['state']}") + continue + + # Check the TCP session message queues + inq = peer["peerTcpInfo"]["inputQueueLength"] + outq = peer["peerTcpInfo"]["outputQueueLength"] + if self.inputs.check_tcp_queues and (inq != 0 or outq != 0): + self.result.is_failure(f"Peer: {peer['peerAddress']} VRF: {vrf} - Session has non-empty message queues - InQ: {inq}, OutQ: {outq}") diff --git a/docs/api/tests.routing.bgp.md b/docs/api/tests.routing.bgp.md index f187c163d..1e0cbec8d 100644 --- a/docs/api/tests.routing.bgp.md +++ b/docs/api/tests.routing.bgp.md @@ -7,8 +7,13 @@ anta_title: ANTA catalog for BGP tests ~ that can be found in the LICENSE file. --> -!!! info "`multi-agent` Service Routing Protocols Model Requirements" - The BGP tests in this section are only supported on switches running the `multi-agent` routing protocols model. Starting from EOS version 4.30.1F, `service routing protocols model` is set to `multi-agent` by default. These BGP commands may **not** be compatible with switches running the legacy `ribd` routing protocols model and may fail if attempted. +!!! info "BGP Test Compatibility Note" + ANTA BGP tests are designed for the `multi-agent` routing protocol model. Starting from EOS 4.30.1F, `service routing protocols models` is set to `multi-agent` by default, and from EOS 4.32.0F it becomes the only supported model. + + The following tests are available for devices using the legacy `ribd` model on earlier EOS versions: + + - `VerifyBGPPeerSessionRibd` + - `VerifyBGPPeersHealthRibd` # Tests diff --git a/examples/tests.yaml b/examples/tests.yaml index 387d3bb0a..790e2878f 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -460,6 +460,18 @@ anta.tests.routing.bgp: vrf: DEV - peer_address: 10.1.255.4 vrf: DEV + - VerifyBGPPeerSessionRibd: + # Verifies the session state of BGP IPv4 peer(s). + check_tcp_queues: false + bgp_peers: + - peer_address: 10.1.0.1 + vrf: default + - peer_address: 10.1.0.2 + vrf: default + - peer_address: 10.1.255.2 + vrf: DEV + - peer_address: 10.1.255.4 + vrf: DEV - VerifyBGPPeerUpdateErrors: # Verifies BGP update error counters for the provided BGP IPv4 peer(s). bgp_peers: @@ -478,6 +490,9 @@ anta.tests.routing.bgp: safi: "unicast" vrf: "DEV" check_tcp_queues: false + - VerifyBGPPeersHealthRibd: + # Verifies the health of all the BGP IPv4 peer(s). + check_tcp_queues: True - 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 4d9e3c026..86e199e0e 100644 --- a/tests/units/anta_tests/routing/test_bgp.py +++ b/tests/units/anta_tests/routing/test_bgp.py @@ -22,7 +22,9 @@ VerifyBGPPeerRouteLimit, VerifyBGPPeerRouteRefreshCap, VerifyBGPPeerSession, + VerifyBGPPeerSessionRibd, VerifyBGPPeersHealth, + VerifyBGPPeersHealthRibd, VerifyBGPPeerUpdateErrors, VerifyBgpRouteMaps, VerifyBGPSpecificPeers, @@ -4048,4 +4050,374 @@ def test_check_bgp_neighbor_capability(input_dict: dict[str, bool], expected: bo ], }, }, + { + "name": "success-no-check-tcp-queues", + "test": VerifyBGPPeerSessionRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "check_tcp_queues": False, + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default"}, + {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + ], + }, + "expected": {"result": "success"}, + }, + { + "name": "success-check-tcp-queues", + "test": VerifyBGPPeerSessionRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "check_tcp_queues": True, + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default"}, + {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + ], + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-peer-not-found", + "test": VerifyBGPPeerSessionRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default"}, + {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.9 VRF: MGMT - Not found", + ], + }, + }, + { + "name": "failure-not-established", + "test": VerifyBGPPeerSessionRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Active", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Active", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default"}, + {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Session state is not established - State: Active", + "Peer: 10.100.0.9 VRF: MGMT - Session state is not established - State: Active", + ], + }, + }, + { + "name": "failure-check-tcp-queues", + "test": VerifyBGPPeerSessionRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default"}, + {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5, OutQ: 10", + ], + }, + }, + { + "name": "success-no-check-tcp-queues", + "test": VerifyBGPPeersHealthRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "check_tcp_queues": False, + }, + "expected": {"result": "success"}, + }, + { + "name": "success-check-tcp-queues", + "test": VerifyBGPPeersHealthRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": { + "check_tcp_queues": True, + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-not-established", + "test": VerifyBGPPeersHealthRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Active", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Active", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": {}, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Session state is not established - State: Active", + "Peer: 10.100.0.9 VRF: MGMT - Session state is not established - State: Active", + ], + }, + }, + { + "name": "failure-check-tcp-queues", + "test": VerifyBGPPeersHealthRibd, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 10, + "inputQueueLength": 5, + }, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.9", + "state": "Established", + "peerTcpInfo": { + "outputQueueLength": 0, + "inputQueueLength": 0, + }, + } + ] + }, + }, + }, + ], + "inputs": {}, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5, OutQ: 10", + ], + }, + }, ] From 35f246f45340bae59e5d5d1f1d3b0ab6ef083fc9 Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:53:03 +0530 Subject: [PATCH 3/4] feat(anta): Added the test case for verify BGP peer group of the BGP IPv4 peer(s) (#815) * issue_810 Added TC for BGP peer-group * issue_810 Handling review comments: updated failure messages * CI fixes after conflicts removal * issue_810 Handling review comment: update new line * issue_810 Added pylint disable check for no of line * Updated input model changes and unit tests for input * Minor fixes * Minor fixes --------- Co-authored-by: VitthalMagadum Co-authored-by: gmuloc Co-authored-by: Carl Baillargeon --- anta/input_models/routing/bgp.py | 2 + anta/tests/routing/bgp.py | 73 ++++++- examples/tests.yaml | 6 + tests/units/anta_tests/routing/test_bgp.py | 189 +++++++++++++++++++ tests/units/input_models/routing/test_bgp.py | 26 +++ 5 files changed, 294 insertions(+), 2 deletions(-) diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py index 2eb14e4bb..e51e0e11a 100644 --- a/anta/input_models/routing/bgp.py +++ b/anta/input_models/routing/bgp.py @@ -142,6 +142,8 @@ class BgpPeer(BaseModel): """IPv4 address of the BGP peer.""" vrf: str = "default" """Optional VRF for the BGP peer. Defaults to `default`.""" + peer_group: str | None = None + """Peer group of the BGP peer. Required field in the `VerifyBGPPeerGroup` test.""" advertised_routes: list[IPv4Network] | None = None """List of advertised routes in CIDR format. Required field in the `VerifyBGPExchangedRoutes` test.""" received_routes: list[IPv4Network] | None = None diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index e17974226..89ad386ac 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -1226,7 +1226,7 @@ def validate_bgp_peers(cls, bgp_peers: list[T]) -> list[T]: """Validate that 'inbound_route_map' or 'outbound_route_map' field is provided in each BGP peer.""" for peer in bgp_peers: if not (peer.inbound_route_map or peer.outbound_route_map): - msg = f"{peer}; At least one of 'inbound_route_map' or 'outbound_route_map' must be provided." + msg = f"{peer} 'inbound_route_map' or 'outbound_route_map' field missing in the input" raise ValueError(msg) return bgp_peers @@ -1304,7 +1304,7 @@ def validate_bgp_peers(cls, bgp_peers: list[T]) -> list[T]: """Validate that 'maximum_routes' field is provided in each BGP peer.""" for peer in bgp_peers: if peer.maximum_routes is None: - msg = f"{peer}; 'maximum_routes' field missing in the input" + msg = f"{peer} 'maximum_routes' field missing in the input" raise ValueError(msg) return bgp_peers @@ -1335,6 +1335,75 @@ def test(self) -> None: self.result.is_failure(f"{peer} - Maximum routes warning limit mismatch - Expected: {warning_limit}, Actual: {actual_warning_limit}") +class VerifyBGPPeerGroup(AntaTest): + """Verifies BGP peer group of 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. Confirms the peer group is correctly assigned to the specified BGP peer. + + Expected Results + ---------------- + * Success: If all of the following conditions are met: + - All specified peers are found in the BGP configuration. + - The peer group is correctly assigned to the specified BGP peer. + * Failure: If any of the following occur: + - A specified peer is not found in the BGP configuration. + - The peer group is not correctly assigned to the specified BGP peer. + + Examples + -------- + ```yaml + anta.tests.routing: + bgp: + - VerifyBGPPeerGroup: + bgp_peers: + - peer_address: 172.30.11.1 + vrf: default + peer_group: IPv4-UNDERLAY-PEERS + ``` + """ + + categories: ClassVar[list[str]] = ["bgp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bgp neighbors vrf all", revision=3)] + + class Input(AntaTest.Input): + """Input model for the VerifyBGPPeerGroup test.""" + + bgp_peers: list[BgpPeer] + """List of BGP IPv4 peers.""" + + @field_validator("bgp_peers") + @classmethod + def validate_bgp_peers(cls, bgp_peers: list[BgpPeer]) -> list[BgpPeer]: + """Validate that 'peer_group' field is provided in each BGP peer.""" + for peer in bgp_peers: + if peer.peer_group is None: + msg = f"{peer} 'peer_group' field missing in the input" + raise ValueError(msg) + return bgp_peers + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyBGPPeerGroup.""" + self.result.is_success() + + output = self.instance_commands[0].json_output + + for peer in self.inputs.bgp_peers: + peer_ip = str(peer.peer_address) + peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) + + # Check if the peer is found + if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + self.result.is_failure(f"{peer} - Not found") + continue + + if (actual_peer_group := peer_data.get("peerGroupName", "Not Found")) != peer.peer_group: + self.result.is_failure(f"{peer} - Incorrect peer group configured - Expected: {peer.peer_group} Actual: {actual_peer_group}") + + class VerifyBGPPeerSessionRibd(AntaTest): """Verifies the session state of BGP IPv4 peer(s). diff --git a/examples/tests.yaml b/examples/tests.yaml index 790e2878f..1efe0d331 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -421,6 +421,12 @@ anta.tests.routing.bgp: drop_stats: - inDropAsloop - prefixEvpnDroppedUnsupportedRouteType + - VerifyBGPPeerGroup: + # Verifies BGP peer group of BGP IPv4 peer(s). + bgp_peers: + - peer_address: 172.30.11.1 + vrf: default + peer_group: IPv4-UNDERLAY-PEERS - VerifyBGPPeerMD5Auth: # Verifies the MD5 authentication and state of IPv4 BGP peer(s) in a specified VRF. bgp_peers: diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py index 86e199e0e..c0de32932 100644 --- a/tests/units/anta_tests/routing/test_bgp.py +++ b/tests/units/anta_tests/routing/test_bgp.py @@ -17,6 +17,7 @@ VerifyBGPPeerASNCap, VerifyBGPPeerCount, VerifyBGPPeerDropStats, + VerifyBGPPeerGroup, VerifyBGPPeerMD5Auth, VerifyBGPPeerMPCaps, VerifyBGPPeerRouteLimit, @@ -4050,6 +4051,194 @@ def test_check_bgp_neighbor_capability(input_dict: dict[str, bool], expected: bo ], }, }, + { + "name": "success", + "test": VerifyBGPPeerGroup, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "peerGroupName": "IPv4-UNDERLAY-PEERS", + }, + { + "peerAddress": "10.100.4.5", + "peerGroupName": "MLAG-IPv4-UNDERLAY-PEER", + }, + { + "peerAddress": "10.100.1.1", + "peerGroupName": "EVPN-OVERLAY-PEERS", + }, + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.10", + "peerGroupName": "IPv4-UNDERLAY-PEERS", + }, + { + "peerAddress": "10.100.1.2", + "peerGroupName": "EVPN-OVERLAY-PEERS", + }, + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.0.10", "vrf": "MGMT", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.1.1", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.1.2", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.4.5", "vrf": "default", "peer_group": "MLAG-IPv4-UNDERLAY-PEER"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-incorrect-peer-group", + "test": VerifyBGPPeerGroup, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + "peerGroupName": "UNDERLAY-PEERS", + }, + { + "peerAddress": "10.100.1.1", + "peerGroupName": "OVERLAY-PEERS", + }, + { + "peerAddress": "10.100.4.5", + "peerGroupName": "UNDERLAY-PEER", + }, + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.10", + "peerGroupName": "UNDERLAY-PEERS", + }, + { + "peerAddress": "10.100.1.2", + "peerGroupName": "OVERLAY-PEERS", + }, + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.0.10", "vrf": "MGMT", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.1.1", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.1.2", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.4.5", "vrf": "default", "peer_group": "MLAG-IPv4-UNDERLAY-PEER"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: UNDERLAY-PEERS", + "Peer: 10.100.0.10 VRF: MGMT - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: UNDERLAY-PEERS", + "Peer: 10.100.1.1 VRF: default - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: OVERLAY-PEERS", + "Peer: 10.100.1.2 VRF: MGMT - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: OVERLAY-PEERS", + "Peer: 10.100.4.5 VRF: default - Incorrect peer group configured - Expected: MLAG-IPv4-UNDERLAY-PEER Actual: UNDERLAY-PEER", + ], + }, + }, + { + "name": "failure-peers-not-found", + "test": VerifyBGPPeerGroup, + "eos_data": [ + { + "vrfs": { + "default": {"peerList": []}, + "MGMT": {"peerList": []}, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.0.10", "vrf": "MGMT", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.1.1", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.1.2", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.4.5", "vrf": "default", "peer_group": "MLAG-IPv4-UNDERLAY-PEER"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Not found", + "Peer: 10.100.0.10 VRF: MGMT - Not found", + "Peer: 10.100.1.1 VRF: default - Not found", + "Peer: 10.100.1.2 VRF: MGMT - Not found", + "Peer: 10.100.4.5 VRF: default - Not found", + ], + }, + }, + { + "name": "failure-peer-group-not-found", + "test": VerifyBGPPeerGroup, + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "10.100.0.8", + }, + { + "peerAddress": "10.100.1.1", + }, + { + "peerAddress": "10.100.4.5", + }, + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "10.100.0.10", + }, + { + "peerAddress": "10.100.1.2", + }, + ] + }, + }, + }, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.0.10", "vrf": "MGMT", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "10.100.1.1", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.1.2", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"peer_address": "10.100.4.5", "vrf": "default", "peer_group": "MLAG-IPv4-UNDERLAY-PEER"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.100.0.8 VRF: default - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: Not Found", + "Peer: 10.100.0.10 VRF: MGMT - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: Not Found", + "Peer: 10.100.1.1 VRF: default - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: Not Found", + "Peer: 10.100.1.2 VRF: MGMT - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: Not Found", + "Peer: 10.100.4.5 VRF: default - Incorrect peer group configured - Expected: MLAG-IPv4-UNDERLAY-PEER Actual: Not Found", + ], + }, + }, { "name": "success-no-check-tcp-queues", "test": VerifyBGPPeerSessionRibd, diff --git a/tests/units/input_models/routing/test_bgp.py b/tests/units/input_models/routing/test_bgp.py index b12b621f9..61f2b4397 100644 --- a/tests/units/input_models/routing/test_bgp.py +++ b/tests/units/input_models/routing/test_bgp.py @@ -15,6 +15,7 @@ from anta.tests.routing.bgp import ( VerifyBGPExchangedRoutes, VerifyBGPPeerCount, + VerifyBGPPeerGroup, VerifyBGPPeerMPCaps, VerifyBGPPeerRouteLimit, VerifyBgpRouteMaps, @@ -236,3 +237,28 @@ def test_invalid(self, bgp_peers: list[BgpPeer]) -> None: """Test VerifyBGPPeerRouteLimit.Input invalid inputs.""" with pytest.raises(ValidationError): VerifyBGPPeerRouteLimit.Input(bgp_peers=bgp_peers) + + +class TestVerifyBGPPeerGroupInput: + """Test anta.tests.routing.bgp.VerifyBGPPeerGroup.Input.""" + + @pytest.mark.parametrize( + ("bgp_peers"), + [ + pytest.param([{"peer_address": "172.30.255.5", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}], id="valid"), + ], + ) + def test_valid(self, bgp_peers: list[BgpPeer]) -> None: + """Test VerifyBGPPeerGroup.Input valid inputs.""" + VerifyBGPPeerGroup.Input(bgp_peers=bgp_peers) + + @pytest.mark.parametrize( + ("bgp_peers"), + [ + pytest.param([{"peer_address": "172.30.255.5", "vrf": "default"}], id="invalid"), + ], + ) + def test_invalid(self, bgp_peers: list[BgpPeer]) -> None: + """Test VerifyBGPPeerGroup.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifyBGPPeerGroup.Input(bgp_peers=bgp_peers) From 529e997e134a7708a6c2a9bb674bbf43c585c887 Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Wed, 15 Jan 2025 00:06:00 +0530 Subject: [PATCH 4/4] fix(anta.tests): Cleaning up Services tests module (VerifyErrdisableRecovery) (#955) * Refactored VerifyErrdisableRecovery test for input models * addressed review comments: updated the input model, docstring * Addressed review comments: updated input models, unit tests for input model * improved the test coverage * removed field validator as 'Field' constraints the input * updated input models and deprecation warning * updated unit test cases * Minor fixes --------- Co-authored-by: Carl Baillargeon --- anta/custom_types.py | 22 +++++++ anta/input_models/services.py | 43 +++++++++++++ anta/tests/services.py | 86 +++++++++++++------------ docs/api/tests.services.md | 4 +- examples/tests.yaml | 4 +- tests/units/anta_tests/test_services.py | 14 ++-- 6 files changed, 123 insertions(+), 50 deletions(-) diff --git a/anta/custom_types.py b/anta/custom_types.py index f3877459f..3af27db97 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -154,16 +154,38 @@ def validate_regex(value: str) -> str: ErrDisableReasons = Literal[ "acl", "arp-inspection", + "bgp-session-tracking", "bpduguard", + "dot1x", + "dot1x-coa", "dot1x-session-replace", + "evpn-sa-mh", + "fabric-link-failure", + "fabric-link-flap", "hitless-reload-down", + "lacp-no-portid", "lacp-rate-limit", + "license-enforce", "link-flap", + "mlagasu", + "mlagdualprimary", + "mlagissu", + "mlagmaintdown", "no-internal-vlan", + "out-of-voqs", "portchannelguard", + "portgroup-disabled", "portsec", + "speed-misconfigured", + "storm-control", + "stp-no-portid", + "stuck-queue", "tapagg", "uplink-failure-detection", + "xcvr-misconfigured", + "xcvr-overheat", + "xcvr-power-unsupported", + "xcvr-unsupported", ] ErrDisableInterval = Annotated[int, Field(ge=30, le=86400)] Percent = Annotated[float, Field(ge=0.0, le=100.0)] diff --git a/anta/input_models/services.py b/anta/input_models/services.py index 9989dae1b..25d772e41 100644 --- a/anta/input_models/services.py +++ b/anta/input_models/services.py @@ -6,9 +6,13 @@ from __future__ import annotations from ipaddress import IPv4Address, IPv6Address +from typing import Any, Literal +from warnings import warn from pydantic import BaseModel, ConfigDict, Field +from anta.custom_types import ErrDisableReasons + class DnsServer(BaseModel): """Model for a DNS server configuration.""" @@ -29,3 +33,42 @@ def __str__(self) -> str: Server 10.0.0.1 (VRF: default, Priority: 1) """ return f"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})" + + +class ErrdisableRecovery(BaseModel): + """Model for the error disable recovery functionality.""" + + model_config = ConfigDict(extra="forbid") + reason: ErrDisableReasons + """Name of the error disable reason.""" + status: Literal["Enabled", "Disabled"] = "Enabled" + """Operational status of the reason. Defaults to 'Enabled'.""" + interval: int = Field(ge=30, le=86400) + """Timer interval of the reason in seconds.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the ErrdisableRecovery for reporting. + + Examples + -------- + Reason: acl Status: Enabled Interval: 300 + """ + return f"Reason: {self.reason} Status: {self.status} Interval: {self.interval}" + + +class ErrDisableReason(ErrdisableRecovery): # pragma: no cover + """Alias for the ErrdisableRecovery model to maintain backward compatibility. + + When initialised, it will emit a deprecation warning and call the ErrdisableRecovery model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the ErrdisableRecovery class, emitting a depreciation warning.""" + warn( + message="ErrDisableReason model is deprecated and will be removed in ANTA v2.0.0. Use the ErrdisableRecovery model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) diff --git a/anta/tests/services.py b/anta/tests/services.py index 89a7f2636..8d8942160 100644 --- a/anta/tests/services.py +++ b/anta/tests/services.py @@ -9,12 +9,9 @@ # mypy: disable-error-code=attr-defined from typing import ClassVar -from pydantic import BaseModel - -from anta.custom_types import ErrDisableInterval, ErrDisableReasons -from anta.input_models.services import DnsServer +from anta.input_models.services import DnsServer, ErrDisableReason, ErrdisableRecovery from anta.models import AntaCommand, AntaTemplate, AntaTest -from anta.tools import get_dict_superset, get_failed_logs +from anta.tools import get_dict_superset, get_item class VerifyHostname(AntaTest): @@ -166,12 +163,24 @@ def test(self) -> None: class VerifyErrdisableRecovery(AntaTest): - """Verifies the errdisable recovery reason, status, and interval. + """Verifies the error disable recovery functionality. + + This test performs the following checks for each specified error disable reason: + + 1. Verifying if the specified error disable reason exists. + 2. Checking if the recovery timer status matches the expected enabled/disabled state. + 3. Validating that the timer interval matches the configured value. Expected Results ---------------- - * Success: The test will pass if the errdisable recovery reason status is enabled and the interval matches the input. - * Failure: The test will fail if the errdisable recovery reason is not found, the status is not enabled, or the interval does not match the input. + * Success: The test will pass if: + - The specified error disable reason exists. + - The recovery timer status matches the expected state. + - The timer interval matches the configured value. + * Failure: The test will fail if: + - The specified error disable reason does not exist. + - The recovery timer status does not match the expected state. + - The timer interval does not match the configured value. Examples -------- @@ -181,8 +190,10 @@ class VerifyErrdisableRecovery(AntaTest): reasons: - reason: acl interval: 30 + status: Enabled - reason: bpduguard interval: 30 + status: Enabled ``` """ @@ -193,44 +204,35 @@ class VerifyErrdisableRecovery(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyErrdisableRecovery test.""" - reasons: list[ErrDisableReason] + reasons: list[ErrdisableRecovery] """List of errdisable reasons.""" - - class ErrDisableReason(BaseModel): - """Model for an errdisable reason.""" - - reason: ErrDisableReasons - """Type or name of the errdisable reason.""" - interval: ErrDisableInterval - """Interval of the reason in seconds.""" + ErrDisableReason: ClassVar[type[ErrdisableRecovery]] = ErrDisableReason @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyErrdisableRecovery.""" - command_output = self.instance_commands[0].text_output self.result.is_success() + + # Skip header and last empty line + command_output = self.instance_commands[0].text_output.split("\n")[2:-1] + + # Collecting the actual errdisable reasons for faster lookup + errdisable_reasons = [ + {"reason": reason, "status": status, "interval": interval} + for line in command_output + if line.strip() # Skip empty lines + for reason, status, interval in [line.split(None, 2)] # Unpack split result + ] + for error_reason in self.inputs.reasons: - input_reason = error_reason.reason - input_interval = error_reason.interval - reason_found = False - - # Skip header and last empty line - lines = command_output.split("\n")[2:-1] - for line in lines: - # Skip empty lines - if not line.strip(): - continue - # Split by first two whitespaces - reason, status, interval = line.split(None, 2) - if reason != input_reason: - continue - reason_found = True - actual_reason_data = {"interval": interval, "status": status} - expected_reason_data = {"interval": str(input_interval), "status": "Enabled"} - if actual_reason_data != expected_reason_data: - failed_log = get_failed_logs(expected_reason_data, actual_reason_data) - self.result.is_failure(f"`{input_reason}`:{failed_log}\n") - break - - if not reason_found: - self.result.is_failure(f"`{input_reason}`: Not found.\n") + if not (reason_output := get_item(errdisable_reasons, "reason", error_reason.reason)): + self.result.is_failure(f"{error_reason} - Not found") + continue + + if not all( + [ + error_reason.status == (act_status := reason_output["status"]), + error_reason.interval == (act_interval := int(reason_output["interval"])), + ] + ): + self.result.is_failure(f"{error_reason} - Incorrect configuration - Status: {act_status} Interval: {act_interval}") diff --git a/docs/api/tests.services.md b/docs/api/tests.services.md index da8e1736b..674f5c081 100644 --- a/docs/api/tests.services.md +++ b/docs/api/tests.services.md @@ -33,4 +33,6 @@ anta_title: ANTA catalog for services tests merge_init_into_class: false anta_hide_test_module_description: true show_labels: true - filters: ["!^__str__"] + filters: + - "!^__init__" + - "!^__str__" diff --git a/examples/tests.yaml b/examples/tests.yaml index 1efe0d331..ce3b851a5 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -731,12 +731,14 @@ anta.tests.services: vrf: MGMT priority: 0 - VerifyErrdisableRecovery: - # Verifies the errdisable recovery reason, status, and interval. + # Verifies the error disable recovery functionality. reasons: - reason: acl interval: 30 + status: Enabled - reason: bpduguard interval: 30 + status: Enabled - VerifyHostname: # Verifies the hostname of a device. hostname: s1-spine1 diff --git a/tests/units/anta_tests/test_services.py b/tests/units/anta_tests/test_services.py index 439b8ea4f..955aab0f0 100644 --- a/tests/units/anta_tests/test_services.py +++ b/tests/units/anta_tests/test_services.py @@ -147,7 +147,7 @@ "inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}, {"reason": "tapagg", "interval": 30}]}, "expected": { "result": "failure", - "messages": ["`tapagg`: Not found."], + "messages": ["Reason: tapagg Status: Enabled Interval: 30 - Not found"], }, }, { @@ -165,7 +165,7 @@ "inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}]}, "expected": { "result": "failure", - "messages": ["`acl`:\nExpected `Enabled` as the status, but found `Disabled` instead."], + "messages": ["Reason: acl Status: Enabled Interval: 300 - Incorrect configuration - Status: Disabled Interval: 300"], }, }, { @@ -183,7 +183,9 @@ "inputs": {"reasons": [{"reason": "acl", "interval": 30}, {"reason": "arp-inspection", "interval": 30}]}, "expected": { "result": "failure", - "messages": ["`acl`:\nExpected `30` as the interval, but found `300` instead."], + "messages": [ + "Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Enabled Interval: 300", + ], }, }, { @@ -202,9 +204,9 @@ "expected": { "result": "failure", "messages": [ - "`acl`:\nExpected `30` as the interval, but found `300` instead.\nExpected `Enabled` as the status, but found `Disabled` instead.", - "`arp-inspection`:\nExpected `300` as the interval, but found `30` instead.", - "`tapagg`: Not found.", + "Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300", + "Reason: arp-inspection Status: Enabled Interval: 300 - Incorrect configuration - Status: Enabled Interval: 30", + "Reason: tapagg Status: Enabled Interval: 30 - Not found", ], }, },