From 80e5c3815660728ab653d4e173b13b4cf04aef01 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Mon, 25 Nov 2024 08:03:44 -0500 Subject: [PATCH 01/13] STUN updated input models --- anta/input_models/stun.py | 35 +++++++++++++++ anta/tests/stun.py | 70 ++++++++++------------------- docs/api/tests.stun.md | 17 +++++++ tests/units/anta_tests/test_stun.py | 22 ++++----- 4 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 anta/input_models/stun.py diff --git a/anta/input_models/stun.py b/anta/input_models/stun.py new file mode 100644 index 000000000..329304d06 --- /dev/null +++ b/anta/input_models/stun.py @@ -0,0 +1,35 @@ +# 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 services tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Address + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import Port + + +class ClientAddress(BaseModel): + """STUN (Session Traversal Utilities for NAT) model represents the configuration of an IPv4-based client.""" + + model_config = ConfigDict(extra="forbid") + source_address: IPv4Address + """The IPv4 address of the STUN client""" + source_port: Port = 4500 + """The port number used by the STUN client for communication. Defaults to 4500.""" + public_address: IPv4Address | None = None + """The public-facing IPv4 address of the STUN client, discovered via the STUN server.""" + public_port: Port | None = None + """The public-facing port number of the STUN client, discovered via the STUN server.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the ClientAddress for reporting. + + Examples + -------- + Client 10.0.0.1 Port: 4500 + """ + return f"Client {self.source_address} Port: {self.source_port}" diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 8b4f4fb2f..4ebbe6f5a 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -7,23 +7,29 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from ipaddress import IPv4Address from typing import ClassVar -from pydantic import BaseModel - -from anta.custom_types import Port +from anta.input_models.stun import ClientAddress from anta.models import AntaCommand, AntaTemplate, AntaTest -from anta.tools import get_failed_logs, get_value +from anta.tools import get_value class VerifyStunClient(AntaTest): - """Verifies STUN client settings, including local IP/port and optionally public IP/port. + """Verifies the STUN client configuration. + + This test performs the following checks for each specified address family: + + 1. Validates that the STUN client is configured. + 2. If public IP and port details are provided, validates their correctness against the configuration. Expected Results ---------------- - * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. - * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. + * Success: If all of the following conditions are met: + - The test will pass if the STUN client is correctly configured. + - If public IP and port details are provided, they must also match the configuration. + * Failure: If any of the following occur: + - The STUN client is not configured. + - The public IP or port details, if specified, are incorrect. Examples -------- @@ -43,24 +49,14 @@ class VerifyStunClient(AntaTest): """ categories: ClassVar[list[str]] = ["stun"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}")] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}", revision=1)] class Input(AntaTest.Input): """Input model for the VerifyStunClient test.""" stun_clients: list[ClientAddress] - - class ClientAddress(BaseModel): - """Source and public address/port details of STUN client.""" - - source_address: IPv4Address - """IPv4 source address of STUN client.""" - source_port: Port = 4500 - """Source port number for STUN client.""" - public_address: IPv4Address | None = None - """Optional IPv4 public address of STUN client.""" - public_port: Port | None = None - """Optional public port number for STUN client.""" + """List of STUN clients.""" + ClientAddress: ClassVar[type[ClientAddress]] = ClientAddress def render(self, template: AntaTemplate) -> list[AntaCommand]: """Render the template for each STUN translation.""" @@ -74,42 +70,24 @@ def test(self) -> None: # Iterate over each command output and corresponding client input for command, client_input in zip(self.instance_commands, self.inputs.stun_clients): bindings = command.json_output["bindings"] - source_address = str(command.params.source_address) - source_port = command.params.source_port + input_public_address = client_input.public_address + input_public_port = client_input.public_port # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client if not bindings: - self.result.is_failure(f"STUN client transaction for source `{source_address}:{source_port}` is not found.") + self.result.is_failure(f"{client_input} - STUN client transaction not found.") continue - # Extract the public address and port from the client input - public_address = client_input.public_address - public_port = client_input.public_port - # Extract the transaction ID from the bindings transaction_id = next(iter(bindings.keys())) - # Prepare the actual and expected STUN data for comparison - actual_stun_data = { - "source ip": get_value(bindings, f"{transaction_id}.sourceAddress.ip"), - "source port": get_value(bindings, f"{transaction_id}.sourceAddress.port"), - } - expected_stun_data = {"source ip": source_address, "source port": source_port} - # If public address is provided, add it to the actual and expected STUN data - if public_address is not None: - actual_stun_data["public ip"] = get_value(bindings, f"{transaction_id}.publicAddress.ip") - expected_stun_data["public ip"] = str(public_address) + if input_public_address and str(input_public_address) != (actual_public_address := get_value(bindings, f"{transaction_id}.publicAddress.ip")): + self.result.is_failure(f"{client_input} - Incorrect public-facing address; Expected: {input_public_address} Actual: {actual_public_address}") # If public port is provided, add it to the actual and expected STUN data - if public_port is not None: - actual_stun_data["public port"] = get_value(bindings, f"{transaction_id}.publicAddress.port") - expected_stun_data["public port"] = public_port - - # If the actual STUN data does not match the expected STUN data, mark the test as failure - if actual_stun_data != expected_stun_data: - failed_log = get_failed_logs(expected_stun_data, actual_stun_data) - self.result.is_failure(f"For STUN source `{source_address}:{source_port}`:{failed_log}") + if input_public_port and input_public_port != (actual_public_port := get_value(bindings, f"{transaction_id}.publicAddress.port")): + self.result.is_failure(f"{client_input} - Incorrect public-facing port; Expected: {input_public_port} Actual: {actual_public_port}") class VerifyStunServer(AntaTest): diff --git a/docs/api/tests.stun.md b/docs/api/tests.stun.md index b4274e9a7..6a73b8880 100644 --- a/docs/api/tests.stun.md +++ b/docs/api/tests.stun.md @@ -7,6 +7,8 @@ anta_title: ANTA catalog for STUN tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.stun options: show_root_heading: false @@ -18,3 +20,18 @@ anta_title: ANTA catalog for STUN tests filters: - "!test" - "!render" + +# Input models + +::: anta.input_models.stun + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + merge_init_into_class: false + anta_hide_test_module_description: true + show_labels: true + filters: + - "!^__init__" + - "!^__str__" diff --git a/tests/units/anta_tests/test_stun.py b/tests/units/anta_tests/test_stun.py index 005ae35f8..0a6e5d4da 100644 --- a/tests/units/anta_tests/test_stun.py +++ b/tests/units/anta_tests/test_stun.py @@ -88,8 +88,8 @@ "expected": { "result": "failure", "messages": [ - "For STUN source `100.64.3.2:4500`:\nExpected `192.164.3.2` as the public ip, but found `192.64.3.2` instead.", - "For STUN source `172.18.3.2:4500`:\nExpected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.", + "Client 100.64.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.164.3.2 Actual: 192.64.3.2", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", ], }, }, @@ -108,7 +108,7 @@ }, "expected": { "result": "failure", - "messages": ["STUN client transaction for source `100.64.3.2:4500` is not found.", "STUN client transaction for source `172.18.3.2:4500` is not found."], + "messages": ["Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", "Client 172.18.3.2 Port: 4500 - STUN client transaction not found."], }, }, { @@ -134,10 +134,9 @@ "expected": { "result": "failure", "messages": [ - "STUN client transaction for source `100.64.3.2:4500` is not found.", - "For STUN source `172.18.3.2:4500`:\n" - "Expected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.\n" - "Expected `6006` as the public port, but found `4800` instead.", + "Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing port; Expected: 6006 Actual: 4800", ], }, }, @@ -164,12 +163,9 @@ "expected": { "result": "failure", "messages": [ - "STUN client transaction for source `100.64.3.2:4500` is not found.", - "For STUN source `172.18.4.2:4800`:\n" - "Expected `172.18.4.2` as the source ip, but found `172.18.3.2` instead.\n" - "Expected `4800` as the source port, but found `4500` instead.\n" - "Expected `192.118.3.2` as the public ip, but found `192.18.3.2` instead.\n" - "Expected `6006` as the public port, but found `4800` instead.", + "Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", + "Client 172.18.4.2 Port: 4800 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", + "Client 172.18.4.2 Port: 4800 - Incorrect public-facing port; Expected: 6006 Actual: 4800", ], }, }, From a60fc2964e88575cfde029a7c954e570bad2cab4 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 28 Nov 2024 01:14:04 -0500 Subject: [PATCH 02/13] addressed review commets: updated inline comments --- anta/tests/stun.py | 12 ++++++------ tests/units/anta_tests/test_stun.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 4ebbe6f5a..5d0bfe64e 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -19,7 +19,7 @@ class VerifyStunClient(AntaTest): This test performs the following checks for each specified address family: - 1. Validates that the STUN client is configured. + 1. Validates that there is a translation for the STUN client. 2. If public IP and port details are provided, validates their correctness against the configuration. Expected Results @@ -75,19 +75,19 @@ def test(self) -> None: # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client if not bindings: - self.result.is_failure(f"{client_input} - STUN client transaction not found.") + self.result.is_failure(f"{client_input} - STUN client translation not found.") continue # Extract the transaction ID from the bindings transaction_id = next(iter(bindings.keys())) - # If public address is provided, add it to the actual and expected STUN data + # Verifying the public address if provided if input_public_address and str(input_public_address) != (actual_public_address := get_value(bindings, f"{transaction_id}.publicAddress.ip")): - self.result.is_failure(f"{client_input} - Incorrect public-facing address; Expected: {input_public_address} Actual: {actual_public_address}") + self.result.is_failure(f"{client_input} - Incorrect public-facing address - Expected: {input_public_address} Actual: {actual_public_address}") - # If public port is provided, add it to the actual and expected STUN data + # Verifying the public port if provided if input_public_port and input_public_port != (actual_public_port := get_value(bindings, f"{transaction_id}.publicAddress.port")): - self.result.is_failure(f"{client_input} - Incorrect public-facing port; Expected: {input_public_port} Actual: {actual_public_port}") + self.result.is_failure(f"{client_input} - Incorrect public-facing port - Expected: {input_public_port} Actual: {actual_public_port}") class VerifyStunServer(AntaTest): diff --git a/tests/units/anta_tests/test_stun.py b/tests/units/anta_tests/test_stun.py index 0a6e5d4da..d893a3732 100644 --- a/tests/units/anta_tests/test_stun.py +++ b/tests/units/anta_tests/test_stun.py @@ -88,8 +88,8 @@ "expected": { "result": "failure", "messages": [ - "Client 100.64.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.164.3.2 Actual: 192.64.3.2", - "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", + "Client 100.64.3.2 Port: 4500 - Incorrect public-facing address - Expected: 192.164.3.2 Actual: 192.64.3.2", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address - Expected: 192.118.3.2 Actual: 192.18.3.2", ], }, }, @@ -108,7 +108,7 @@ }, "expected": { "result": "failure", - "messages": ["Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", "Client 172.18.3.2 Port: 4500 - STUN client transaction not found."], + "messages": ["Client 100.64.3.2 Port: 4500 - STUN client translation not found.", "Client 172.18.3.2 Port: 4500 - STUN client translation not found."], }, }, { @@ -134,9 +134,9 @@ "expected": { "result": "failure", "messages": [ - "Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", - "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", - "Client 172.18.3.2 Port: 4500 - Incorrect public-facing port; Expected: 6006 Actual: 4800", + "Client 100.64.3.2 Port: 4500 - STUN client translation not found.", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing address - Expected: 192.118.3.2 Actual: 192.18.3.2", + "Client 172.18.3.2 Port: 4500 - Incorrect public-facing port - Expected: 6006 Actual: 4800", ], }, }, @@ -163,9 +163,9 @@ "expected": { "result": "failure", "messages": [ - "Client 100.64.3.2 Port: 4500 - STUN client transaction not found.", - "Client 172.18.4.2 Port: 4800 - Incorrect public-facing address; Expected: 192.118.3.2 Actual: 192.18.3.2", - "Client 172.18.4.2 Port: 4800 - Incorrect public-facing port; Expected: 6006 Actual: 4800", + "Client 100.64.3.2 Port: 4500 - STUN client translation not found.", + "Client 172.18.4.2 Port: 4800 - Incorrect public-facing address - Expected: 192.118.3.2 Actual: 192.18.3.2", + "Client 172.18.4.2 Port: 4800 - Incorrect public-facing port - Expected: 6006 Actual: 4800", ], }, }, From d3e46adffd5cbe23dbac962bafef066c9f989ca2 Mon Sep 17 00:00:00 2001 From: Guillaume Mulocher Date: Fri, 29 Nov 2024 12:10:13 +0100 Subject: [PATCH 03/13] Apply suggestions from code review --- anta/tests/stun.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 5d0bfe64e..563760022 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -15,20 +15,20 @@ class VerifyStunClient(AntaTest): - """Verifies the STUN client configuration. + """Verifies the translation for a source address on a STUN client. This test performs the following checks for each specified address family: - 1. Validates that there is a translation for the STUN client. + 1. Validates that there is a translation for the source address on the STUN client. 2. If public IP and port details are provided, validates their correctness against the configuration. Expected Results ---------------- * Success: If all of the following conditions are met: - - The test will pass if the STUN client is correctly configured. - - If public IP and port details are provided, they must also match the configuration. + - The test will pass if the source address translation is present. + - If public IP and port details are provided, they must also match the translation information. * Failure: If any of the following occur: - - The STUN client is not configured. + - There is no translation for the source address on the STUN client. - The public IP or port details, if specified, are incorrect. Examples From 2520a0fe2bfd49e0559e5d46786ff0624c8a909f Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Mon, 2 Dec 2024 06:10:05 -0500 Subject: [PATCH 04/13] Updated test name and model name --- anta/input_models/stun.py | 6 +++--- anta/tests/stun.py | 14 +++++++------- examples/tests.yaml | 3 +-- tests/units/anta_tests/test_stun.py | 12 ++++++------ 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/anta/input_models/stun.py b/anta/input_models/stun.py index 329304d06..d1af40508 100644 --- a/anta/input_models/stun.py +++ b/anta/input_models/stun.py @@ -12,8 +12,8 @@ from anta.custom_types import Port -class ClientAddress(BaseModel): - """STUN (Session Traversal Utilities for NAT) model represents the configuration of an IPv4-based client.""" +class StunClientTranslation(BaseModel): + """STUN (Session Traversal Utilities for NAT) model represents the configuration of an IPv4-based client translations.""" model_config = ConfigDict(extra="forbid") source_address: IPv4Address @@ -26,7 +26,7 @@ class ClientAddress(BaseModel): """The public-facing port number of the STUN client, discovered via the STUN server.""" def __str__(self) -> str: - """Return a human-readable string representation of the ClientAddress for reporting. + """Return a human-readable string representation of the StunClientTranslation for reporting. Examples -------- diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 563760022..b6ae4127e 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -9,12 +9,12 @@ from typing import ClassVar -from anta.input_models.stun import ClientAddress +from anta.input_models.stun import StunClientTranslation from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value -class VerifyStunClient(AntaTest): +class VerifyStunClientTranslation(AntaTest): """Verifies the translation for a source address on a STUN client. This test performs the following checks for each specified address family: @@ -35,7 +35,7 @@ class VerifyStunClient(AntaTest): -------- ```yaml anta.tests.stun: - - VerifyStunClient: + - VerifyStunClientTranslation: stun_clients: - source_address: 172.18.3.2 public_address: 172.18.3.21 @@ -52,11 +52,11 @@ class VerifyStunClient(AntaTest): commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}", revision=1)] class Input(AntaTest.Input): - """Input model for the VerifyStunClient test.""" + """Input model for the VerifyStunClientTranslation test.""" - stun_clients: list[ClientAddress] + stun_clients: list[StunClientTranslation] """List of STUN clients.""" - ClientAddress: ClassVar[type[ClientAddress]] = ClientAddress + StunClientTranslation: ClassVar[type[StunClientTranslation]] = StunClientTranslation def render(self, template: AntaTemplate) -> list[AntaCommand]: """Render the template for each STUN translation.""" @@ -64,7 +64,7 @@ def render(self, template: AntaTemplate) -> list[AntaCommand]: @AntaTest.anta_test def test(self) -> None: - """Main test function for VerifyStunClient.""" + """Main test function for VerifyStunClientTranslation.""" self.result.is_success() # Iterate over each command output and corresponding client input diff --git a/examples/tests.yaml b/examples/tests.yaml index 273d20a5b..ed9772108 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -744,8 +744,7 @@ anta.tests.stp: # Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. threshold: 10 anta.tests.stun: - - VerifyStunClient: - # Verifies STUN client settings, including local IP/port and optionally public IP/port. + - VerifyStunClientTranslation: stun_clients: - source_address: 172.18.3.2 public_address: 172.18.3.21 diff --git a/tests/units/anta_tests/test_stun.py b/tests/units/anta_tests/test_stun.py index d893a3732..23834831a 100644 --- a/tests/units/anta_tests/test_stun.py +++ b/tests/units/anta_tests/test_stun.py @@ -7,13 +7,13 @@ from typing import Any -from anta.tests.stun import VerifyStunClient, VerifyStunServer +from anta.tests.stun import VerifyStunClientTranslation, VerifyStunServer from tests.units.anta_tests import test DATA: list[dict[str, Any]] = [ { "name": "success", - "test": VerifyStunClient, + "test": VerifyStunClientTranslation, "eos_data": [ { "bindings": { @@ -60,7 +60,7 @@ }, { "name": "failure-incorrect-public-ip", - "test": VerifyStunClient, + "test": VerifyStunClientTranslation, "eos_data": [ { "bindings": { @@ -95,7 +95,7 @@ }, { "name": "failure-no-client", - "test": VerifyStunClient, + "test": VerifyStunClientTranslation, "eos_data": [ {"bindings": {}}, {"bindings": {}}, @@ -113,7 +113,7 @@ }, { "name": "failure-incorrect-public-port", - "test": VerifyStunClient, + "test": VerifyStunClientTranslation, "eos_data": [ {"bindings": {}}, { @@ -142,7 +142,7 @@ }, { "name": "failure-all-type", - "test": VerifyStunClient, + "test": VerifyStunClientTranslation, "eos_data": [ {"bindings": {}}, { From 8e1e49bc272632f55bae4e884a558c727522b202 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 3 Dec 2024 08:02:37 -0500 Subject: [PATCH 05/13] Added deprecation to maintain backward compatibility --- anta/tests/stun.py | 33 ++++++++++++++++++++++++++++++++- examples/tests.yaml | 8 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index b6ae4127e..b050ea7d5 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -7,8 +7,10 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import ClassVar +from typing import Any, ClassVar +from warnings import warn +from anta.decorators import deprecated_test from anta.input_models.stun import StunClientTranslation from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -90,6 +92,35 @@ def test(self) -> None: self.result.is_failure(f"{client_input} - Incorrect public-facing port - Expected: {input_public_port} Actual: {actual_public_port}") +class VerifyStunClient(VerifyStunClientTranslation): + """[deprecated] Verifies the translation for a source address on a STUN client. + + Alias for the VerifyStunClientTranslation test to maintain backward compatibility. + When initialized, it will emit a deprecation warning and call the VerifyStunClientTranslation test. + + TODO: Remove this class in ANTA v2.0.0. + + Examples + -------- + ```yaml + anta.tests.stun: + - VerifyStunClient: + stun_clients: + - source_address: 172.18.3.2 + public_address: 172.18.3.21 + source_port: 4500 + public_port: 6006 + ``` + """ + + name = "VerifyStunClient" + description = "[deprecated] Verifies the translation for a source address on a STUN client." + + @deprecated_test("VerifyStunClientTranslation") + def test(self): + super.test() + + class VerifyStunServer(AntaTest): """Verifies the STUN server status is enabled and running. diff --git a/examples/tests.yaml b/examples/tests.yaml index ed9772108..acba8a456 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -744,7 +744,15 @@ anta.tests.stp: # Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. threshold: 10 anta.tests.stun: + - VerifyStunClient: + # Verifies the translation for a source address on a STUN client. + stun_clients: + - source_address: 172.18.3.2 + public_address: 172.18.3.21 + source_port: 4500 + public_port: 6006 - VerifyStunClientTranslation: + # Verifies the translation for a source address on a STUN client. stun_clients: - source_address: 172.18.3.2 public_address: 172.18.3.21 From b19ae2a869fedd7b4bdbfd8eb097cbe8f313c09f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:05:50 +0000 Subject: [PATCH 06/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- anta/tests/stun.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index b050ea7d5..a591ad6e7 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -7,8 +7,7 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import Any, ClassVar -from warnings import warn +from typing import ClassVar from anta.decorators import deprecated_test from anta.input_models.stun import StunClientTranslation @@ -118,7 +117,7 @@ class VerifyStunClient(VerifyStunClientTranslation): @deprecated_test("VerifyStunClientTranslation") def test(self): - super.test() + super().test() class VerifyStunServer(AntaTest): From a2e207e36970aa8c4a4282e1e3c1fccd991f4ad8 Mon Sep 17 00:00:00 2001 From: gmuloc Date: Tue, 3 Dec 2024 14:48:09 +0100 Subject: [PATCH 07/13] Feat: Add a class decortor to deprecate test and make every linter happy --- anta/decorators.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ anta/tests/stun.py | 12 +++++------- examples/tests.yaml | 2 +- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/anta/decorators.py b/anta/decorators.py index f5608ef26..c3f662855 100644 --- a/anta/decorators.py +++ b/anta/decorators.py @@ -62,6 +62,52 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: return decorator +def deprecated_test_class(new_tests: list[str] | None = None) -> Callable[[type[AntaTest]], type[AntaTest]]: + """Return a decorator to log a message of WARNING severity when a test is deprecated. + + Parameters + ---------- + new_tests + A list of new test classes that should replace the deprecated test. + + Returns + ------- + Callable[[type], type] + A decorator that can be used to wrap test functions. + + """ + + def decorator(cls: type[AntaTest]) -> type[AntaTest]: + """Actual decorator that logs the message. + + Parameters + ---------- + cls + The cls to be decorated. + + Returns + ------- + cls + The decorated cls. + """ + orig_init = cls.__init__ + + def new_init(*args: Any, **kwargs: Any) -> None: + """Overload __init__ to generate a warning message for deprecation.""" + if new_tests: + new_test_names = ", ".join(new_tests) + logger.warning("%s test is deprecated. Consider using the following new tests: %s.", cls.name, new_test_names) + else: + logger.warning("%s test is deprecated.", cls.name) + orig_init(*args, **kwargs) + + # NOTE: we are ignoring mypy warning as we want to assign to a method here + cls.__init__ = new_init # type: ignore[method-assign] + return cls + + return decorator + + def skip_on_platforms(platforms: list[str]) -> Callable[[F], F]: """Return a decorator to skip a test based on the device's hardware model. diff --git a/anta/tests/stun.py b/anta/tests/stun.py index a591ad6e7..3dd72f6c8 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -9,7 +9,7 @@ from typing import ClassVar -from anta.decorators import deprecated_test +from anta.decorators import deprecated_test_class from anta.input_models.stun import StunClientTranslation from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -91,8 +91,9 @@ def test(self) -> None: self.result.is_failure(f"{client_input} - Incorrect public-facing port - Expected: {input_public_port} Actual: {actual_public_port}") +@deprecated_test_class(new_tests=["VerifyStunClientTranslation"]) class VerifyStunClient(VerifyStunClientTranslation): - """[deprecated] Verifies the translation for a source address on a STUN client. + """(deprecated) Verifies the translation for a source address on a STUN client. Alias for the VerifyStunClientTranslation test to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the VerifyStunClientTranslation test. @@ -112,12 +113,9 @@ class VerifyStunClient(VerifyStunClientTranslation): ``` """ + # required to redefine name an description to overwrite parent class. name = "VerifyStunClient" - description = "[deprecated] Verifies the translation for a source address on a STUN client." - - @deprecated_test("VerifyStunClientTranslation") - def test(self): - super().test() + description = "(deprecated) Verifies the translation for a source address on a STUN client." class VerifyStunServer(AntaTest): diff --git a/examples/tests.yaml b/examples/tests.yaml index acba8a456..f65291df8 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -745,7 +745,7 @@ anta.tests.stp: threshold: 10 anta.tests.stun: - VerifyStunClient: - # Verifies the translation for a source address on a STUN client. + # (deprecated) Verifies the translation for a source address on a STUN client. stun_clients: - source_address: 172.18.3.2 public_address: 172.18.3.21 From 409158d0968482d4f4dc7473ae57003d3fc44f45 Mon Sep 17 00:00:00 2001 From: gmuloc Date: Tue, 3 Dec 2024 14:55:09 +0100 Subject: [PATCH 08/13] Test: We do have a new test... --- tests/units/cli/get/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/units/cli/get/test_commands.py b/tests/units/cli/get/test_commands.py index 6a842bf04..8edfa73b5 100644 --- a/tests/units/cli/get/test_commands.py +++ b/tests/units/cli/get/test_commands.py @@ -383,7 +383,7 @@ def test_from_ansible_overwrite( None, False, True, - "There are 2 tests available in 'anta.tests.stun'", + "There are 3 tests available in 'anta.tests.stun'", ExitCode.OK, id="Get multiple test count", ), From bf9ab51a409088f81a43541a4c6f3593dbcf1d12 Mon Sep 17 00:00:00 2001 From: gmuloc Date: Wed, 4 Dec 2024 17:28:40 +0100 Subject: [PATCH 09/13] test: Add tests for new decoarator --- anta/decorators.py | 3 +- tests/units/test_decorators.py | 77 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/units/test_decorators.py diff --git a/anta/decorators.py b/anta/decorators.py index c3f662855..08fbd6532 100644 --- a/anta/decorators.py +++ b/anta/decorators.py @@ -17,7 +17,8 @@ F = TypeVar("F", bound=Callable[..., Any]) -def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]: +# TODO: Remove this decorator in ANTA v2.0.0 in favor of deprecated_test_class +def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]: # pragma: no cover """Return a decorator to log a message of WARNING severity when a test is deprecated. Parameters diff --git a/tests/units/test_decorators.py b/tests/units/test_decorators.py new file mode 100644 index 000000000..ac7aa035a --- /dev/null +++ b/tests/units/test_decorators.py @@ -0,0 +1,77 @@ +# 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. +"""test anta.decorators.py.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, ClassVar + +import pytest + +from anta.decorators import deprecated_test_class, skip_on_platforms +from anta.models import AntaCommand, AntaTemplate, AntaTest + +if TYPE_CHECKING: + from anta.device import AntaDevice + + +class ExampleTest(AntaTest): + """ANTA test that always succeed.""" + + categories: ClassVar[list[str]] = [] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [] + + @AntaTest.anta_test + def test(self) -> None: + """Test function.""" + self.result.is_success() + + +@pytest.mark.parametrize( + "new_tests", + [ + pytest.param(None, id="No new_tests"), + pytest.param(["NewExampleTest"], id="one new_tests"), + pytest.param(["NewExampleTest1", "NewExampleTest2"], id="multiple new_tests"), + ], +) +def test_deprecated_test_class(caplog: pytest.LogCaptureFixture, device: AntaDevice, new_tests: list[str] | None) -> None: + """Test deprecated_test_class decorator.""" + caplog.set_level(logging.INFO) + + decorated_test_class = deprecated_test_class(new_tests=new_tests)(ExampleTest) + + # Initialize the decorated test + decorated_test_class(device) + + if new_tests is None: + assert "ExampleTest test is deprecated." in caplog.messages + else: + assert f"ExampleTest test is deprecated. Consider using the following new tests: {", ".join(new_tests)}." in caplog.messages + + +@pytest.mark.parametrize( + ("platforms", "device_platform", "expected_result"), + [ + pytest.param([], "cEOS-lab", "success", id="empty platforms"), + pytest.param(["cEOS-lab"], "cEOS-lab", "skipped", id="skip on one platform - match"), + pytest.param(["cEOS-lab"], "vEOS", "success", id="skip on one platform - no match"), + pytest.param(["cEOS-lab", "vEOS"], "cEOS-lab", "skipped", id="skip on multiple platforms - match"), + ], +) +async def test_skip_on_platforms(device: AntaDevice, platforms: list[str], device_platform: str, expected_result: str) -> None: + """Test skip_on_platforms decorator. + + Leverage the ExampleTest defined at the top of the module. + """ + # Apply the decorator + ExampleTest.test = skip_on_platforms(platforms)(ExampleTest.test) + + device.hw_model = device_platform + + test_instance = ExampleTest(device) + await test_instance.test() + + assert test_instance.result.result == expected_result From 054b65cf1cef11712f4cb3a9769ac797f4d0aeaa Mon Sep 17 00:00:00 2001 From: gmuloc Date: Wed, 4 Dec 2024 17:35:24 +0100 Subject: [PATCH 10/13] ci: Make mypy ignore test stuff again --- tests/units/test_decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/units/test_decorators.py b/tests/units/test_decorators.py index ac7aa035a..bdca6a291 100644 --- a/tests/units/test_decorators.py +++ b/tests/units/test_decorators.py @@ -66,8 +66,8 @@ async def test_skip_on_platforms(device: AntaDevice, platforms: list[str], devic Leverage the ExampleTest defined at the top of the module. """ - # Apply the decorator - ExampleTest.test = skip_on_platforms(platforms)(ExampleTest.test) + # Apply the decorator - ignoring mypy warning - this is for testing + ExampleTest.test = skip_on_platforms(platforms)(ExampleTest.test) # type: ignore[method-assign] device.hw_model = device_platform From 476e74f79e1e2bc73e0f5a3dd5c909a4ebac80b6 Mon Sep 17 00:00:00 2001 From: gmuloc Date: Thu, 5 Dec 2024 09:18:43 +0100 Subject: [PATCH 11/13] test: Make it backward compatible you python3.13 obnoxious user --- tests/units/test_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/units/test_decorators.py b/tests/units/test_decorators.py index bdca6a291..c267df1d1 100644 --- a/tests/units/test_decorators.py +++ b/tests/units/test_decorators.py @@ -49,7 +49,7 @@ def test_deprecated_test_class(caplog: pytest.LogCaptureFixture, device: AntaDev if new_tests is None: assert "ExampleTest test is deprecated." in caplog.messages else: - assert f"ExampleTest test is deprecated. Consider using the following new tests: {", ".join(new_tests)}." in caplog.messages + assert f"ExampleTest test is deprecated. Consider using the following new tests: {', '.join(new_tests)}." in caplog.messages @pytest.mark.parametrize( From ae7a4d24a4186af380079089ffdcfd9cd988c6e3 Mon Sep 17 00:00:00 2001 From: Guillaume Mulocher Date: Thu, 5 Dec 2024 09:42:33 +0100 Subject: [PATCH 12/13] Update anta/tests/stun.py --- anta/tests/stun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 3dd72f6c8..78d1de474 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -93,7 +93,7 @@ def test(self) -> None: @deprecated_test_class(new_tests=["VerifyStunClientTranslation"]) class VerifyStunClient(VerifyStunClientTranslation): - """(deprecated) Verifies the translation for a source address on a STUN client. + """(Deprecated) Verifies the translation for a source address on a STUN client. Alias for the VerifyStunClientTranslation test to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the VerifyStunClientTranslation test. From a2c14282d423553be12156ae2646396b5c6de87c Mon Sep 17 00:00:00 2001 From: gmuloc Date: Thu, 5 Dec 2024 10:38:30 +0100 Subject: [PATCH 13/13] refactor: The real one --- anta/tests/stun.py | 2 +- examples/tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/anta/tests/stun.py b/anta/tests/stun.py index 78d1de474..925abd147 100644 --- a/anta/tests/stun.py +++ b/anta/tests/stun.py @@ -115,7 +115,7 @@ class VerifyStunClient(VerifyStunClientTranslation): # required to redefine name an description to overwrite parent class. name = "VerifyStunClient" - description = "(deprecated) Verifies the translation for a source address on a STUN client." + description = "(Deprecated) Verifies the translation for a source address on a STUN client." class VerifyStunServer(AntaTest): diff --git a/examples/tests.yaml b/examples/tests.yaml index 0cc23cb21..e14b40823 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -745,7 +745,7 @@ anta.tests.stp: threshold: 10 anta.tests.stun: - VerifyStunClient: - # (deprecated) Verifies the translation for a source address on a STUN client. + # (Deprecated) Verifies the translation for a source address on a STUN client. stun_clients: - source_address: 172.18.3.2 public_address: 172.18.3.21