Repository: aristanetworks/anta
Pull request #1140 (2025-04-07 16:50:09)

ci: pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.4)
<!--pre-commit.ci end-->

sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=1140) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=1140&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=1140&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=1140&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=1140&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=1140&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=1140)



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7697713f..0a3a95d9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -47,7 +47,7 @@ repos:
           - "<!--|  ~|  -->"
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.11.2
+    rev: v0.11.4
     hooks:
       - id: ruff
         name: Run Ruff linter
diff --git a/anta/custom_types.py b/anta/custom_types.py
index 92edabc1..5c87e29b 100644
--- a/anta/custom_types.py
+++ b/anta/custom_types.py
@@ -189,7 +189,7 @@ def update_bgp_redistributed_proto_user(value: str) -> str:
 
 # AntaTest.Input types
 AAAAuthMethod = Annotated[str, AfterValidator(aaa_group_prefix)]
-Vlan = Annotated[int, Field(ge=0, le=4094)]
+VlanId = Annotated[int, Field(ge=0, le=4094)]
 MlagPriority = Annotated[int, Field(ge=1, le=32767)]
 Vni = Annotated[int, Field(ge=1, le=16777215)]
 Interface = Annotated[
diff --git a/anta/input_models/vlan.py b/anta/input_models/vlan.py
new file mode 100644
index 00000000..25588577
--- /dev/null
+++ b/anta/input_models/vlan.py
@@ -0,0 +1,26 @@
+# 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.
+"""Module containing input models for VLAN tests."""
+
+from __future__ import annotations
+
+from typing import Literal
+
+from pydantic import BaseModel, ConfigDict
+
+from anta.custom_types import VlanId
+
+
+class Vlan(BaseModel):
+    """Model for a VLAN."""
+
+    model_config = ConfigDict(extra="forbid")
+    vlan_id: VlanId
+    """The VLAN ID."""
+    status: Literal["active", "suspended", "inactive"]
+    """The VLAN administrative status."""
+
+    def __str__(self) -> str:
+        """Representation of the VLAN model."""
+        return f"VLAN: Vlan{self.vlan_id}"


Test patch:
diff --git a/anta/tests/multicast.py b/anta/tests/multicast.py
index fe09c940..0f8a278c 100644
--- a/anta/tests/multicast.py
+++ b/anta/tests/multicast.py
@@ -9,7 +9,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING, ClassVar
 
-from anta.custom_types import Vlan
+from anta.custom_types import VlanId
 from anta.models import AntaCommand, AntaTest
 
 if TYPE_CHECKING:
@@ -41,7 +41,7 @@ class VerifyIGMPSnoopingVlans(AntaTest):
     class Input(AntaTest.Input):
         """Input model for the VerifyIGMPSnoopingVlans test."""
 
-        vlans: dict[Vlan, bool]
+        vlans: dict[VlanId, bool]
         """Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False)."""
 
     @AntaTest.anta_test
diff --git a/anta/tests/stp.py b/anta/tests/stp.py
index a25e514b..6b3d4407 100644
--- a/anta/tests/stp.py
+++ b/anta/tests/stp.py
@@ -11,7 +11,7 @@ from typing import ClassVar, Literal
 
 from pydantic import Field
 
-from anta.custom_types import Vlan
+from anta.custom_types import VlanId
 from anta.models import AntaCommand, AntaTemplate, AntaTest
 from anta.tools import get_value
 
@@ -44,7 +44,7 @@ class VerifySTPMode(AntaTest):
 
         mode: Literal["mstp", "rstp", "rapidPvst"] = "mstp"
         """STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp."""
-        vlans: list[Vlan]
+        vlans: list[VlanId]
         """List of VLAN on which to verify STP mode."""
 
     def render(self, template: AntaTemplate) -> list[AntaCommand]:
@@ -157,7 +157,7 @@ class VerifySTPForwardingPorts(AntaTest):
     class Input(AntaTest.Input):
         """Input model for the VerifySTPForwardingPorts test."""
 
-        vlans: list[Vlan]
+        vlans: list[VlanId]
         """List of VLAN on which to verify forwarding states."""
 
     def render(self, template: AntaTemplate) -> list[AntaCommand]:
@@ -213,7 +213,7 @@ class VerifySTPRootPriority(AntaTest):
 
         priority: int
         """STP root priority to verify."""
-        instances: list[Vlan] = Field(default=[])
+        instances: list[VlanId] = Field(default=[])
         """List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified."""
 
     @AntaTest.anta_test
@@ -331,7 +331,7 @@ class VerifySTPDisabledVlans(AntaTest):
     class Input(AntaTest.Input):
         """Input model for the VerifySTPDisabledVlans test."""
 
-        vlans: list[Vlan]
+        vlans: list[VlanId]
         """List of STP disabled VLAN(s)."""
 
     @AntaTest.anta_test
diff --git a/anta/tests/vlan.py b/anta/tests/vlan.py
index 25fc9d52..461a215c 100644
--- a/anta/tests/vlan.py
+++ b/anta/tests/vlan.py
@@ -9,7 +9,8 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING, ClassVar, Literal
 
-from anta.custom_types import DynamicVlanSource, Vlan
+from anta.custom_types import DynamicVlanSource, VlanId
+from anta.input_models.vlan import Vlan
 from anta.models import AntaCommand, AntaTest
 from anta.tools import get_value
 
@@ -47,9 +48,9 @@ class VerifyVlanInternalPolicy(AntaTest):
 
         policy: Literal["ascending", "descending"]
         """The VLAN internal allocation policy. Supported values: ascending, descending."""
-        start_vlan_id: Vlan
+        start_vlan_id: VlanId
         """The starting VLAN ID in the range."""
-        end_vlan_id: Vlan
+        end_vlan_id: VlanId
         """The ending VLAN ID in the range."""
 
     @AntaTest.anta_test
@@ -145,3 +146,48 @@ class VerifyDynamicVlanSource(AntaTest):
             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))}")
+
+
+class VerifyVlanStatus(AntaTest):
+    """Verifies the administrative status of specified VLANs.
+
+    Expected Results
+    ----------------
+    * Success: The test will pass if all specified VLANs exist in the configuration and their administrative status is correct.
+    * Failure: The test will fail if any of the specified VLANs is not found in the configuration or if its administrative status is incorrect.
+
+    Examples
+    --------
+    ```yaml
+    anta.tests.vlan:
+      - VerifyVlanStatus:
+          vlans:
+            - vlan_id: 10
+              status: suspended
+            - vlan_id: 4094
+              status: active
+    ```
+    """
+
+    categories: ClassVar[list[str]] = ["vlan"]
+    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan", revision=1)]
+
+    class Input(AntaTest.Input):
+        """Input model for the VerifyVlanStatus test."""
+
+        vlans: list[Vlan]
+        """List of VLAN details."""
+
+    @AntaTest.anta_test
+    def test(self) -> None:
+        """Main test function for VerifyVlanStatus."""
+        self.result.is_success()
+        command_output = self.instance_commands[0].json_output
+
+        for vlan in self.inputs.vlans:
+            if (vlan_detail := get_value(command_output, f"vlans.{vlan.vlan_id}")) is None:
+                self.result.is_failure(f"{vlan} - Not configured")
+                continue
+
+            if (act_status := vlan_detail["status"]) != vlan.status:
+                self.result.is_failure(f"{vlan} - Incorrect administrative status - Expected: {vlan.status} Actual: {act_status}")
diff --git a/anta/tests/vxlan.py b/anta/tests/vxlan.py
index 09bd30f3..ecd7b7a7 100644
--- a/anta/tests/vxlan.py
+++ b/anta/tests/vxlan.py
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, ClassVar
 
 from pydantic import Field
 
-from anta.custom_types import Vlan, Vni, VxlanSrcIntf
+from anta.custom_types import VlanId, Vni, VxlanSrcIntf
 from anta.models import AntaCommand, AntaTest
 from anta.tools import get_value
 
@@ -127,7 +127,7 @@ class VerifyVxlanVniBinding(AntaTest):
     class Input(AntaTest.Input):
         """Input model for the VerifyVxlanVniBinding test."""
 
-        bindings: dict[Vni, Vlan]
+        bindings: dict[Vni, VlanId]
         """VNI to VLAN bindings to verify."""
 
     @AntaTest.anta_test
diff --git a/docs/api/tests/vlan.md b/docs/api/tests/vlan.md
index 04dca0e8..46f28b88 100644
--- a/docs/api/tests/vlan.md
+++ b/docs/api/tests/vlan.md
@@ -8,6 +8,8 @@ anta_title: ANTA catalog for VLAN tests
   ~ that can be found in the LICENSE file.
   -->
 
+# Tests
+
 ::: anta.tests.vlan
 
     options:
@@ -22,3 +24,19 @@ anta_title: ANTA catalog for VLAN tests
       show_root_toc_entry: false
       show_symbol_type_heading: false
       show_symbol_type_toc: false
+
+# Input models
+
+::: anta.input_models.vlan
+
+    options:
+      anta_hide_test_module_description: true
+      filters:
+        - "!^__str__"
+      merge_init_into_class: false
+      show_bases: false
+      show_labels: true
+      show_root_heading: false
+      show_root_toc_entry: false
+      show_symbol_type_heading: false
+      show_symbol_type_toc: false
diff --git a/examples/tests.yaml b/examples/tests.yaml
index f0e014ac..bfeba4c7 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -1026,6 +1026,13 @@ anta.tests.vlan:
       policy: ascending
       start_vlan_id: 1006
       end_vlan_id: 4094
+  - VerifyVlanStatus:
+      # Verifies the administrative status of specified VLANs.
+      vlans:
+        - vlan_id: 10
+          status: suspended
+        - vlan_id: 4094
+          status: active
 anta.tests.vxlan:
   - VerifyVxlan1ConnSettings:
       # Verifies Vxlan1 source interface and UDP port.
diff --git a/tests/units/anta_tests/test_vlan.py b/tests/units/anta_tests/test_vlan.py
index fd900ca4..a5accdec 100644
--- a/tests/units/anta_tests/test_vlan.py
+++ b/tests/units/anta_tests/test_vlan.py
@@ -7,7 +7,7 @@ from __future__ import annotations
 
 from typing import Any
 
-from anta.tests.vlan import VerifyDynamicVlanSource, VerifyVlanInternalPolicy
+from anta.tests.vlan import VerifyDynamicVlanSource, VerifyVlanInternalPolicy, VerifyVlanStatus
 from tests.units.anta_tests import test
 
 DATA: list[dict[str, Any]] = [
@@ -89,4 +89,62 @@ DATA: list[dict[str, Any]] = [
         "inputs": {"sources": ["evpn", "mlagsync"], "strict": True},
         "expected": {"result": "failure", "messages": ["Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync"]},
     },
+    {
+        "name": "success",
+        "test": VerifyVlanStatus,
+        "eos_data": [
+            {
+                "vlans": {
+                    "1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}},
+                    "4092": {"name": "VLAN4092", "dynamic": True, "status": "active", "interfaces": {}},
+                    "4094": {"name": "VLAN4094", "dynamic": True, "status": "active", "interfaces": {}},
+                },
+                "sourceDetail": "",
+            }
+        ],
+        "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "active"}]},
+        "expected": {"result": "success"},
+    },
+    {
+        "name": "failure-vlan-not-conifgured",
+        "test": VerifyVlanStatus,
+        "eos_data": [
+            {
+                "vlans": {
+                    "1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}},
+                },
+                "sourceDetail": "",
+            }
+        ],
+        "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "active"}]},
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "VLAN: Vlan4092 - Not configured",
+                "VLAN: Vlan4094 - Not configured",
+            ],
+        },
+    },
+    {
+        "name": "failure-incorrect-status",
+        "test": VerifyVlanStatus,
+        "eos_data": [
+            {
+                "vlans": {
+                    "1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}},
+                    "4092": {"name": "VLAN4092", "dynamic": True, "status": "suspended", "interfaces": {}},
+                    "4094": {"name": "VLAN4094", "dynamic": True, "status": "active", "interfaces": {}},
+                },
+                "sourceDetail": "",
+            }
+        ],
+        "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "suspended"}]},
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "VLAN: Vlan4092 - Incorrect administrative status - Expected: active Actual: suspended",
+                "VLAN: Vlan4094 - Incorrect administrative status - Expected: suspended Actual: active",
+            ],
+        },
+    },
 ]