From 206de524ad00661771fed719cd0ef0c90c4953be Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:00:19 +0530 Subject: [PATCH] refactor(anta): Refactor VerifyInterfacesStatus test for nicer failure message (#899) --- anta/input_models/interfaces.py | 23 ++++++++++ anta/tests/interfaces.py | 53 +++++++++-------------- docs/api/tests.interfaces.md | 15 +++++++ tests/units/anta_tests/test_interfaces.py | 41 +++++++++++++++--- 4 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 anta/input_models/interfaces.py diff --git a/anta/input_models/interfaces.py b/anta/input_models/interfaces.py new file mode 100644 index 000000000..5036156de --- /dev/null +++ b/anta/input_models/interfaces.py @@ -0,0 +1,23 @@ +# Copyright (c) 2023-2024 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 interface tests.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel + +from anta.custom_types import Interface + + +class InterfaceState(BaseModel): + """Model for an interface state.""" + + name: Interface + """Interface to validate.""" + status: Literal["up", "down", "adminDown"] + """Expected status of the interface.""" + line_protocol_status: Literal["up", "down", "testing", "unknown", "dormant", "notPresent", "lowerLayerDown"] | None = None + """Expected line protocol status of the interface.""" diff --git a/anta/tests/interfaces.py b/anta/tests/interfaces.py index 5dab16729..dc6938110 100644 --- a/anta/tests/interfaces.py +++ b/anta/tests/interfaces.py @@ -9,7 +9,7 @@ import re from ipaddress import IPv4Network -from typing import Any, ClassVar, Literal +from typing import Any, ClassVar from pydantic import BaseModel, Field from pydantic_extra_types.mac_address import MacAddress @@ -17,6 +17,7 @@ from anta import GITHUB_SUGGESTION from anta.custom_types import EthernetInterface, Interface, Percent, PortChannelInterface, PositiveInteger from anta.decorators import skip_on_platforms +from anta.input_models.interfaces import InterfaceState from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import custom_division, get_failed_logs, get_item, get_value @@ -183,16 +184,20 @@ def test(self) -> None: class VerifyInterfacesStatus(AntaTest): - """Verifies if the provided list of interfaces are all in the expected state. + """Verifies the operational states of specified interfaces to ensure they match expected configurations. - - If line protocol status is provided, prioritize checking against both status and line protocol status - - If line protocol status is not provided and interface status is "up", expect both status and line protocol to be "up" - - If interface status is not "up", check only the interface status without considering line protocol status + This test performs the following checks for each specified interface: + + 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface. + 2. If `line_protocol_status` is not provided but the `status` is "up", it is assumed that both the status and line protocol should be "up". + 3. If the interface `status` is not "up", only the interface's status is validated, with no line protocol check performed. Expected Results ---------------- - * Success: The test will pass if the provided interfaces are all in the expected state. - * Failure: The test will fail if any interface is not in the expected state. + * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. + * Failure: If any of the following occur: + - The specified interface is not configured. + - The specified interface status and line protocol status does not match the expected operational state for any interface. Examples -------- @@ -219,30 +224,17 @@ class Input(AntaTest.Input): interfaces: list[InterfaceState] """List of interfaces with their expected state.""" - - class InterfaceState(BaseModel): - """Model for an interface state.""" - - name: Interface - """Interface to validate.""" - status: Literal["up", "down", "adminDown"] - """Expected status of the interface.""" - line_protocol_status: Literal["up", "down", "testing", "unknown", "dormant", "notPresent", "lowerLayerDown"] | None = None - """Expected line protocol status of the interface.""" + InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfacesStatus.""" - command_output = self.instance_commands[0].json_output - self.result.is_success() - intf_not_configured = [] - intf_wrong_state = [] - + command_output = self.instance_commands[0].json_output for interface in self.inputs.interfaces: if (intf_status := get_value(command_output["interfaceDescriptions"], interface.name, separator="..")) is None: - intf_not_configured.append(interface.name) + self.result.is_failure(f"{interface.name} - Not configured") continue status = "up" if intf_status["interfaceStatus"] in {"up", "connected"} else intf_status["interfaceStatus"] @@ -251,18 +243,15 @@ def test(self) -> None: # If line protocol status is provided, prioritize checking against both status and line protocol status if interface.line_protocol_status: if interface.status != status or interface.line_protocol_status != proto: - intf_wrong_state.append(f"{interface.name} is {status}/{proto}") + actual_state = f"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}" + self.result.is_failure(f"{interface.name} - {actual_state}") # If line protocol status is not provided and interface status is "up", expect both status and proto to be "up" # If interface status is not "up", check only the interface status without considering line protocol status - elif (interface.status == "up" and (status != "up" or proto != "up")) or (interface.status != status): - intf_wrong_state.append(f"{interface.name} is {status}/{proto}") - - if intf_not_configured: - self.result.is_failure(f"The following interface(s) are not configured: {intf_not_configured}") - - if intf_wrong_state: - self.result.is_failure(f"The following interface(s) are not in the expected state: {intf_wrong_state}") + elif interface.status == "up" and (status != "up" or proto != "up"): + self.result.is_failure(f"{interface.name} - Expected: up/up, Actual: {status}/{proto}") + elif interface.status != status: + self.result.is_failure(f"{interface.name} - Expected: {interface.status}, Actual: {status}") class VerifyStormControlDrops(AntaTest): diff --git a/docs/api/tests.interfaces.md b/docs/api/tests.interfaces.md index 95630f581..821b80bd3 100644 --- a/docs/api/tests.interfaces.md +++ b/docs/api/tests.interfaces.md @@ -7,7 +7,10 @@ anta_title: ANTA catalog for interfaces tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.interfaces + options: show_root_heading: false show_root_toc_entry: false @@ -18,3 +21,15 @@ anta_title: ANTA catalog for interfaces tests filters: - "!test" - "!render" + +# Input models + +::: anta.input_models.interfaces + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + anta_hide_test_module_description: true + show_labels: true + filters: ["!^__str__"] diff --git a/tests/units/anta_tests/test_interfaces.py b/tests/units/anta_tests/test_interfaces.py index ea8106e84..ac0530881 100644 --- a/tests/units/anta_tests/test_interfaces.py +++ b/tests/units/anta_tests/test_interfaces.py @@ -1108,7 +1108,7 @@ "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, "expected": { "result": "failure", - "messages": ["The following interface(s) are not configured: ['Ethernet8']"], + "messages": ["Ethernet8 - Not configured"], }, }, { @@ -1126,7 +1126,7 @@ "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, "expected": { "result": "failure", - "messages": ["The following interface(s) are not in the expected state: ['Ethernet8 is down/down'"], + "messages": ["Ethernet8 - Expected: up/up, Actual: down/down"], }, }, { @@ -1150,7 +1150,7 @@ }, "expected": { "result": "failure", - "messages": ["The following interface(s) are not in the expected state: ['Ethernet8 is up/down'"], + "messages": ["Ethernet8 - Expected: up/up, Actual: up/down"], }, }, { @@ -1166,7 +1166,7 @@ "inputs": {"interfaces": [{"name": "PortChannel100", "status": "up"}]}, "expected": { "result": "failure", - "messages": ["The following interface(s) are not in the expected state: ['Port-Channel100 is down/lowerLayerDown'"], + "messages": ["Port-Channel100 - Expected: up/up, Actual: down/lowerLayerDown"], }, }, { @@ -1190,7 +1190,38 @@ }, "expected": { "result": "failure", - "messages": ["The following interface(s) are not in the expected state: ['Ethernet2 is up/unknown'"], + "messages": [ + "Ethernet2 - Expected: up/down, Actual: up/unknown", + "Ethernet8 - Expected: up/up, Actual: up/down", + ], + }, + }, + { + "name": "failure-interface-status-down", + "test": VerifyInterfacesStatus, + "eos_data": [ + { + "interfaceDescriptions": { + "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "down"}, + "Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "unknown"}, + "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, + } + } + ], + "inputs": { + "interfaces": [ + {"name": "Ethernet2", "status": "down"}, + {"name": "Ethernet8", "status": "down"}, + {"name": "Ethernet3", "status": "down"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Ethernet2 - Expected: down, Actual: up", + "Ethernet8 - Expected: down, Actual: up", + "Ethernet3 - Expected: down, Actual: up", + ], }, }, {