diff --git a/anta/tests/stp.py b/anta/tests/stp.py index 7cbfc9cf0..3208f0c40 100644 --- a/anta/tests/stp.py +++ b/anta/tests/stp.py @@ -7,7 +7,7 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import ClassVar, Literal +from typing import Any, ClassVar, Literal from pydantic import Field @@ -259,3 +259,64 @@ def test(self) -> None: self.result.is_failure(f"The following instance(s) have the wrong STP root priority configured: {wrong_priority_instances}") else: self.result.is_success() + + +class VerifyStpTopologyChanges(AntaTest): + """Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. + + Expected Results + ---------------- + * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. + * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, + indicating potential instability in the topology. + + Examples + -------- + ```yaml + anta.tests.stp: + - VerifyStpTopologyChanges: + threshold: 10 + ``` + """ + + name = "VerifyStpTopologyChanges" + description = "Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold." + categories: ClassVar[list[str]] = ["stp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree topology status detail", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyStpTopologyChanges test.""" + + threshold: int + """The threshold number of changes in the STP topology.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyStpTopologyChanges.""" + failures: dict[str, Any] = {"topologies": {}} + + command_output = self.instance_commands[0].json_output + stp_topologies = command_output.get("topologies", {}) + + # verifies all available topologies except the "NoStp" topology. + stp_topologies.pop("NoStp", None) + + # Verify the STP topology(s). + if not stp_topologies: + self.result.is_failure("STP is not configured.") + return + + # Verifies the number of changes across all interfaces + for topology, topology_details in stp_topologies.items(): + interfaces = { + interface: {"Number of changes": num_of_changes} + for interface, details in topology_details.get("interfaces", {}).items() + if (num_of_changes := details.get("numChanges")) > self.inputs.threshold + } + if interfaces: + failures["topologies"][topology] = interfaces + + if failures["topologies"]: + self.result.is_failure(f"The following STP topologies are not configured or number of changes not within the threshold:\n{failures}") + else: + self.result.is_success() diff --git a/examples/tests.yaml b/examples/tests.yaml index 954b5b736..ade4e7640 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -427,6 +427,8 @@ anta.tests.stp: instances: - 10 - 20 + - VerifyStpTopologyChanges: + threshold: 10 anta.tests.stun: - VerifyStunClient: diff --git a/tests/units/anta_tests/test_stp.py b/tests/units/anta_tests/test_stp.py index a6855aa88..37422108b 100644 --- a/tests/units/anta_tests/test_stp.py +++ b/tests/units/anta_tests/test_stp.py @@ -7,7 +7,7 @@ from typing import Any -from anta.tests.stp import VerifySTPBlockedPorts, VerifySTPCounters, VerifySTPForwardingPorts, VerifySTPMode, VerifySTPRootPriority +from anta.tests.stp import VerifySTPBlockedPorts, VerifySTPCounters, VerifySTPForwardingPorts, VerifySTPMode, VerifySTPRootPriority, VerifyStpTopologyChanges from tests.units.anta_tests import test DATA: list[dict[str, Any]] = [ @@ -324,4 +324,166 @@ "inputs": {"priority": 32768, "instances": [10, 20, 30]}, "expected": {"result": "failure", "messages": ["The following instance(s) have the wrong STP root priority configured: ['VL20', 'VL30']"]}, }, + { + "name": "success-mstp", + "test": VerifyStpTopologyChanges, + "eos_data": [ + { + "unmappedVlans": [], + "topologies": { + "Cist": { + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.735365}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.7353542}, + } + }, + "NoStp": { + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.735365}, + "Ethernet1": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.7353542}, + } + }, + }, + }, + ], + "inputs": {"threshold": 10}, + "expected": {"result": "success"}, + }, + { + "name": "success-rstp", + "test": VerifyStpTopologyChanges, + "eos_data": [ + { + "unmappedVlans": [], + "topologies": { + "Cist": { + "interfaces": { + "Vxlan1": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.735365}, + "PeerEthernet3": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.7353542}, + } + }, + "NoStp": { + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.735365}, + "Ethernet1": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.7353542}, + } + }, + }, + }, + ], + "inputs": {"threshold": 10}, + "expected": {"result": "success"}, + }, + { + "name": "success-rapid-pvst", + "test": VerifyStpTopologyChanges, + "eos_data": [ + { + "unmappedVlans": [], + "topologies": { + "NoStp": { + "vlans": [4094, 4093, 1006], + "interfaces": { + "PeerEthernet2": {"state": "forwarding", "numChanges": 1, "lastChange": 1727151356.1330667}, + }, + }, + "Vl1": {"vlans": [1], "interfaces": {"Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0615358}}}, + "Vl10": { + "vlans": [10], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0673406}, + "Vxlan1": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0677001}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0728855}, + "Ethernet3": {"state": "forwarding", "numChanges": 3, "lastChange": 1727326730.255137}, + }, + }, + "Vl1198": { + "vlans": [1198], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.074386}, + "Vxlan1": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0743902}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0743942}, + }, + }, + "Vl1199": { + "vlans": [1199], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0744}, + "Vxlan1": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.07453}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.074535}, + }, + }, + "Vl20": { + "vlans": [20], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.073489}, + "Vxlan1": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0743747}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0743794}, + "Ethernet3": {"state": "forwarding", "numChanges": 3, "lastChange": 1727326730.2551405}, + }, + }, + "Vl3009": { + "vlans": [3009], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.074541}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0745454}, + }, + }, + "Vl3019": { + "vlans": [3019], + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0745502}, + "Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0745537}, + }, + }, + }, + }, + ], + "inputs": {"threshold": 10}, + "expected": {"result": "success"}, + }, + { + "name": "failure-unstable-topology", + "test": VerifyStpTopologyChanges, + "eos_data": [ + { + "unmappedVlans": [], + "topologies": { + "Cist": { + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.735365}, + "Port-Channel5": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.7353542}, + } + }, + }, + }, + ], + "inputs": {"threshold": 10}, + "expected": { + "result": "failure", + "messages": [ + "The following STP topologies are not configured or number of changes not within the threshold:\n" + "{'topologies': {'Cist': {'Cpu': {'Number of changes': 15}, 'Port-Channel5': {'Number of changes': 15}}}}" + ], + }, + }, + { + "name": "failure-topologies-not-configured", + "test": VerifyStpTopologyChanges, + "eos_data": [ + { + "unmappedVlans": [], + "topologies": { + "NoStp": { + "interfaces": { + "Cpu": {"state": "forwarding", "numChanges": 1, "lastChange": 1723990624.735365}, + "Ethernet1": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.7353542}, + } + } + }, + }, + ], + "inputs": {"threshold": 10}, + "expected": {"result": "failure", "messages": ["STP is not configured."]}, + }, ]