Skip to content

Commit

Permalink
refactor(anta): Refactor VerifyInterfacesStatus test for nicer failur…
Browse files Browse the repository at this point in the history
…e message (aristanetworks#899)
  • Loading branch information
vitthalmagadum authored Nov 5, 2024
1 parent 595790f commit 206de52
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 37 deletions.
23 changes: 23 additions & 0 deletions anta/input_models/interfaces.py
Original file line number Diff line number Diff line change
@@ -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."""
53 changes: 21 additions & 32 deletions anta/tests/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

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

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

Expand Down Expand Up @@ -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
--------
Expand All @@ -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"]
Expand All @@ -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):
Expand Down
15 changes: 15 additions & 0 deletions docs/api/tests.interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__"]
41 changes: 36 additions & 5 deletions tests/units/anta_tests/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
},
{
Expand All @@ -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"],
},
},
{
Expand All @@ -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"],
},
},
{
Expand All @@ -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"],
},
},
{
Expand All @@ -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",
],
},
},
{
Expand Down

0 comments on commit 206de52

Please sign in to comment.