From 164b736e4d0b2c1c6394100007c47024f357515e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:07:21 +0100 Subject: [PATCH 1/4] chore: update codespell requirement from <2.4.0,>=2.2.6 to >=2.2.6,<2.5.0 (#1018) chore: update codespell requirement Updates the requirements on [codespell](https://github.com/codespell-project/codespell) to permit the latest version. - [Release notes](https://github.com/codespell-project/codespell/releases) - [Commits](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.4.0) --- updated-dependencies: - dependency-name: codespell dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d47f6897..306d2521a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ cli = [ ] dev = [ "bumpver>=2023.1129", - "codespell>=2.2.6,<2.4.0", + "codespell>=2.2.6,<2.5.0", "mypy-extensions~=1.0", "mypy~=1.10", "pre-commit>=3.3.3", From e09488bbe87f51f59199b764221936c5b53f0ccb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:58:51 +0100 Subject: [PATCH 2/4] ci: pre-commit autoupdate (#1020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.2 → v0.9.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.2...v0.9.3) - [github.com/codespell-project/codespell: v2.3.0 → v2.4.0](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0) - [github.com/igorshubovych/markdownlint-cli: v0.43.0 → v0.44.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.43.0...v0.44.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0bf3c72d..a47b84e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - '' - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.2 + rev: v0.9.3 hooks: - id: ruff name: Run Ruff linter @@ -76,7 +76,7 @@ repos: - respx - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.0 hooks: - id: codespell name: Checks for common misspellings in text files. @@ -100,7 +100,7 @@ repos: files: ^(anta|tests)/ - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.43.0 + rev: v0.44.0 hooks: - id: markdownlint name: Check Markdown files style. From 461fa69be4802edde97c6f2b05b92a4cd45dee2a Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:34:51 +0530 Subject: [PATCH 3/4] fix(anta.tests): Cleaning up flow-tracking tests module (#964) * Refactored VerifyHardwareFlowTrackerStatus test * addressed review comments: updated failure msga, test doc * reverted the latest revision * Addressed review comments: updated docstring * Update anta/tests/flow_tracking.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestions from code review --------- Co-authored-by: Guillaume Mulocher Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- anta/input_models/flow_tracking.py | 72 ++++++++ anta/tests/flow_tracking.py | 171 +++++++------------ docs/api/tests.flow_tracking.md | 18 ++ examples/tests.yaml | 2 +- tests/units/anta_tests/test_flow_tracking.py | 120 ++++--------- 5 files changed, 185 insertions(+), 198 deletions(-) create mode 100644 anta/input_models/flow_tracking.py diff --git a/anta/input_models/flow_tracking.py b/anta/input_models/flow_tracking.py new file mode 100644 index 000000000..79676d937 --- /dev/null +++ b/anta/input_models/flow_tracking.py @@ -0,0 +1,72 @@ +# Copyright (c) 2023-2025 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 flow tracking tests.""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + + +class FlowTracker(BaseModel): + """Flow Tracking model representing the tracker details.""" + + model_config = ConfigDict(extra="forbid") + name: str + """The name of the flow tracker.""" + record_export: RecordExport | None = None + """Configuration for record export, specifying details about timeouts.""" + exporters: list[Exporter] | None = None + """A list of exporters associated with the flow tracker.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the FlowTracker for reporting. + + Examples + -------- + Flow Tracker: FLOW-TRACKER + + """ + return f"Flow Tracker: {self.name}" + + +class RecordExport(BaseModel): + """Model representing the record export configuration for a flow tracker.""" + + model_config = ConfigDict(extra="forbid") + on_inactive_timeout: int + """The timeout in milliseconds for exporting flow records when the flow becomes inactive.""" + on_interval: int + """The interval in milliseconds for exporting flow records.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the RecordExport for reporting. + + Examples + -------- + Inactive Timeout: 60000, Active Interval: 300000 + + """ + return f"Inactive Timeout: {self.on_inactive_timeout}, Active Interval: {self.on_interval}" + + +class Exporter(BaseModel): + """Model representing the exporter used for flow record export.""" + + model_config = ConfigDict(extra="forbid") + name: str + """The name of the exporter.""" + local_interface: str + """The local interface used by the exporter to send flow records.""" + template_interval: int + """The template interval, in milliseconds, for the exporter to refresh the flow template.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the Exporter for reporting. + + Examples + -------- + Exporter: CVP-TELEMETRY + + """ + return f"Exporter: {self.name}" diff --git a/anta/tests/flow_tracking.py b/anta/tests/flow_tracking.py index 245d1fd8c..5c10b302d 100644 --- a/anta/tests/flow_tracking.py +++ b/anta/tests/flow_tracking.py @@ -9,37 +9,13 @@ from typing import ClassVar -from pydantic import BaseModel - from anta.decorators import skip_on_platforms +from anta.input_models.flow_tracking import FlowTracker from anta.models import AntaCommand, AntaTemplate, AntaTest -from anta.tools import get_failed_logs - +from anta.tools import get_value -def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str: - """Validate the record export configuration against the tracker info. - - Parameters - ---------- - record_export - The expected record export configuration. - tracker_info - The actual tracker info from the command output. - Returns - ------- - str - A failure message if the record export configuration does not match, otherwise blank string. - """ - failed_log = "" - actual_export = {"inactive timeout": tracker_info.get("inactiveTimeout"), "interval": tracker_info.get("activeInterval")} - expected_export = {"inactive timeout": record_export.get("on_inactive_timeout"), "interval": record_export.get("on_interval")} - if actual_export != expected_export: - failed_log = get_failed_logs(expected_export, actual_export) - return failed_log - - -def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str: +def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> list[str]: """Validate the exporter configurations against the tracker info. Parameters @@ -51,36 +27,52 @@ def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, Returns ------- - str - Failure message if any exporter configuration does not match. + list + List of failure messages for any exporter configuration that does not match. """ - failed_log = "" + failure_messages = [] for exporter in exporters: - exporter_name = exporter["name"] + exporter_name = exporter.name actual_exporter_info = tracker_info["exporters"].get(exporter_name) if not actual_exporter_info: - failed_log += f"\nExporter `{exporter_name}` is not configured." + failure_messages.append(f"{exporter} - Not configured") continue + local_interface = actual_exporter_info["localIntf"] + template_interval = actual_exporter_info["templateInterval"] - expected_exporter_data = {"local interface": exporter["local_interface"], "template interval": exporter["template_interval"]} - actual_exporter_data = {"local interface": actual_exporter_info["localIntf"], "template interval": actual_exporter_info["templateInterval"]} + if local_interface != exporter.local_interface: + failure_messages.append(f"{exporter} - Incorrect local interface - Expected: {exporter.local_interface}, Actual: {local_interface}") - if expected_exporter_data != actual_exporter_data: - failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data) - failed_log += f"\nExporter `{exporter_name}`: {failed_msg}" - return failed_log + if template_interval != exporter.template_interval: + failure_messages.append(f"{exporter} - Incorrect template interval - Expected: {exporter.template_interval}, Actual: {template_interval}") + return failure_messages class VerifyHardwareFlowTrackerStatus(AntaTest): - """Verifies if hardware flow tracking is running and an input tracker is active. + """Verifies the hardware flow tracking state. + + This test performs the following checks: - This test optionally verifies the tracker interval/timeout and exporter configuration. + 1. Confirms that hardware flow tracking is running. + 2. For each specified flow tracker: + - Confirms that the tracker is active. + - Optionally, checks the tracker interval/timeout configuration. + - Optionally, verifies the tracker exporter configuration Expected Results ---------------- - * Success: The test will pass if hardware flow tracking is running and an input tracker is active. - * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, - or the tracker interval/timeout and exporter configuration does not match the expected values. + * Success: The test will pass if all of the following conditions are met: + - Hardware flow tracking is running. + - For each specified flow tracker: + - The flow tracker is active. + - The tracker interval/timeout matches the expected values, if provided. + - The exporter configuration matches the expected values, if provided. + * Failure: The test will fail if any of the following conditions are met: + - Hardware flow tracking is not running. + - For any specified flow tracker: + - The flow tracker is not active. + - The tracker interval/timeout does not match the expected values, if provided. + - The exporter configuration does not match the expected values, if provided. Examples -------- @@ -99,11 +91,8 @@ class VerifyHardwareFlowTrackerStatus(AntaTest): ``` """ - description = ( - "Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration." - ) categories: ClassVar[list[str]] = ["flow tracking"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show flow tracking hardware tracker {name}", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show flow tracking hardware", revision=1)] class Input(AntaTest.Input): """Input model for the VerifyHardwareFlowTrackerStatus test.""" @@ -111,82 +100,42 @@ class Input(AntaTest.Input): trackers: list[FlowTracker] """List of flow trackers to verify.""" - class FlowTracker(BaseModel): - """Detail of a flow tracker.""" - - name: str - """Name of the flow tracker.""" - - record_export: RecordExport | None = None - """Record export configuration for the flow tracker.""" - - exporters: list[Exporter] | None = None - """List of exporters for the flow tracker.""" - - class RecordExport(BaseModel): - """Record export configuration.""" - - on_inactive_timeout: int - """Timeout in milliseconds for exporting records when inactive.""" - - on_interval: int - """Interval in milliseconds for exporting records.""" - - class Exporter(BaseModel): - """Detail of an exporter.""" - - name: str - """Name of the exporter.""" - - local_interface: str - """Local interface used by the exporter.""" - - template_interval: int - """Template interval in milliseconds for the exporter.""" - - def render(self, template: AntaTemplate) -> list[AntaCommand]: - """Render the template for each hardware tracker.""" - return [template.render(name=tracker.name) for tracker in self.inputs.trackers] - @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyHardwareFlowTrackerStatus.""" self.result.is_success() - for command, tracker_input in zip(self.instance_commands, self.inputs.trackers): - hardware_tracker_name = command.params.name - record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None - exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None - command_output = command.json_output - # Check if hardware flow tracking is configured - if not command_output.get("running"): - self.result.is_failure("Hardware flow tracking is not running.") - return + command_output = self.instance_commands[0].json_output + # Check if hardware flow tracking is configured + if not command_output.get("running"): + self.result.is_failure("Hardware flow tracking is not running.") + return + for tracker in self.inputs.trackers: # Check if the input hardware tracker is configured - tracker_info = command_output["trackers"].get(hardware_tracker_name) - if not tracker_info: - self.result.is_failure(f"Hardware flow tracker `{hardware_tracker_name}` is not configured.") + if not (tracker_info := get_value(command_output["trackers"], f"{tracker.name}")): + self.result.is_failure(f"{tracker} - Not found") continue # Check if the input hardware tracker is active if not tracker_info.get("active"): - self.result.is_failure(f"Hardware flow tracker `{hardware_tracker_name}` is not active.") + self.result.is_failure(f"{tracker} - Disabled") continue # Check the input hardware tracker timeouts - failure_msg = "" - if record_export: - record_export_failure = validate_record_export(record_export, tracker_info) - if record_export_failure: - failure_msg += record_export_failure - - # Check the input hardware tracker exporters' configuration - if exporters: - exporters_failure = validate_exporters(exporters, tracker_info) - if exporters_failure: - failure_msg += exporters_failure - - if failure_msg: - self.result.is_failure(f"{hardware_tracker_name}: {failure_msg}\n") + if tracker.record_export: + inactive_interval = tracker.record_export.on_inactive_timeout + on_interval = tracker.record_export.on_interval + act_inactive = tracker_info.get("inactiveTimeout") + act_interval = tracker_info.get("activeInterval") + if not all([inactive_interval == act_inactive, on_interval == act_interval]): + self.result.is_failure( + f"{tracker}, {tracker.record_export} - Incorrect timers - Inactive Timeout: {act_inactive}, OnActive Interval: {act_interval}" + ) + + # Check the input hardware tracker exporters configuration + if tracker.exporters: + failure_messages = validate_exporters(tracker.exporters, tracker_info) + for message in failure_messages: + self.result.is_failure(f"{tracker}, {message}") diff --git a/docs/api/tests.flow_tracking.md b/docs/api/tests.flow_tracking.md index e6e9df2d2..786f56ee7 100644 --- a/docs/api/tests.flow_tracking.md +++ b/docs/api/tests.flow_tracking.md @@ -7,6 +7,8 @@ anta_title: ANTA catalog for flow tracking tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.flow_tracking options: show_root_heading: false @@ -18,3 +20,19 @@ anta_title: ANTA catalog for flow tracking tests filters: - "!test" - "!render" + - "!validate_exporters" + +# Input models + +::: anta.input_models.flow_tracking + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + anta_hide_test_module_description: true + merge_init_into_class: false + show_labels: true + filters: + - "!^__init__" + - "!^__str__" diff --git a/examples/tests.yaml b/examples/tests.yaml index 6c64f5d9f..b231c4b6a 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -162,7 +162,7 @@ anta.tests.field_notices: # Verifies if the device is exposed to FN0072, and if the issue has been mitigated. anta.tests.flow_tracking: - VerifyHardwareFlowTrackerStatus: - # Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration. + # Verifies the hardware flow tracking state. trackers: - name: FLOW-TRACKER record_export: diff --git a/tests/units/anta_tests/test_flow_tracking.py b/tests/units/anta_tests/test_flow_tracking.py index 19f4d325b..6934c0219 100644 --- a/tests/units/anta_tests/test_flow_tracking.py +++ b/tests/units/anta_tests/test_flow_tracking.py @@ -22,18 +22,13 @@ "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER": { "active": True, "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } + }, }, "running": True, }, @@ -52,18 +47,13 @@ "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER": { "active": True, "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}}, - } + }, }, "running": True, }, @@ -113,7 +103,7 @@ "inputs": {"trackers": [{"name": "FLOW-Sample"}]}, "expected": { "result": "failure", - "messages": ["Hardware flow tracker `FLOW-Sample` is not configured."], + "messages": ["Flow Tracker: FLOW-Sample - Not found"], }, }, { @@ -127,18 +117,13 @@ "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER": { "active": False, "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}}, - } + }, }, "running": True, }, @@ -159,7 +144,7 @@ }, "expected": { "result": "failure", - "messages": ["Hardware flow tracker `FLOW-TRACKER` is not active.", "Hardware flow tracker `HARDWARE-TRACKER` is not active."], + "messages": ["Flow Tracker: FLOW-TRACKER - Disabled", "Flow Tracker: HARDWARE-TRACKER - Disabled"], }, }, { @@ -173,18 +158,13 @@ "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER": { "active": True, "inactiveTimeout": 6000, "activeInterval": 30000, "exporters": {"CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}}, - } + }, }, "running": True, }, @@ -204,10 +184,9 @@ "expected": { "result": "failure", "messages": [ - "FLOW-TRACKER: \n" - "Expected `6000` as the inactive timeout, but found `60000` instead.\nExpected `30000` as the interval, but found `300000` instead.\n", - "HARDWARE-TRACKER: \n" - "Expected `60000` as the inactive timeout, but found `6000` instead.\nExpected `300000` as the interval, but found `30000` instead.\n", + "Flow Tracker: FLOW-TRACKER, Inactive Timeout: 6000, Active Interval: 30000 - Incorrect timers - Inactive Timeout: 60000, OnActive Interval: 300000", + "Flow Tracker: HARDWARE-TRACKER, Inactive Timeout: 60000, Active Interval: 300000 - Incorrect timers - " + "Inactive Timeout: 6000, OnActive Interval: 30000", ], }, }, @@ -225,12 +204,7 @@ "CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}, "CVP-FLOW": {"localIntf": "Loopback0", "templateInterval": 3600000}, }, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER": { "active": True, "inactiveTimeout": 6000, @@ -239,7 +213,7 @@ "CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}, "Hardware-flow": {"localIntf": "Loopback10", "templateInterval": 3600000}, }, - } + }, }, "running": True, }, @@ -265,15 +239,11 @@ "expected": { "result": "failure", "messages": [ - "FLOW-TRACKER: \n" - "Exporter `CVP-FLOW`: \n" - "Expected `Loopback10` as the local interface, but found `Loopback0` instead.\n" - "Expected `3500000` as the template interval, but found `3600000` instead.\n", - "HARDWARE-TRACKER: \n" - "Exporter `Hardware-flow`: \n" - "Expected `Loopback99` as the local interface, but found `Loopback10` instead.\n" - "Expected `3000000` as the template interval, but found `3600000` instead.\n" - "Exporter `Reverse-flow` is not configured.\n", + "Flow Tracker: FLOW-TRACKER, Exporter: CVP-FLOW - Incorrect local interface - Expected: Loopback10, Actual: Loopback0", + "Flow Tracker: FLOW-TRACKER, Exporter: CVP-FLOW - Incorrect template interval - Expected: 3500000, Actual: 3600000", + "Flow Tracker: HARDWARE-TRACKER, Exporter: Hardware-flow - Incorrect local interface - Expected: Loopback99, Actual: Loopback10", + "Flow Tracker: HARDWARE-TRACKER, Exporter: Hardware-flow - Incorrect template interval - Expected: 3000000, Actual: 3600000", + "Flow Tracker: HARDWARE-TRACKER, Exporter: Reverse-flow - Not configured", ], }, }, @@ -288,34 +258,19 @@ "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "FLOW-TRIGGER": { "active": False, "inactiveTimeout": 60000, "activeInterval": 300000, "exporters": {"CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-FLOW": { "active": True, "inactiveTimeout": 6000, "activeInterval": 30000, "exporters": {"CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}}, - } - }, - "running": True, - }, - { - "trackers": { + }, "FLOW-TRACKER2": { "active": True, "inactiveTimeout": 60000, @@ -324,12 +279,7 @@ "CV-TELEMETRY": {"localIntf": "Loopback0", "templateInterval": 3600000}, "CVP-FLOW": {"localIntf": "Loopback0", "templateInterval": 3600000}, }, - } - }, - "running": True, - }, - { - "trackers": { + }, "HARDWARE-TRACKER2": { "active": True, "inactiveTimeout": 6000, @@ -338,7 +288,7 @@ "CVP-TELEMETRY": {"localIntf": "Loopback10", "templateInterval": 3600000}, "Hardware-flow": {"localIntf": "Loopback10", "templateInterval": 3600000}, }, - } + }, }, "running": True, }, @@ -374,17 +324,15 @@ "expected": { "result": "failure", "messages": [ - "Hardware flow tracker `FLOW-Sample` is not configured.", - "Hardware flow tracker `FLOW-TRIGGER` is not active.", - "HARDWARE-FLOW: \n" - "Expected `60000` as the inactive timeout, but found `6000` instead.\nExpected `300000` as the interval, but found `30000` instead.\n", - "FLOW-TRACKER2: \nExporter `CVP-FLOW`: \n" - "Expected `Loopback10` as the local interface, but found `Loopback0` instead.\n" - "Expected `3500000` as the template interval, but found `3600000` instead.\n", - "HARDWARE-TRACKER2: \nExporter `Hardware-flow`: \n" - "Expected `Loopback99` as the local interface, but found `Loopback10` instead.\n" - "Expected `3000000` as the template interval, but found `3600000` instead.\n" - "Exporter `Reverse-flow` is not configured.\n", + "Flow Tracker: FLOW-Sample - Not found", + "Flow Tracker: FLOW-TRIGGER - Disabled", + "Flow Tracker: HARDWARE-FLOW, Inactive Timeout: 60000, Active Interval: 300000 - Incorrect timers - " + "Inactive Timeout: 6000, OnActive Interval: 30000", + "Flow Tracker: FLOW-TRACKER2, Exporter: CVP-FLOW - Incorrect local interface - Expected: Loopback10, Actual: Loopback0", + "Flow Tracker: FLOW-TRACKER2, Exporter: CVP-FLOW - Incorrect template interval - Expected: 3500000, Actual: 3600000", + "Flow Tracker: HARDWARE-TRACKER2, Exporter: Hardware-flow - Incorrect local interface - Expected: Loopback99, Actual: Loopback10", + "Flow Tracker: HARDWARE-TRACKER2, Exporter: Hardware-flow - Incorrect template interval - Expected: 3000000, Actual: 3600000", + "Flow Tracker: HARDWARE-TRACKER2, Exporter: Reverse-flow - Not configured", ], }, }, From 1dd5a6085b7afbf171e0518cda77c7db057a0cc8 Mon Sep 17 00:00:00 2001 From: geetanjalimanegslab <96573243+geetanjalimanegslab@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:04:00 +0530 Subject: [PATCH 4/4] refactor(anta.tests): Refactor VerifySpecificPath test case (#965) --------- Co-authored-by: Geetanjali Mane Co-authored-by: Guillaume Mulocher --- anta/input_models/path_selection.py | 28 +++ anta/tests/path_selection.py | 98 +++++----- docs/api/tests.path_selection.md | 15 ++ examples/tests.yaml | 2 +- tests/units/anta_tests/test_path_selection.py | 171 ++++++++++++------ 5 files changed, 206 insertions(+), 108 deletions(-) create mode 100644 anta/input_models/path_selection.py diff --git a/anta/input_models/path_selection.py b/anta/input_models/path_selection.py new file mode 100644 index 000000000..1c8e977c2 --- /dev/null +++ b/anta/input_models/path_selection.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023-2025 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 path-selection tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Address + +from pydantic import BaseModel, ConfigDict + + +class DpsPath(BaseModel): + """Model for a list of DPS path entries.""" + + model_config = ConfigDict(extra="forbid") + peer: IPv4Address + """Static peer IPv4 address.""" + path_group: str + """Router path group name.""" + source_address: IPv4Address + """Source IPv4 address of path.""" + destination_address: IPv4Address + """Destination IPv4 address of path.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the DpsPath for reporting.""" + return f"Peer: {self.peer}, PathGroup: {self.path_group}, Source: {self.source_address}, Destination: {self.destination_address}" diff --git a/anta/tests/path_selection.py b/anta/tests/path_selection.py index 58b86860d..762c9d6ee 100644 --- a/anta/tests/path_selection.py +++ b/anta/tests/path_selection.py @@ -7,12 +7,10 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from ipaddress import IPv4Address from typing import ClassVar -from pydantic import BaseModel - from anta.decorators import skip_on_platforms +from anta.input_models.path_selection import DpsPath from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -70,16 +68,23 @@ def test(self) -> None: class VerifySpecificPath(AntaTest): - """Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. + """Verifies the DPS path and telemetry state of an IPv4 peer. - The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry. + This test performs the following checks: + + 1. Verifies that the specified peer is configured. + 2. Verifies that the specified path group is found. + 3. For each specified DPS path: + - Verifies that the expected source and destination address matches the expected. + - Verifies that the state is `ipsecEstablished` or `routeResolved`. + - Verifies that the telemetry state is `active`. Expected Results ---------------- - * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved' + * Success: The test will pass if the path state under router path-selection is either 'IPsecEstablished' or 'Resolved' and telemetry state as 'active'. - * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved', - or if the telemetry state is 'inactive'. + * Failure: The test will fail if router path selection or the peer is not configured or if the path state is not 'IPsec established' or 'Resolved', + or the telemetry state is 'inactive'. Examples -------- @@ -95,36 +100,15 @@ class VerifySpecificPath(AntaTest): """ categories: ClassVar[list[str]] = ["path-selection"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ - AntaTemplate(template="show path-selection paths peer {peer} path-group {group} source {source} destination {destination}", revision=1) - ] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show path-selection paths", revision=1)] class Input(AntaTest.Input): """Input model for the VerifySpecificPath test.""" - paths: list[RouterPath] + paths: list[DpsPath] """List of router paths to verify.""" - - class RouterPath(BaseModel): - """Detail of a router path.""" - - peer: IPv4Address - """Static peer IPv4 address.""" - - path_group: str - """Router path group name.""" - - source_address: IPv4Address - """Source IPv4 address of path.""" - - destination_address: IPv4Address - """Destination IPv4 address of path.""" - - def render(self, template: AntaTemplate) -> list[AntaCommand]: - """Render the template for each router path.""" - return [ - template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths - ] + RouterPath: ClassVar[type[DpsPath]] = DpsPath + """To maintain backward compatibility.""" @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test @@ -132,28 +116,42 @@ def test(self) -> None: """Main test function for VerifySpecificPath.""" self.result.is_success() - # Check the state of each path - for command in self.instance_commands: - peer = command.params.peer - path_group = command.params.group - source = command.params.source - destination = command.params.destination - command_output = command.json_output.get("dpsPeers", []) + command_output = self.instance_commands[0].json_output + + # If the dpsPeers details are not found in the command output, the test fails. + if not (dps_peers_details := get_value(command_output, "dpsPeers")): + self.result.is_failure("Router path-selection not configured") + return + # Iterating on each DPS peer mentioned in the inputs. + for dps_path in self.inputs.paths: + peer = str(dps_path.peer) + peer_details = dps_peers_details.get(peer, {}) # If the peer is not configured for the path group, the test fails - if not command_output: - self.result.is_failure(f"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.") + if not peer_details: + self.result.is_failure(f"{dps_path} - Peer not found") + continue + + path_group = dps_path.path_group + source = str(dps_path.source_address) + destination = str(dps_path.destination_address) + path_group_details = get_value(peer_details, f"dpsGroups..{path_group}..dpsPaths", separator="..") + # If the expected path group is not found for the peer, the test fails. + if not path_group_details: + self.result.is_failure(f"{dps_path} - No DPS path found for this peer and path group.") + continue + + path_data = next((path for path in path_group_details.values() if (path.get("source") == source and path.get("destination") == destination)), None) + # Source and destination address do not match, the test fails. + if not path_data: + self.result.is_failure(f"{dps_path} - No path matching the source and destination found") continue - # Extract the state of the path - path_output = get_value(command_output, f"{peer}..dpsGroups..{path_group}..dpsPaths", separator="..") - path_state = next(iter(path_output.values())).get("state") - session = get_value(next(iter(path_output.values())), "dpsSessions.0.active") + path_state = path_data.get("state") + session = get_value(path_data, "dpsSessions.0.active") # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails if path_state not in ["ipsecEstablished", "routeResolved"]: - self.result.is_failure(f"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.") + self.result.is_failure(f"{dps_path} - State is not in ipsecEstablished, routeResolved. Actual: {path_state}") elif not session: - self.result.is_failure( - f"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`." - ) + self.result.is_failure(f"{dps_path} - Telemetry state inactive for this path") diff --git a/docs/api/tests.path_selection.md b/docs/api/tests.path_selection.md index f4d41d6f2..68488b664 100644 --- a/docs/api/tests.path_selection.md +++ b/docs/api/tests.path_selection.md @@ -7,6 +7,8 @@ anta_title: ANTA catalog for Router path-selection tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.path_selection options: show_root_heading: false @@ -18,3 +20,16 @@ anta_title: ANTA catalog for Router path-selection tests filters: - "!test" - "!render" + +# Input models + +::: anta.input_models.path_selection + + 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: ["!^__str__"] diff --git a/examples/tests.yaml b/examples/tests.yaml index b231c4b6a..c417537da 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -345,7 +345,7 @@ anta.tests.path_selection: - VerifyPathsHealth: # Verifies the path and telemetry state of all paths under router path-selection. - VerifySpecificPath: - # Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. + # Verifies the DPS path and telemetry state of an IPv4 peer. paths: - peer: 10.255.0.1 path_group: internet diff --git a/tests/units/anta_tests/test_path_selection.py b/tests/units/anta_tests/test_path_selection.py index 08377e675..f003df959 100644 --- a/tests/units/anta_tests/test_path_selection.py +++ b/tests/units/anta_tests/test_path_selection.py @@ -160,105 +160,171 @@ "eos_data": [ { "dpsPeers": { - "10.255.0.1": { + "10.255.0.2": { "dpsGroups": { - "internet": { + "mpls": { "dpsPaths": { - "path3": { - "state": "ipsecEstablished", + "path7": {}, + "path8": { "source": "172.18.13.2", "destination": "172.18.15.2", + "state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}, - } + }, } - } + }, + "internet": {}, } - } - } - }, - { - "dpsPeers": { - "10.255.0.2": { + }, + "10.255.0.1": { "dpsGroups": { - "mpls": { + "internet": { "dpsPaths": { - "path2": { + "path6": { + "source": "100.64.3.2", + "destination": "100.64.1.2", "state": "ipsecEstablished", - "source": "172.18.3.2", - "destination": "172.18.5.2", "dpsSessions": {"0": {"active": True}}, } } - } + }, + "mpls": {}, } - } + }, } - }, + } ], "inputs": { "paths": [ - {"peer": "10.255.0.1", "path_group": "internet", "source_address": "172.18.3.2", "destination_address": "172.18.5.2"}, + {"peer": "10.255.0.1", "path_group": "internet", "source_address": "100.64.3.2", "destination_address": "100.64.1.2"}, {"peer": "10.255.0.2", "path_group": "mpls", "source_address": "172.18.13.2", "destination_address": "172.18.15.2"}, ] }, "expected": {"result": "success"}, }, { - "name": "failure-no-peer", + "name": "failure-expected-path-group-not-found", "test": VerifySpecificPath, "eos_data": [ - {"dpsPeers": {}}, - {"dpsPeers": {}}, + { + "dpsPeers": { + "10.255.0.2": { + "dpsGroups": {"internet": {}}, + }, + "10.255.0.1": {"peerName": "", "dpsGroups": {"mpls": {}}}, + } + } ], "inputs": { "paths": [ - {"peer": "10.255.0.1", "path_group": "internet", "source_address": "172.18.3.2", "destination_address": "172.18.5.2"}, + {"peer": "10.255.0.1", "path_group": "internet", "source_address": "100.64.3.2", "destination_address": "100.64.1.2"}, {"peer": "10.255.0.2", "path_group": "mpls", "source_address": "172.18.13.2", "destination_address": "172.18.15.2"}, ] }, "expected": { "result": "failure", "messages": [ - "Path `peer: 10.255.0.1 source: 172.18.3.2 destination: 172.18.5.2` is not configured for path-group `internet`.", - "Path `peer: 10.255.0.2 source: 172.18.13.2 destination: 172.18.15.2` is not configured for path-group `mpls`.", + "Peer: 10.255.0.1, PathGroup: internet, Source: 100.64.3.2, Destination: 100.64.1.2 - No DPS path found for this peer and path group", + "Peer: 10.255.0.2, PathGroup: mpls, Source: 172.18.13.2, Destination: 172.18.15.2 - No DPS path found for this peer and path group.", ], }, }, + { + "name": "failure-no-router-path-configured", + "test": VerifySpecificPath, + "eos_data": [{"dpsPeers": {}}], + "inputs": {"paths": [{"peer": "10.255.0.1", "path_group": "internet", "source_address": "100.64.3.2", "destination_address": "100.64.1.2"}]}, + "expected": {"result": "failure", "messages": ["Router path-selection not configured"]}, + }, + { + "name": "failure-no-specific-peer-configured", + "test": VerifySpecificPath, + "eos_data": [{"dpsPeers": {"10.255.0.2": {}}}], + "inputs": {"paths": [{"peer": "10.255.0.1", "path_group": "internet", "source_address": "172.18.3.2", "destination_address": "172.18.5.2"}]}, + "expected": {"result": "failure", "messages": ["Peer: 10.255.0.1, PathGroup: internet, Source: 172.18.3.2, Destination: 172.18.5.2 - Peer not found"]}, + }, { "name": "failure-not-established", "test": VerifySpecificPath, "eos_data": [ { "dpsPeers": { + "10.255.0.2": { + "dpsGroups": { + "mpls": { + "dpsPaths": { + "path7": {}, + "path8": { + "source": "172.18.13.2", + "destination": "172.18.15.2", + "state": "ipsecPending", + "dpsSessions": {"0": {"active": True}}, + }, + } + }, + "internet": {"dpsPaths": {}}, + } + }, "10.255.0.1": { "dpsGroups": { "internet": { "dpsPaths": { - "path3": {"state": "ipsecPending", "source": "172.18.3.2", "destination": "172.18.5.2", "dpsSessions": {"0": {"active": True}}} + "path6": {"source": "172.18.3.2", "destination": "172.18.5.2", "state": "ipsecPending", "dpsSessions": {"0": {"active": True}}} } - } + }, + "mpls": {"dpsPaths": {}}, } - } + }, } - }, + } + ], + "inputs": { + "paths": [ + {"peer": "10.255.0.1", "path_group": "internet", "source_address": "172.18.3.2", "destination_address": "172.18.5.2"}, + {"peer": "10.255.0.2", "path_group": "mpls", "source_address": "172.18.13.2", "destination_address": "172.18.15.2"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Peer: 10.255.0.1, PathGroup: internet, Source: 172.18.3.2, Destination: 172.18.5.2 - State is not in ipsecEstablished, routeResolved." + " Actual: ipsecPending", + "Peer: 10.255.0.2, PathGroup: mpls, Source: 172.18.13.2, Destination: 172.18.15.2 - State is not in ipsecEstablished, routeResolved." + " Actual: ipsecPending", + ], + }, + }, + { + "name": "failure-inactive", + "test": VerifySpecificPath, + "eos_data": [ { "dpsPeers": { "10.255.0.2": { "dpsGroups": { "mpls": { "dpsPaths": { - "path4": { - "state": "ipsecPending", + "path8": { "source": "172.18.13.2", "destination": "172.18.15.2", + "state": "routeResolved", "dpsSessions": {"0": {"active": False}}, } } } } - } + }, + "10.255.0.1": { + "dpsGroups": { + "internet": { + "dpsPaths": { + "path6": {"source": "172.18.3.2", "destination": "172.18.5.2", "state": "routeResolved", "dpsSessions": {"0": {"active": False}}} + } + } + } + }, } - }, + } ], "inputs": { "paths": [ @@ -269,46 +335,37 @@ "expected": { "result": "failure", "messages": [ - "Path state for `peer: 10.255.0.1 source: 172.18.3.2 destination: 172.18.5.2` in path-group internet is `ipsecPending`.", - "Path state for `peer: 10.255.0.2 source: 172.18.13.2 destination: 172.18.15.2` in path-group mpls is `ipsecPending`.", + "Peer: 10.255.0.1, PathGroup: internet, Source: 172.18.3.2, Destination: 172.18.5.2 - Telemetry state inactive for this path", + "Peer: 10.255.0.2, PathGroup: mpls, Source: 172.18.13.2, Destination: 172.18.15.2 - Telemetry state inactive for this path", ], }, }, { - "name": "failure-inactive", + "name": "failure-source-destination-not-configured", "test": VerifySpecificPath, "eos_data": [ { "dpsPeers": { - "10.255.0.1": { + "10.255.0.2": { "dpsGroups": { - "internet": { + "mpls": { "dpsPaths": { - "path3": {"state": "routeResolved", "source": "172.18.3.2", "destination": "172.18.5.2", "dpsSessions": {"0": {"active": False}}} + "path8": {"source": "172.18.3.2", "destination": "172.8.15.2", "state": "routeResolved", "dpsSessions": {"0": {"active": False}}} } } } - } - } - }, - { - "dpsPeers": { - "10.255.0.2": { + }, + "10.255.0.1": { "dpsGroups": { - "mpls": { + "internet": { "dpsPaths": { - "path4": { - "state": "routeResolved", - "source": "172.18.13.2", - "destination": "172.18.15.2", - "dpsSessions": {"0": {"active": False}}, - } + "path6": {"source": "172.8.3.2", "destination": "172.8.5.2", "state": "routeResolved", "dpsSessions": {"0": {"active": False}}} } } } - } + }, } - }, + } ], "inputs": { "paths": [ @@ -319,8 +376,8 @@ "expected": { "result": "failure", "messages": [ - "Telemetry state for path `peer: 10.255.0.1 source: 172.18.3.2 destination: 172.18.5.2` in path-group internet is `inactive`.", - "Telemetry state for path `peer: 10.255.0.2 source: 172.18.13.2 destination: 172.18.15.2` in path-group mpls is `inactive`.", + "Peer: 10.255.0.1, PathGroup: internet, Source: 172.18.3.2, Destination: 172.18.5.2 - No path matching the source and destination found", + "Peer: 10.255.0.2, PathGroup: mpls, Source: 172.18.13.2, Destination: 172.18.15.2 - No path matching the source and destination found", ], }, },