Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(anta.tests): Cleaning up Services tests module (VerifyErrdisableRecovery) #955

Merged
merged 12 commits into from
Jan 14, 2025
Merged
24 changes: 23 additions & 1 deletion anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,41 @@ def validate_regex(value: str) -> str:
MultiProtocolCaps = Annotated[str, BeforeValidator(bgp_multiprotocol_capabilities_abbreviations)]
BfdInterval = Annotated[int, Field(ge=50, le=60000)]
BfdMultiplier = Annotated[int, Field(ge=3, le=50)]
ErrDisableReasons = Literal[
ErrdisableReason = 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
24 changes: 24 additions & 0 deletions anta/input_models/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from __future__ import annotations

from ipaddress import IPv4Address, IPv6Address
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field

from anta.custom_types import ErrdisableReason


class DnsServer(BaseModel):
"""Model for a DNS server configuration."""
Expand All @@ -29,3 +32,24 @@ 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: ErrdisableReason
"""Name of the error disable reason."""
status: Literal["Enabled", "Disabled"] = "Enabled"
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
"""Operational status of the reason. Defaults to 'Enabled'."""
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
timer_interval: int = Field(ge=30, le=86400)
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
"""Timer interval of the reason in seconds. Required field in the `VerifyErrdisableRecovery` test."""
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved

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.timer_interval}"
90 changes: 46 additions & 44 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, 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 @@ -180,9 +189,11 @@ class VerifyErrdisableRecovery(AntaTest):
- VerifyErrdisableRecovery:
reasons:
- reason: acl
interval: 30
timer_interval: 30
status: Enabled
- reason: bpduguard
interval: 30
timer_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]] = ErrdisableRecovery
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved

@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, "timer_interval": timer_interval}
for line in command_output
if line.strip() # Skip empty lines
for reason, status, timer_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.timer_interval == (act_interval := int(reason_output["timer_interval"])),
]
):
self.result.is_failure(f"{error_reason} - Incorrect configuration - Status: {act_status} Interval: {act_interval}")
8 changes: 5 additions & 3 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -707,12 +707,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
timer_interval: 30
status: Enabled
- reason: bpduguard
interval: 30
timer_interval: 30
status: Enabled
- VerifyHostname:
# Verifies the hostname of a device.
hostname: s1-spine1
Expand Down
28 changes: 17 additions & 11 deletions tests/units/anta_tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
arp-inspection Enabled 30
"""
],
"inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "bpduguard", "interval": 300}]},
"inputs": {"reasons": [{"reason": "acl", "timer_interval": 300}, {"reason": "bpduguard", "timer_interval": 300}]},
"expected": {"result": "success"},
},
{
Expand All @@ -144,10 +144,12 @@
arp-inspection Enabled 30
"""
],
"inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}, {"reason": "tapagg", "interval": 30}]},
"inputs": {
"reasons": [{"reason": "acl", "timer_interval": 300}, {"reason": "arp-inspection", "timer_interval": 30}, {"reason": "tapagg", "timer_interval": 30}]
},
"expected": {
"result": "failure",
"messages": ["`tapagg`: Not found."],
"messages": ["Reason: tapagg Status: Enabled Interval: 30 - Not found"],
},
},
{
Expand All @@ -162,10 +164,10 @@
arp-inspection Enabled 30
"""
],
"inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}]},
"inputs": {"reasons": [{"reason": "acl", "timer_interval": 300}, {"reason": "arp-inspection", "timer_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 @@ -180,10 +182,12 @@
arp-inspection Enabled 30
"""
],
"inputs": {"reasons": [{"reason": "acl", "interval": 30}, {"reason": "arp-inspection", "interval": 30}]},
"inputs": {"reasons": [{"reason": "acl", "timer_interval": 30}, {"reason": "arp-inspection", "timer_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 @@ -198,13 +202,15 @@
arp-inspection Enabled 30
"""
],
"inputs": {"reasons": [{"reason": "acl", "interval": 30}, {"reason": "arp-inspection", "interval": 300}, {"reason": "tapagg", "interval": 30}]},
"inputs": {
"reasons": [{"reason": "acl", "timer_interval": 30}, {"reason": "arp-inspection", "timer_interval": 300}, {"reason": "tapagg", "timer_interval": 30}]
},
"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
Loading