diff --git a/anta/custom_types.py b/anta/custom_types.py index 4763be495..f3877459f 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -240,3 +240,4 @@ def validate_regex(value: str) -> str: SnmpVersion = Literal["v1", "v2c", "v3"] SnmpHashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"] SnmpEncryptionAlgorithm = Literal["AES-128", "AES-192", "AES-256", "DES"] +DynamicVlanSource = Literal["dmf", "dot1x", "dynvtep", "evpn", "mlag", "mlagsync", "mvpn", "swfwd", "vccbfd"] diff --git a/anta/tests/vlan.py b/anta/tests/vlan.py index f2a1934cc..09b450ae6 100644 --- a/anta/tests/vlan.py +++ b/anta/tests/vlan.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, ClassVar, Literal -from anta.custom_types import Vlan +from anta.custom_types import DynamicVlanSource, Vlan from anta.models import AntaCommand, AntaTest from anta.tools import get_failed_logs, get_value @@ -68,3 +68,77 @@ def test(self) -> None: self.result.is_failure(failed_log) else: self.result.is_success() + + +class VerifyDynamicVlanSource(AntaTest): + """Verifies dynamic VLAN allocation for specified VLAN sources. + + This test performs the following checks for each specified VLAN source: + + 1. Validates source exists in dynamic VLAN table. + 2. Verifies at least one VLAN is allocated to the source. + 3. When strict mode is enabled (`strict: true`), ensures no other sources have VLANs allocated. + + Expected Results + ---------------- + * Success: The test will pass if all of the following conditions are met: + - Each specified source exists in dynamic VLAN table. + - Each specified source has at least one VLAN allocated. + - In strict mode: No other sources have VLANs allocated. + * Failure: The test will fail if any of the following conditions is met: + - Specified source not found in configuration. + - Source exists but has no VLANs allocated. + - In strict mode: Non-specified sources have VLANs allocated. + + Examples + -------- + ```yaml + anta.tests.vlan: + - VerifyDynamicVlanSource: + sources: + - evpn + - mlagsync + strict: False + ``` + """ + + categories: ClassVar[list[str]] = ["vlan"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan dynamic", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyDynamicVlanSource test.""" + + sources: list[DynamicVlanSource] + """The dynamic VLAN source list.""" + strict: bool = False + """If True, only specified sources are allowed to have VLANs allocated. Default is False.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyDynamicVlanSource.""" + self.result.is_success() + command_output = self.instance_commands[0].json_output + dynamic_vlans = command_output.get("dynamicVlans", {}) + + # Get all configured sources and sources with VLANs allocated + configured_sources = set(dynamic_vlans.keys()) + sources_with_vlans = {source for source, data in dynamic_vlans.items() if data.get("vlanIds")} + expected_sources = set(self.inputs.sources) + + # Check if all specified sources exist in configuration + missing_sources = expected_sources - configured_sources + if missing_sources: + self.result.is_failure(f"Dynamic VLAN source(s) not found in configuration: {', '.join(sorted(missing_sources))}") + return + + # Check if configured sources have VLANs allocated + sources_without_vlans = expected_sources - sources_with_vlans + if sources_without_vlans: + self.result.is_failure(f"Dynamic VLAN source(s) exist but have no VLANs allocated: {', '.join(sorted(sources_without_vlans))}") + return + + # In strict mode, verify no other sources have VLANs allocated + if self.inputs.strict: + unexpected_sources = sources_with_vlans - expected_sources + if unexpected_sources: + self.result.is_failure(f"Strict mode enabled: Unexpected sources have VLANs allocated: {', '.join(sorted(unexpected_sources))}") diff --git a/examples/tests.yaml b/examples/tests.yaml index c2e00f009..db1d179d6 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -847,6 +847,12 @@ anta.tests.system: # Verifies the device uptime. minimum: 86400 anta.tests.vlan: + - VerifyDynamicVlanSource: + # Verifies dynamic VLAN allocation for specified VLAN sources. + sources: + - evpn + - mlagsync + strict: False - VerifyVlanInternalPolicy: # Verifies the VLAN internal allocation policy and the range of VLANs. policy: ascending diff --git a/tests/units/anta_tests/test_vlan.py b/tests/units/anta_tests/test_vlan.py index e68bd06dc..7fc8b8688 100644 --- a/tests/units/anta_tests/test_vlan.py +++ b/tests/units/anta_tests/test_vlan.py @@ -7,7 +7,7 @@ from typing import Any -from anta.tests.vlan import VerifyVlanInternalPolicy +from anta.tests.vlan import VerifyDynamicVlanSource, VerifyVlanInternalPolicy from tests.units.anta_tests import test DATA: list[dict[str, Any]] = [ @@ -33,4 +33,52 @@ ], }, }, + { + "name": "success", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1401]}, "vccbfd": {"vlanIds": [1501]}}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, + "expected": {"result": "success"}, + }, + { + "name": "failure-no-dynamic-vlan-sources", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, + "expected": {"result": "failure", "messages": ["Dynamic VLAN source(s) not found in configuration: evpn, mlagsync"]}, + }, + { + "name": "failure-dynamic-vlan-sources-mismatch", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {"vccbfd": {"vlanIds": [1500]}, "mlagsync": {"vlanIds": [1501]}}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, + "expected": { + "result": "failure", + "messages": ["Dynamic VLAN source(s) not found in configuration: evpn"], + }, + }, + { + "name": "success-strict-mode", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1502], "vccbfd": {"vlanIds": []}}}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, + "expected": {"result": "success"}, + }, + { + "name": "failure-all-sources-exact-match-additional-source-found", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1500]}, "vccbfd": {"vlanIds": [1500]}}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, + "expected": { + "result": "failure", + "messages": ["Strict mode enabled: Unexpected sources have VLANs allocated: vccbfd"], + }, + }, + { + "name": "failure-all-sources-exact-match-expected-source-not-found", + "test": VerifyDynamicVlanSource, + "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": []}}}], + "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, + "expected": {"result": "failure", "messages": ["Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync"]}, + }, ]