Skip to content

Commit

Permalink
fix(anta.tests): Cleaning up Services tests module (VerifyErrdisableR…
Browse files Browse the repository at this point in the history
…ecovery) (#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 <[email protected]>
  • Loading branch information
vitthalmagadum and carl-baillargeon authored Jan 14, 2025
1 parent 35f246f commit 529e997
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 50 deletions.
22 changes: 22 additions & 0 deletions anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
43 changes: 43 additions & 0 deletions anta/input_models/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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)
86 changes: 44 additions & 42 deletions anta/tests/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
--------
Expand All @@ -181,8 +190,10 @@ class VerifyErrdisableRecovery(AntaTest):
reasons:
- reason: acl
interval: 30
status: Enabled
- reason: bpduguard
interval: 30
status: Enabled
```
"""

Expand All @@ -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}")
4 changes: 3 additions & 1 deletion docs/api/tests.services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__"
4 changes: 3 additions & 1 deletion examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions tests/units/anta_tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
},
{
Expand All @@ -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"],
},
},
{
Expand All @@ -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",
],
},
},
{
Expand All @@ -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",
],
},
},
Expand Down

0 comments on commit 529e997

Please sign in to comment.