From 7996c8103140b5d8bb0f6c270c510d3a0adbf4a1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 15 May 2025 09:34:27 +0200 Subject: [PATCH] Adding upstream version 1.4.0. Signed-off-by: Daniel Baumann --- .arista/secret_allowlist.yaml | 10 - .github/release.md | 18 +- .github/workflows/code-testing.yml | 28 +- .github/workflows/secret-scanner.yml | 15 - .github/workflows/sonar.yml | 64 +- .pre-commit-config.yaml | 7 +- Dockerfile | 2 +- anta/_runner.py | 491 ++ anta/cli/__init__.py | 5 +- anta/cli/check/commands.py | 2 +- anta/cli/get/__init__.py | 1 + anta/cli/get/commands.py | 63 +- anta/cli/get/utils.py | 290 +- anta/cli/nrfu/__init__.py | 2 +- anta/cli/nrfu/utils.py | 12 +- anta/cli/utils.py | 94 +- anta/custom_types.py | 35 +- anta/decorators.py | 2 +- anta/device.py | 83 +- anta/input_models/connectivity.py | 13 +- anta/input_models/cvx.py | 2 + anta/input_models/evpn.py | 65 + anta/input_models/routing/bgp.py | 48 +- anta/input_models/routing/isis.py | 4 + anta/input_models/vlan.py | 26 + anta/inventory/__init__.py | 30 +- anta/reporter/__init__.py | 2 +- anta/reporter/md_reporter.py | 111 +- anta/result_manager/__init__.py | 24 +- anta/result_manager/models.py | 2 - anta/runner.py | 83 +- anta/settings.py | 86 + anta/tests/connectivity.py | 14 +- anta/tests/cvx.py | 2 +- anta/tests/evpn.py | 185 + anta/tests/field_notices.py | 4 +- anta/tests/flow_tracking.py | 2 +- anta/tests/hardware.py | 19 +- anta/tests/interfaces.py | 233 +- anta/tests/lanz.py | 2 +- anta/tests/logging.py | 8 +- anta/tests/multicast.py | 4 +- anta/tests/profiles.py | 4 +- anta/tests/ptp.py | 10 +- anta/tests/routing/bgp.py | 381 +- anta/tests/routing/generic.py | 57 +- anta/tests/routing/isis.py | 75 + anta/tests/snmp.py | 2 +- anta/tests/stp.py | 16 +- anta/tests/system.py | 35 +- anta/tests/vlan.py | 52 +- anta/tests/vxlan.py | 41 +- anta/tools.py | 4 +- asynceapi/_models.py | 1 - asynceapi/device.py | 21 +- docs/advanced_usages/as-python-lib.md | 4 +- docs/api/settings.md | 13 + docs/api/tests.md | 1 + docs/api/tests/aaa.md | 5 +- docs/api/tests/avt.md | 8 +- docs/api/tests/bfd.md | 8 +- docs/api/tests/configuration.md | 5 +- docs/api/tests/connectivity.md | 9 +- docs/api/tests/cvx.md | 25 +- docs/api/tests/{routing.isis.md => evpn.md} | 13 +- docs/api/tests/field_notices.md | 5 +- docs/api/tests/flow_tracking.md | 8 +- docs/api/tests/greent.md | 5 +- docs/api/tests/hardware.md | 5 +- docs/api/tests/interfaces.md | 10 +- docs/api/tests/lanz.md | 5 +- docs/api/tests/logging.md | 8 +- docs/api/tests/mlag.md | 5 +- docs/api/tests/multicast.md | 5 +- docs/api/tests/path_selection.md | 8 +- docs/api/tests/profiles.md | 5 +- docs/api/tests/ptp.md | 5 +- docs/api/tests/routing.bgp.md | 8 +- docs/api/tests/routing.generic.md | 8 +- docs/api/tests/routing.isis.md | 8 +- docs/api/tests/routing.ospf.md | 5 +- docs/api/tests/security.md | 8 +- docs/api/tests/services.md | 8 +- docs/api/tests/snmp.md | 8 +- docs/api/tests/software.md | 5 +- docs/api/tests/stp.md | 5 +- docs/api/tests/stun.md | 8 +- docs/api/tests/system.md | 8 +- docs/api/tests/vlan.md | 24 +- docs/api/tests/vxlan.md | 5 +- docs/cli/get-inventory-information.md | 35 +- docs/cli/get-tests.md | 152 +- docs/cli/inv-from-ansible.md | 18 +- docs/cli/inv-from-cvp.md | 21 +- docs/contribution.md | 69 +- docs/faq.md | 33 +- docs/requirements-and-installation.md | 2 +- docs/scripts/generate_doc_snippets.py | 5 + docs/snippets/anta_get_commands_help.txt | 19 + docs/snippets/anta_get_fromansible_help.txt | 17 + docs/snippets/anta_get_fromcvp_help.txt | 22 + docs/snippets/anta_get_inventory_help.txt | 37 + docs/snippets/anta_get_tests_help.txt | 13 + examples/tests.yaml | 262 +- mkdocs.yml | 10 +- pyproject.toml | 16 +- sonar-project.properties | 2 +- tests/benchmark/test_anta.py | 17 +- tests/benchmark/test_reporter.py | 5 +- tests/benchmark/test_runner.py | 42 + tests/benchmark/utils.py | 31 +- tests/conftest.py | 8 +- tests/data/syntax_error.py | 6 +- tests/data/test_md_report.md | 251 +- tests/data/test_md_report_custom_sections.md | 355 ++ tests/units/anta_tests/__init__.py | 63 +- tests/units/anta_tests/conftest.py | 20 +- tests/units/anta_tests/routing/test_bgp.py | 5378 +++++++---------- .../units/anta_tests/routing/test_generic.py | 322 +- tests/units/anta_tests/routing/test_isis.py | 1448 ++--- tests/units/anta_tests/routing/test_ospf.py | 275 +- tests/units/anta_tests/test_aaa.py | 421 +- tests/units/anta_tests/test_avt.py | 330 +- tests/units/anta_tests/test_bfd.py | 511 +- tests/units/anta_tests/test_configuration.py | 63 +- tests/units/anta_tests/test_connectivity.py | 505 +- tests/units/anta_tests/test_cvx.py | 341 +- tests/units/anta_tests/test_evpn.py | 449 ++ tests/units/anta_tests/test_field_notices.py | 313 +- tests/units/anta_tests/test_flow_tracking.py | 96 +- tests/units/anta_tests/test_greent.py | 50 +- tests/units/anta_tests/test_hardware.py | 337 +- tests/units/anta_tests/test_interfaces.py | 2051 ++++--- tests/units/anta_tests/test_lanz.py | 27 +- tests/units/anta_tests/test_logging.py | 354 +- tests/units/anta_tests/test_mlag.py | 315 +- tests/units/anta_tests/test_multicast.py | 92 +- tests/units/anta_tests/test_path_selection.py | 194 +- tests/units/anta_tests/test_profiles.py | 40 +- tests/units/anta_tests/test_ptp.py | 134 +- tests/units/anta_tests/test_security.py | 591 +- tests/units/anta_tests/test_services.py | 169 +- tests/units/anta_tests/test_snmp.py | 577 +- tests/units/anta_tests/test_software.py | 101 +- tests/units/anta_tests/test_stp.py | 291 +- tests/units/anta_tests/test_stun.py | 180 +- tests/units/anta_tests/test_system.py | 659 +- tests/units/anta_tests/test_vlan.py | 113 +- tests/units/anta_tests/test_vxlan.py | 251 +- tests/units/cli/conftest.py | 2 +- tests/units/cli/get/test_commands.py | 184 +- tests/units/cli/get/test_utils.py | 6 +- tests/units/cli/nrfu/test_commands.py | 18 +- tests/units/cli/test__init__.py | 47 +- tests/units/conftest.py | 11 +- tests/units/input_models/routing/test_bgp.py | 31 +- tests/units/inventory/test__init__.py | 22 + tests/units/reporter/test_md_reporter.py | 72 + tests/units/result_manager/test__init__.py | 100 +- .../test_files/test_md_report_results.json | 2621 +++++++- tests/units/test__runner.py | 414 ++ tests/units/test_custom_types.py | 26 + tests/units/test_device.py | 43 +- tests/units/test_models.py | 208 +- tests/units/test_runner.py | 141 +- tests/units/test_settings.py | 126 + 166 files changed, 13787 insertions(+), 11959 deletions(-) delete mode 100644 .arista/secret_allowlist.yaml delete mode 100644 .github/workflows/secret-scanner.yml create mode 100644 anta/_runner.py create mode 100644 anta/input_models/evpn.py create mode 100644 anta/input_models/vlan.py create mode 100644 anta/settings.py create mode 100644 anta/tests/evpn.py create mode 100644 docs/api/settings.md copy docs/api/tests/{routing.isis.md => evpn.md} (78%) create mode 100644 docs/snippets/anta_get_commands_help.txt create mode 100644 docs/snippets/anta_get_fromansible_help.txt create mode 100644 docs/snippets/anta_get_fromcvp_help.txt create mode 100644 docs/snippets/anta_get_inventory_help.txt create mode 100644 docs/snippets/anta_get_tests_help.txt create mode 100644 tests/data/test_md_report_custom_sections.md create mode 100644 tests/units/anta_tests/test_evpn.py create mode 100644 tests/units/test__runner.py create mode 100644 tests/units/test_settings.py diff --git a/.arista/secret_allowlist.yaml b/.arista/secret_allowlist.yaml deleted file mode 100644 index fea5054..0000000 --- a/.arista/secret_allowlist.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Arista Secret Scanner allow list - -version: v1.0 -allowed_secrets: -- secret_pattern: "https://ansible:ansible@192.168.0.2" - category: FALSE_POSITIVE - reason: Used as example in documentation -- secret_pattern: "https://ansible:ansible@192.168.0.17" - category: FALSE_POSITIVE - reason: Used as example in documentation diff --git a/.github/release.md b/.github/release.md index 14c7d44..4a9e2e9 100644 --- a/.github/release.md +++ b/.github/release.md @@ -14,11 +14,12 @@ Also, [Github CLI](https://cli.github.com/) can be helpful and is recommended In a branch specific for this, use the `bumpver` tool. It is configured to update: -* pyproject.toml -* docs/contribution.md -* docs/requirements-and-installation.md +- pyproject.toml +- docs/contribution.md +- docs/requirements-and-installation.md For instance to bump a patch version: + ``` bumpver update --patch ``` @@ -54,35 +55,41 @@ This is to be executed at the top of the repo ```bash git switch -c rel/vx.x.x ``` + 3. [Optional] Clean dist if required 4. Build the package locally ```bash python -m build ``` + 5. Check the package with `twine` (replace with your vesion) ```bash twine check dist/* ``` + 6. Upload the package to test.pypi ```bash twine upload -r testpypi dist/anta-x.x.x.* ``` + 7. Verify the package by installing it in a local venv and checking it installs and run correctly (run the tests) ```bash # In a brand new venv - pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --no-cache anta + pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --no-cache anta[cli] ``` + 8. Push to anta repository and create a Pull Request ```bash git push origin HEAD gh pr create --title 'bump: ANTA vx.x.x' ``` + 9. Merge PR after review and wait for [workflow](https://github.com/aristanetworks/anta/actions/workflows/release.yml) to be executed. ```bash @@ -100,4 +107,5 @@ This is to be executed at the top of the repo ```bash anta --version - ``` \ No newline at end of file + ``` + diff --git a/.github/workflows/code-testing.yml b/.github/workflows/code-testing.yml index 84777d6..1c20477 100644 --- a/.github/workflows/code-testing.yml +++ b/.github/workflows/code-testing.yml @@ -1,5 +1,5 @@ --- -name: Linting and Testing Anta +name: Linting and Testing ANTA on: push: branches: @@ -59,24 +59,10 @@ jobs: pip install . - name: install dev requirements run: pip install .[dev] - # @gmuloc: commenting this out for now - #missing-documentation: - # name: "Warning documentation is missing" - # runs-on: ubuntu-latest - # needs: [file-changes] - # if: needs.file-changes.outputs.cli == 'true' && needs.file-changes.outputs.docs == 'false' - # steps: - # - name: Documentation is missing - # uses: GrantBirki/comment@v2.0.10 - # with: - # body: | - # Please consider that documentation is missing under `docs/` folder. - # You should update documentation to reflect your change, or maybe not :) lint-python: name: Check the code style runs-on: ubuntu-latest needs: file-changes - if: needs.file-changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - name: Setup Python @@ -91,7 +77,6 @@ jobs: name: Check typing runs-on: ubuntu-latest needs: file-changes - if: needs.file-changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - name: Setup Python @@ -119,10 +104,20 @@ jobs: run: pip install tox tox-gh-actions - name: "Run pytest via tox for ${{ matrix.python }}" run: tox + - name: Upload coverage from pytest + # Coverage only runs as part of 3.11. + if: | + matrix.python == '3.11' + uses: actions/upload-artifact@v4 + with: + name: pytest-coverage + include-hidden-files: true + path: .coverage.xml test-python-windows: name: Pytest on 3.12 for windows runs-on: windows-2022 needs: [lint-python, type-python] + if: needs.file-changes.outputs.code == 'true' env: # Required to prevent asyncssh to fail. USERNAME: WindowsUser @@ -154,6 +149,7 @@ jobs: name: Benchmark ANTA for Python 3.12 runs-on: ubuntu-latest needs: [test-python] + if: needs.file-changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - name: Setup Python diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml deleted file mode 100644 index 80a0fe7..0000000 --- a/.github/workflows/secret-scanner.yml +++ /dev/null @@ -1,15 +0,0 @@ -# Secret-scanner workflow from Arista Networks. -on: - pull_request: - types: [synchronize] - push: - branches: - - main -name: Secret Scanner (go/secret-scanner) -jobs: - scan_secret: - name: Scan incoming changes - runs-on: ubuntu-latest - steps: - - name: Run scanner - uses: aristanetworks/secret-scanner-service-public@main diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 7ec7896..15d81ae 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -1,15 +1,9 @@ --- name: Analysis with Sonarlint and publish to SonarCloud on: - push: - branches: - - main - # Need to do this to be able to have coverage on PR across forks. - pull_request_target: - -# TODO this can be made better by running only coverage, it happens that today -# in tox gh-actions we have configured 3.11 to run the report side in -# pyproject.toml + workflow_run: + workflows: ["Linting and Testing ANTA"] + types: [completed] jobs: sonarcloud: @@ -19,26 +13,50 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Setup Python - uses: actions/setup-python@v5 + - name: Download coverage from unit tests + continue-on-error: true + uses: actions/download-artifact@v4 with: - python-version: 3.11 - - name: Install dependencies - run: pip install tox tox-gh-actions - - name: "Run pytest via tox for ${{ matrix.python }}" - run: tox - - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v5.0.0 + name: pytest-coverage + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + merge-multiple: true + + - name: Get PR context + # Source: https://github.com/orgs/community/discussions/25220#discussioncomment-11316244 + id: pr-context + if: github.event.workflow_run.event == 'pull_request' + env: + # Token required for GH CLI: + GH_TOKEN: ${{ github.token }} + # Best practice for scripts is to reference via ENV at runtime. Avoid using the expression syntax in the script content directly: + PR_TARGET_REPO: ${{ github.repository }} + # If the PR is from a fork, prefix it with `:`, otherwise only the PR branch name is relevant: + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + # Query the PR number by repo + branch, then assign to step output: + run: | + gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number,baseRefName' --jq '"number=\(.number)\nbase_ref=\(.baseRefName)"' \ + >> "${GITHUB_OUTPUT}" + echo "pr_branch=${PR_BRANCH}" >> "${GITHUB_OUTPUT}" + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: # Using ACTION_STEP_DEBUG to trigger verbose when debugging in Github Action args: > - -Dsonar.scm.revision=${{ github.event.pull_request.head.sha }} - -Dsonar.pullrequest.key=${{ github.event.number }} - -Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} - -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} + -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} + -Dsonar.pullrequest.key=${{ steps.pr-context.outputs.number }} + -Dsonar.pullrequest.branch=${{ steps.pr-context.outputs.pr_branch }} + -Dsonar.pullrequest.base=${{ steps.pr-context.outputs.base_ref }} -Dsonar.verbose=${{ secrets.ACTIONS_STEP_DEBUG }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dde835..8236c99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - "" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.10.0 + rev: v0.11.9 hooks: - id: ruff name: Run Ruff linter @@ -56,7 +56,7 @@ repos: name: Run Ruff formatter - repo: https://github.com/pycqa/pylint - rev: "v3.3.5" + rev: "v3.3.7" hooks: - id: pylint name: Check code style with pylint @@ -75,6 +75,7 @@ repos: - pytest - pytest-codspeed - respx + - pydantic-settings - repo: https://github.com/codespell-project/codespell rev: v2.4.1 @@ -124,6 +125,7 @@ repos: pass_filenames: false additional_dependencies: - anta[cli] + - pydantic-settings - id: doc-snippets name: Generate doc snippets entry: >- @@ -135,3 +137,4 @@ repos: pass_filenames: false additional_dependencies: - anta[cli] + - pydantic-settings diff --git a/Dockerfile b/Dockerfile index c7cdf65..0435d6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ LABEL "org.opencontainers.image.title"="anta" \ "org.opencontainers.artifact.description"="network-test-automation in a Python package and Python scripts to test Arista devices." \ "org.opencontainers.image.description"="network-test-automation in a Python package and Python scripts to test Arista devices." \ "org.opencontainers.image.source"="https://github.com/aristanetworks/anta" \ - "org.opencontainers.image.url"="https://www.anta.ninja" \ + "org.opencontainers.image.url"="https://anta.arista.com" \ "org.opencontainers.image.documentation"="https://anta.arista.com" \ "org.opencontainers.image.licenses"="Apache-2.0" \ "org.opencontainers.image.vendor"="Arista Networks" \ diff --git a/anta/_runner.py b/anta/_runner.py new file mode 100644 index 0000000..109108b --- /dev/null +++ b/anta/_runner.py @@ -0,0 +1,491 @@ +# 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. +"""ANTA runner classes.""" + +from __future__ import annotations + +import logging +from asyncio import Semaphore, gather +from collections import defaultdict +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from inspect import getcoroutinelocals +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, ConfigDict + +from anta import GITHUB_SUGGESTION +from anta.cli.console import console +from anta.inventory import AntaInventory +from anta.logger import anta_log_exception +from anta.models import AntaTest +from anta.result_manager import ResultManager +from anta.settings import AntaRunnerSettings +from anta.tools import Catchtime + +if TYPE_CHECKING: + from collections.abc import Coroutine + + from anta.catalog import AntaCatalog, AntaTestDefinition + from anta.device import AntaDevice + from anta.result_manager.models import TestResult + +logger = logging.getLogger(__name__) + + +class AntaRunFilters(BaseModel): + """Define filters for an ANTA run. + + Filters determine which devices and tests to include in a run, and how to + filter them with tags. This class is used by the `AntaRunner.run()` method. + + Attributes + ---------- + devices : set[str] | None, optional + Set of device names to run tests on. If `None`, includes all devices in + the inventory. Commonly set via the NRFU CLI `--device/-d` option. + tests : set[str] | None, optional + Set of test names to run. If `None`, runs all available tests in the + catalog. Commonly set via the NRFU CLI `--test/-t` option. + tags : set[str] | None, optional + Set of tags used to filter both devices and tests. A device or test + must match any of the provided tags to be included. Commonly set via + the NRFU CLI `--tags` option. + established_only : bool, default=True + When `True`, only includes devices with established connections in the + test run. + """ + + model_config = ConfigDict(frozen=True, extra="forbid") + devices: set[str] | None = None + tests: set[str] | None = None + tags: set[str] | None = None + established_only: bool = True + + +@dataclass +class AntaRunContext: + """Store the complete context and results of an ANTA run. + + A unique context is created and returned per ANTA run. + + Attributes + ---------- + inventory: AntaInventory + Initial inventory of devices provided to the run. + catalog: AntaCatalog + Initial catalog of tests provided to the run. + manager: ResultManager + Manager with the final test results. + filters: AntaRunFilters + Provided filters to the run. + selected_inventory: AntaInventory + The final inventory of devices selected for testing. + selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] + A mapping containing the final tests to be run per device. + devices_filtered_at_setup: list[str] + List of device names that were filtered during the inventory setup phase. + devices_unreachable_at_setup: list[str] + List of device names that were found unreachable during the inventory setup phase. + warnings_at_setup: list[str] + List of warnings caught during the setup phase. + start_time: datetime | None + Start time of the run. None if not set yet. + end_time: datetime | None + End time of the run. None if not set yet. + """ + + inventory: AntaInventory + catalog: AntaCatalog + manager: ResultManager + filters: AntaRunFilters + dry_run: bool = False + + # State populated during the run + selected_inventory: AntaInventory = field(default_factory=AntaInventory) + selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = field(default_factory=lambda: defaultdict(set)) + devices_filtered_at_setup: list[str] = field(default_factory=list) + devices_unreachable_at_setup: list[str] = field(default_factory=list) + warnings_at_setup: list[str] = field(default_factory=list) + start_time: datetime | None = None + end_time: datetime | None = None + + @property + def total_devices_in_inventory(self) -> int: + """Total devices in the initial inventory provided to the run.""" + return len(self.inventory) + + @property + def total_devices_filtered_by_tags(self) -> int: + """Total devices filtered by tags at inventory setup.""" + return len(self.devices_filtered_at_setup) + + @property + def total_devices_unreachable(self) -> int: + """Total devices unreachable at inventory setup.""" + return len(self.devices_unreachable_at_setup) + + @property + def total_devices_selected_for_testing(self) -> int: + """Total devices selected for testing.""" + return len(self.selected_inventory) + + @property + def total_tests_scheduled(self) -> int: + """Total tests scheduled to run across all selected devices.""" + return sum(len(tests) for tests in self.selected_tests.values()) + + @property + def duration(self) -> timedelta | None: + """Calculate the duration of the run. Returns None if start or end time is not set.""" + if self.start_time and self.end_time: + return self.end_time - self.start_time + return None + + +# pylint: disable=too-few-public-methods +class AntaRunner: + """Run and manage ANTA test execution. + + This class orchestrates the execution of ANTA tests across network devices. It handles + inventory filtering, test selection, concurrent test execution, and result collection. + An `AntaRunner` instance is stateless between runs. All necessary inputs like inventory + and catalog are provided to the `run()` method. + + Attributes + ---------- + _settings : AntaRunnerSettings + Settings container for the runner. This can be provided during initialization; + otherwise, it is loaded from environment variables by default. See the + `AntaRunnerSettings` class definition in the `anta.settings` module for details. + + Notes + ----- + After initializing an `AntaRunner` instance, tests should only be executed through + the `run()` method. This method manages the complete test lifecycle including setup, + execution, and cleanup. + + Examples + -------- + ```python + import asyncio + + from anta._runner import AntaRunner, AntaRunFilters + from anta.catalog import AntaCatalog + from anta.inventory import AntaInventory + + inventory = AntaInventory.parse( + filename="anta_inventory.yml", + username="arista", + password="arista", + ) + catalog = AntaCatalog.parse(filename="anta_catalog.yml") + + # Create an ANTA runner + runner = AntaRunner() + + # Run all tests + first_run_results = asyncio.run(runner.run(inventory, catalog)) + + # Run with filters + second_run_results = asyncio.run(runner.run(inventory, catalog, filters=AntaRunFilters(tags={"leaf"}))) + ``` + """ + + def __init__(self, settings: AntaRunnerSettings | None = None) -> None: + """Initialize AntaRunner.""" + self._settings = settings if settings is not None else AntaRunnerSettings() + logger.debug("AntaRunner initialized with settings: %s", self._settings.model_dump()) + + async def run( + self, + inventory: AntaInventory, + catalog: AntaCatalog, + result_manager: ResultManager | None = None, + filters: AntaRunFilters | None = None, + *, + dry_run: bool = False, + ) -> AntaRunContext: + """Run ANTA. + + Run workflow: + + 1. Build the context object for the run. + 2. Set up the selected inventory, removing filtered/unreachable devices. + 3. Set up the selected tests, removing filtered tests. + 4. Prepare the `AntaTest` coroutines from the selected inventory and tests. + 5. Run the test coroutines if it is not a dry run. + + Parameters + ---------- + inventory + Inventory of network devices to test. + catalog + Catalog of tests to run. + result_manager + Manager for collecting and storing test results. If `None`, a new manager + is returned for each run, otherwise the provided manager is used + and results from subsequent runs are appended to it. + filters + Filters for the ANTA run. If `None`, run all tests on all devices. + dry_run + Dry-run mode flag. If `True`, run all setup steps but do not execute tests. + + Returns + ------- + AntaRunContext + The complete context and results of this ANTA run. + """ + start_time = datetime.now(tz=timezone.utc) + logger.info("ANTA run starting ...") + + ctx = AntaRunContext( + inventory=inventory, + catalog=catalog, + manager=result_manager if result_manager is not None else ResultManager(), + filters=filters if filters is not None else AntaRunFilters(), + dry_run=dry_run, + start_time=start_time, + ) + + if len(ctx.manager) > 0: + msg = ( + f"Appending new results to the provided ResultManager which already holds {len(ctx.manager)} results. " + "Statistics in this run context are for the current execution only." + ) + self._log_warning_msg(msg=msg, ctx=ctx) + + if not ctx.catalog.tests: + self._log_warning_msg(msg="The list of tests is empty. Exiting ...", ctx=ctx) + ctx.end_time = datetime.now(tz=timezone.utc) + return ctx + + with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"): + # Set up inventory + setup_inventory_ok = await self._setup_inventory(ctx) + if not setup_inventory_ok: + ctx.end_time = datetime.now(tz=timezone.utc) + return ctx + + # Set up tests + with Catchtime(logger=logger, message="Preparing Tests"): + setup_tests_ok = self._setup_tests(ctx) + if not setup_tests_ok: + ctx.end_time = datetime.now(tz=timezone.utc) + return ctx + + # Get test coroutines + test_coroutines = self._get_test_coroutines(ctx) + + self._log_run_information(ctx) + + if ctx.dry_run: + logger.info("Dry-run mode, exiting before running the tests.") + self._close_test_coroutines(test_coroutines, ctx) + ctx.end_time = datetime.now(tz=timezone.utc) + return ctx + + if AntaTest.progress is not None: + AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=ctx.total_tests_scheduled) + + with Catchtime(logger=logger, message="Running Tests"): + sem = Semaphore(self._settings.max_concurrency) + + async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult: + """Wrap the test coroutine with semaphore control.""" + async with sem: + return await test_coro + + results = await gather(*[run_with_sem(coro) for coro in test_coroutines]) + for res in results: + ctx.manager.add(res) + + self._log_cache_statistics(ctx) + + ctx.end_time = datetime.now(tz=timezone.utc) + return ctx + + async def _setup_inventory(self, ctx: AntaRunContext) -> bool: + """Set up the inventory for the ANTA run. + + Returns True if the inventory setup was successful, otherwise False. + """ + initial_device_names = set(ctx.inventory.keys()) + + if not initial_device_names: + self._log_warning_msg(msg="The initial inventory is empty. Exiting ...", ctx=ctx) + return False + + # Filter the inventory based on the provided filters if any + filtered_inventory = ( + ctx.inventory.get_inventory(tags=ctx.filters.tags, devices=ctx.filters.devices) if ctx.filters.tags or ctx.filters.devices else ctx.inventory + ) + filtered_device_names = set(filtered_inventory.keys()) + ctx.devices_filtered_at_setup = sorted(initial_device_names - filtered_device_names) + + if not filtered_device_names: + msg_parts = ["The inventory is empty after filtering by tags/devices."] + if ctx.filters.devices: + msg_parts.append(f"Devices filter: {', '.join(sorted(ctx.filters.devices))}.") + if ctx.filters.tags: + msg_parts.append(f"Tags filter: {', '.join(sorted(ctx.filters.tags))}.") + msg_parts.append("Exiting ...") + self._log_warning_msg(msg=" ".join(msg_parts), ctx=ctx) + return False + + # In dry-run mode, set the selected inventory to the filtered inventory + if ctx.dry_run: + ctx.selected_inventory = filtered_inventory + return True + + # Attempt to connect to devices that passed filters + with Catchtime(logger=logger, message="Connecting to devices"): + await filtered_inventory.connect_inventory() + + # Remove devices that are unreachable if required + ctx.selected_inventory = filtered_inventory.get_inventory(established_only=True) if ctx.filters.established_only else filtered_inventory + selected_device_names = set(ctx.selected_inventory.keys()) + ctx.devices_unreachable_at_setup = sorted(filtered_device_names - selected_device_names) + + if not selected_device_names: + msg = "No reachable devices found for testing after connectivity checks. Exiting ..." + self._log_warning_msg(msg=msg, ctx=ctx) + return False + + return True + + def _setup_tests(self, ctx: AntaRunContext) -> bool: + """Set up tests for the ANTA run. + + Returns True if the test setup was successful, otherwise False. + """ + # Build indexes for the catalog. If `ctx.filters.tests` is set, filter the indexes based on these tests + ctx.catalog.build_indexes(filtered_tests=ctx.filters.tests) + + # Create the device to tests mapping from the tags + for device in ctx.selected_inventory.devices: + if ctx.filters.tags: + # If there are CLI tags, execute tests with matching tags for this device + if not (matching_tags := ctx.filters.tags.intersection(device.tags)): + # The device does not have any selected tag, skipping + # This should not never happen because the device will already be filtered by `_setup_inventory` + continue + ctx.selected_tests[device].update(ctx.catalog.get_tests_by_tags(matching_tags)) + else: + # If there is no CLI tags, execute all tests that do not have any tags + ctx.selected_tests[device].update(ctx.catalog.tag_to_tests[None]) + + # Then add the tests with matching tags from device tags + ctx.selected_tests[device].update(ctx.catalog.get_tests_by_tags(device.tags)) + + if ctx.total_tests_scheduled == 0: + msg_parts = ["No tests scheduled to run after filtering by tags/tests."] + if ctx.filters.tests: + msg_parts.append(f"Tests filter: {', '.join(sorted(ctx.filters.tests))}.") + if ctx.filters.tags: + msg_parts.append(f"Tags filter: {', '.join(sorted(ctx.filters.tags))}.") + msg_parts.append("Exiting ...") + self._log_warning_msg(msg=" ".join(msg_parts), ctx=ctx) + return False + + return True + + def _get_test_coroutines(self, ctx: AntaRunContext) -> list[Coroutine[Any, Any, TestResult]]: + """Get the test coroutines for the ANTA run.""" + coros = [] + for device, test_definitions in ctx.selected_tests.items(): + for test_def in test_definitions: + try: + coros.append(test_def.test(device=device, inputs=test_def.inputs).test()) + except Exception as exc: # noqa: BLE001, PERF203 + # An AntaTest instance is potentially user-defined code. + # We need to catch everything and exit gracefully with an error message. + msg = "\n".join( + [ + f"There is an error when creating test {test_def.test.__module__}.{test_def.test.__name__}.", + f"If this is not a custom test implementation: {GITHUB_SUGGESTION}", + ], + ) + anta_log_exception(exc, msg, logger) + return coros + + def _close_test_coroutines(self, coros: list[Coroutine[Any, Any, TestResult]], ctx: AntaRunContext) -> None: + """Close the test coroutines. Used in dry-run.""" + for coro in coros: + # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated + coro_locals = getcoroutinelocals(coro) + test = coro_locals.get("self") or coro_locals.get("args", (None))[0] + if isinstance(test, AntaTest): + ctx.manager.add(test.result) + else: + logger.error("Coroutine %s does not have an AntaTest instance.", coro) + coro.close() + + def _log_run_information(self, ctx: AntaRunContext) -> None: + """Log ANTA run information and potential resource limit warnings.""" + # 34 is an estimate of the combined length of timestamp, log level name, filename and spacing added by the Rich logger + width = min(int(console.width) - 34, len(" Potential connections needed: 100000000\n")) + + # Build device information + device_lines = [ + "Devices:", + f" Total in initial inventory: {ctx.total_devices_in_inventory}", + ] + if ctx.total_devices_filtered_by_tags > 0: + device_lines.append(f" Excluded by tags: {ctx.total_devices_filtered_by_tags}") + if ctx.total_devices_unreachable > 0: + device_lines.append(f" Failed to connect: {ctx.total_devices_unreachable}") + device_lines.append(f" Selected for testing: {ctx.total_devices_selected_for_testing}") + joined_device_lines = "\n".join(device_lines) + + # Build title + title = " ANTA NRFU Dry Run Information " if ctx.dry_run else " ANTA NRFU Run Information " + formatted_title_line = f"{title:-^{width}}" + + # Log run information + run_info = "\n".join( + [ + f"{formatted_title_line}", + f"{joined_device_lines}", + f"Total number of selected tests: {ctx.total_tests_scheduled}", + f"{'':-^{width}}", + ] + ) + logger.info(run_info) + logger.debug("Max concurrent tests: %d", self._settings.max_concurrency) + if potential_connections := ctx.selected_inventory.max_potential_connections: + logger.debug("Potential connections needed: %d", potential_connections) + logger.debug("File descriptors limit: %d", self._settings.file_descriptor_limit) + + # Log warnings for potential resource limits + if ctx.total_tests_scheduled > self._settings.max_concurrency: + msg = ( + f"Tests count ({ctx.total_tests_scheduled}) exceeds concurrent limit ({self._settings.max_concurrency}). " + "Tests will be throttled. Please consult the ANTA FAQ." + ) + self._log_warning_msg(msg=msg, ctx=ctx) + if potential_connections is not None and potential_connections > self._settings.file_descriptor_limit: + msg = ( + f"Potential connections ({potential_connections}) exceeds file descriptor limit ({self._settings.file_descriptor_limit}). " + "Connection errors may occur. Please consult the ANTA FAQ." + ) + self._log_warning_msg(msg=msg, ctx=ctx) + + def _log_cache_statistics(self, ctx: AntaRunContext) -> None: + """Log cache statistics for each device in the inventory.""" + for device in ctx.selected_inventory.devices: + if device.cache_statistics is not None: + msg = ( + f"Cache statistics for '{device.name}': " + f"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} " + f"command(s) ({device.cache_statistics['cache_hit_ratio']})" + ) + logger.debug(msg) + else: + logger.debug("Caching is not enabled on %s", device.name) + + def _log_warning_msg(self, msg: str, ctx: AntaRunContext) -> None: + """Log the provided message at WARNING level and add it to the context warnings_at_setup list.""" + logger.warning(msg) + ctx.warnings_at_setup.append(msg) diff --git a/anta/cli/__init__.py b/anta/cli/__init__.py index dd39f78..689c427 100644 --- a/anta/cli/__init__.py +++ b/anta/cli/__init__.py @@ -16,11 +16,14 @@ try: except ImportError as exc: - def build_cli(exception: Exception) -> Callable[[], None]: + def build_cli(exception: ImportError) -> Callable[[], None]: """Build CLI function using the caught exception.""" def wrap() -> None: """Error message if any CLI dependency is missing.""" + if not exception.name or "click" not in exception.name: + raise exception + print( "The ANTA command line client could not run because the required " "dependencies were not installed.\nMake sure you've installed " diff --git a/anta/cli/check/commands.py b/anta/cli/check/commands.py index 2ca6013..b810cc0 100644 --- a/anta/cli/check/commands.py +++ b/anta/cli/check/commands.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) @click.command -@catalog_options +@catalog_options() def catalog(catalog: AntaCatalog) -> None: """Check that the catalog is valid.""" console.print(f"[bold][green]Catalog is valid: {catalog.filename}") diff --git a/anta/cli/get/__init__.py b/anta/cli/get/__init__.py index d0393ad..468dae7 100644 --- a/anta/cli/get/__init__.py +++ b/anta/cli/get/__init__.py @@ -18,3 +18,4 @@ get.add_command(commands.from_ansible) get.add_command(commands.inventory) get.add_command(commands.tags) get.add_command(commands.tests) +get.add_command(commands.commands) diff --git a/anta/cli/get/commands.py b/anta/cli/get/commands.py index e34be2c..6ce3c1a 100644 --- a/anta/cli/get/commands.py +++ b/anta/cli/get/commands.py @@ -10,7 +10,7 @@ import asyncio import json import logging from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import click import requests @@ -20,11 +20,21 @@ from rich.pretty import pretty_repr from anta.cli.console import console from anta.cli.get.utils import inventory_output_options -from anta.cli.utils import ExitCode, inventory_options +from anta.cli.utils import ExitCode, catalog_options, inventory_options -from .utils import create_inventory_from_ansible, create_inventory_from_cvp, explore_package, get_cv_token +from .utils import ( + _explore_package, + _filter_tests_via_catalog, + _get_unique_commands, + _print_commands, + create_inventory_from_ansible, + create_inventory_from_cvp, + get_cv_token, + print_tests, +) if TYPE_CHECKING: + from anta.catalog import AntaCatalog from anta.inventory import AntaInventory logger = logging.getLogger(__name__) @@ -147,14 +157,53 @@ def tags(inventory: AntaInventory, **kwargs: Any) -> None: def tests(ctx: click.Context, module: str, test: str | None, *, short: bool, count: bool) -> None: """Show all builtin ANTA tests with an example output retrieved from each test documentation.""" try: - tests_found = explore_package(module, test_name=test, short=short, count=count) - if tests_found == 0: + tests_found = _explore_package(module, test_name=test, short=short, count=count) + if len(tests_found) == 0: console.print(f"""No test {f"'{test}' " if test else ""}found in '{module}'.""") elif count: - if tests_found == 1: + if len(tests_found) == 1: console.print(f"There is 1 test available in '{module}'.") else: - console.print(f"There are {tests_found} tests available in '{module}'.") + console.print(f"There are {len(tests_found)} tests available in '{module}'.") + else: + print_tests(tests_found, short=short) + except ValueError as e: + logger.error(str(e)) + ctx.exit(ExitCode.USAGE_ERROR) + + +@click.command +@click.pass_context +@click.option("--module", help="Filter commands by module name.", default="anta.tests", show_default=True) +@click.option("--test", help="Filter by specific test name. If module is specified, searches only within that module.", type=str) +@catalog_options(required=False) +@click.option("--unique", help="Print only the unique commands.", is_flag=True, default=False) +def commands( + ctx: click.Context, + module: str, + test: str | None, + catalog: AntaCatalog, + catalog_format: Literal["yaml", "json"] = "yaml", + *, + unique: bool, +) -> None: + """Print all EOS commands used by the selected ANTA tests. + + It can be filtered by module, test or using a catalog. + If no filter is given, all built-in ANTA tests commands are retrieved. + """ + # TODO: implement catalog and catalog format + try: + tests_found = _explore_package(module, test_name=test) + if catalog: + tests_found = _filter_tests_via_catalog(tests_found, catalog) + if len(tests_found) == 0: + console.print(f"""No test {f"'{test}' " if test else ""}found in '{module}'{f" for catalog '{catalog.filename}'" if catalog else ""}.""") + if unique: + for command in _get_unique_commands(tests_found): + console.print(command) + else: + _print_commands(tests_found) except ValueError as e: logger.error(str(e)) ctx.exit(ExitCode.USAGE_ERROR) diff --git a/anta/cli/get/utils.py b/anta/cli/get/utils.py index e609065..7ce5ba4 100644 --- a/anta/cli/get/utils.py +++ b/anta/cli/get/utils.py @@ -16,21 +16,25 @@ import sys import textwrap from pathlib import Path from sys import stdin -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import click import requests import urllib3 import yaml +from typing_extensions import deprecated from anta.cli.console import console from anta.cli.utils import ExitCode from anta.inventory import AntaInventory from anta.inventory.models import AntaInventoryHost, AntaInventoryInput -from anta.models import AntaTest +from anta.models import AntaCommand, AntaTest urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +if TYPE_CHECKING: + from anta.catalog import AntaCatalog + logger = logging.getLogger(__name__) @@ -231,7 +235,240 @@ def create_inventory_from_ansible(inventory: Path, output: Path, ansible_group: write_inventory_to_file(ansible_hosts, output) -def explore_package(module_name: str, test_name: str | None = None, *, short: bool = False, count: bool = False) -> int: +def _explore_package(module_name: str, test_name: str | None = None, *, short: bool = False, count: bool = False) -> list[type[AntaTest]]: + """Parse ANTA test submodules recursively and return a list of the found AntaTest. + + Parameters + ---------- + module_name + Name of the module to explore (e.g., 'anta.tests.routing.bgp'). + test_name + If provided, only show tests starting with this name. + short + If True, only print test names without their inputs. + count + If True, only count the tests. + + Returns + ------- + list[type[AntaTest]]: + A list of the AntaTest found. + """ + result: list[type[AntaTest]] = [] + try: + module_spec = importlib.util.find_spec(module_name) + except ModuleNotFoundError: + # Relying on module_spec check below. + module_spec = None + except ImportError as e: + msg = "`--module ` option does not support relative imports" + raise ValueError(msg) from e + + # Giving a second chance adding CWD to PYTHONPATH + if module_spec is None: + try: + logger.info("Could not find module `%s`, injecting CWD in PYTHONPATH and retrying...", module_name) + sys.path = [str(Path.cwd()), *sys.path] + module_spec = importlib.util.find_spec(module_name) + except ImportError: + module_spec = None + + if module_spec is None or module_spec.origin is None: + msg = f"Module `{module_name}` was not found!" + raise ValueError(msg) + + if module_spec.submodule_search_locations: + for _, sub_module_name, ispkg in pkgutil.walk_packages(module_spec.submodule_search_locations): + qname = f"{module_name}.{sub_module_name}" + if ispkg: + result.extend(_explore_package(qname, test_name=test_name, short=short, count=count)) + continue + result.extend(find_tests_in_module(qname, test_name)) + + else: + result.extend(find_tests_in_module(module_spec.name, test_name)) + + return result + + +def find_tests_in_module(qname: str, test_name: str | None) -> list[type[AntaTest]]: + """Return the list of AntaTest in the passed module qname, potentially filtering on test_name. + + Parameters + ---------- + qname + Name of the module to explore (e.g., 'anta.tests.routing.bgp'). + test_name + If provided, only show tests starting with this name. + + Returns + ------- + list[type[AntaTest]]: + A list of the AntaTest found in the module. + """ + results: list[type[AntaTest]] = [] + try: + qname_module = importlib.import_module(qname) + except (AssertionError, ImportError) as e: + msg = f"Error when importing `{qname}` using importlib!" + raise ValueError(msg) from e + + for _name, obj in inspect.getmembers(qname_module): + # Only retrieves the subclasses of AntaTest + if not inspect.isclass(obj) or not issubclass(obj, AntaTest) or obj == AntaTest: + continue + if test_name and not obj.name.startswith(test_name): + continue + results.append(obj) + + return results + + +def _filter_tests_via_catalog(tests: list[type[AntaTest]], catalog: AntaCatalog) -> list[type[AntaTest]]: + """Return the filtered list of tests present in the catalog. + + Parameters + ---------- + tests: + List of tests. + catalog: + The AntaCatalog to use as filtering. + + Returns + ------- + list[type[AntaTest]]: + The filtered list of tests containing uniquely the tests found in the catalog. + """ + catalog_test_names = {test.test.name for test in catalog.tests} + return [test for test in tests if test.name in catalog_test_names] + + +def print_tests(tests: list[type[AntaTest]], *, short: bool = False) -> None: + """Print a list of AntaTest. + + Parameters + ---------- + tests + A list of AntaTest subclasses. + short + If True, only print test names without their inputs. + """ + + def module_name(test: type[AntaTest]) -> str: + """Return the module name for the input test. + + Used to group the test by module. + """ + return test.__module__ + + from itertools import groupby + + for module, module_tests in groupby(tests, module_name): + console.print(f"{module}:") + for test in module_tests: + print_test(test, short=short) + + +def print_test(test: type[AntaTest], *, short: bool = False) -> None: + """Print a single test. + + Parameters + ---------- + test + the representation of the AntaTest as returned by inspect.getmembers + short + If True, only print test names without their inputs. + """ + if not test.__doc__ or (example := extract_examples(test.__doc__)) is None: + msg = f"Test {test.name} in module {test.__module__} is missing an Example" + raise LookupError(msg) + # Picking up only the inputs in the examples + # Need to handle the fact that we nest the routing modules in Examples. + # This is a bit fragile. + inputs = example.split("\n") + test_name_lines = [i for i, input_entry in enumerate(inputs) if test.name in input_entry] + if not test_name_lines: + msg = f"Could not find the name of the test '{test.name}' in the Example section in the docstring." + raise ValueError(msg) + for list_index, line_index in enumerate(test_name_lines): + end = test_name_lines[list_index + 1] if list_index + 1 < len(test_name_lines) else -1 + console.print(f" {inputs[line_index].strip()}") + # Injecting the description for the first example + if list_index == 0: + console.print(f" # {test.description}", soft_wrap=True) + if not short and len(inputs) > line_index + 2: # There are params + console.print(textwrap.indent(textwrap.dedent("\n".join(inputs[line_index + 1 : end])), " " * 6)) + + +def extract_examples(docstring: str) -> str | None: + """Extract the content of the Example section in a Numpy docstring. + + Returns + ------- + str | None + The content of the section if present, None if the section is absent or empty. + """ + pattern = r"Examples\s*--------\s*(.*)(?:\n\s*\n|\Z)" + match = re.search(pattern, docstring, flags=re.DOTALL) + return match[1].strip() if match and match[1].strip() != "" else None + + +def _print_commands(tests: list[type[AntaTest]]) -> None: + """Print a list of commands per module and per test. + + Parameters + ---------- + tests + A list of AntaTest subclasses. + """ + + def module_name(test: type[AntaTest]) -> str: + """Return the module name for the input test. + + Used to group the test by module. + """ + return test.__module__ + + from itertools import groupby + + for module, module_tests in groupby(tests, module_name): + console.print(f"{module}:") + for test in module_tests: + console.print(f" - {test.name}:") + for command in test.commands: + if isinstance(command, AntaCommand): + console.print(f" - {command.command}") + else: # isinstance(command, AntaTemplate): + console.print(f" - {command.template}") + + +def _get_unique_commands(tests: list[type[AntaTest]]) -> set[str]: + """Return a set of unique commands used by the tests. + + Parameters + ---------- + tests + A list of AntaTest subclasses. + + Returns + ------- + set[str] + A set of commands or templates used by each test. + """ + result: set[str] = set() + + for test in tests: + for command in test.commands: + if isinstance(command, AntaCommand): + result.add(command.command) + else: # isinstance(command, AntaTemplate): + result.add(command.template) + + return result + + +@deprecated("This function is deprecated, use `_explore_package`. This will be removed in ANTA v2.0.0.", category=DeprecationWarning) +def explore_package(module_name: str, test_name: str | None = None, *, short: bool = False, count: bool = False) -> int: # pragma: no cover """Parse ANTA test submodules recursively and print AntaTest examples. Parameters @@ -287,7 +524,8 @@ def explore_package(module_name: str, test_name: str | None = None, *, short: bo return tests_found -def find_tests_examples(qname: str, test_name: str | None, *, short: bool = False, count: bool = False) -> int: +@deprecated("This function is deprecated, use `find_tests_in_module`. This will be removed in ANTA v2.0.0.", category=DeprecationWarning) +def find_tests_examples(qname: str, test_name: str | None, *, short: bool = False, count: bool = False) -> int: # pragma: no cover """Print tests from `qname`, filtered by `test_name` if provided. Parameters @@ -331,47 +569,3 @@ def find_tests_examples(qname: str, test_name: str | None, *, short: bool = Fals print_test(obj, short=short) return tests_found - - -def print_test(test: type[AntaTest], *, short: bool = False) -> None: - """Print a single test. - - Parameters - ---------- - test - the representation of the AntaTest as returned by inspect.getmembers - short - If True, only print test names without their inputs. - """ - if not test.__doc__ or (example := extract_examples(test.__doc__)) is None: - msg = f"Test {test.name} in module {test.__module__} is missing an Example" - raise LookupError(msg) - # Picking up only the inputs in the examples - # Need to handle the fact that we nest the routing modules in Examples. - # This is a bit fragile. - inputs = example.split("\n") - test_name_lines = [i for i, input_entry in enumerate(inputs) if test.name in input_entry] - if not test_name_lines: - msg = f"Could not find the name of the test '{test.name}' in the Example section in the docstring." - raise ValueError(msg) - for list_index, line_index in enumerate(test_name_lines): - end = test_name_lines[list_index + 1] if list_index + 1 < len(test_name_lines) else -1 - console.print(f" {inputs[line_index].strip()}") - # Injecting the description for the first example - if list_index == 0: - console.print(f" # {test.description}", soft_wrap=True) - if not short and len(inputs) > line_index + 2: # There are params - console.print(textwrap.indent(textwrap.dedent("\n".join(inputs[line_index + 1 : end])), " " * 6)) - - -def extract_examples(docstring: str) -> str | None: - """Extract the content of the Example section in a Numpy docstring. - - Returns - ------- - str | None - The content of the section if present, None if the section is absent or empty. - """ - pattern = r"Examples\s*--------\s*(.*)(?:\n\s*\n|\Z)" - match = re.search(pattern, docstring, flags=re.DOTALL) - return match[1].strip() if match and match[1].strip() != "" else None diff --git a/anta/cli/nrfu/__init__.py b/anta/cli/nrfu/__init__.py index 6dc912d..776b6fc 100644 --- a/anta/cli/nrfu/__init__.py +++ b/anta/cli/nrfu/__init__.py @@ -57,7 +57,7 @@ HIDE_STATUS.remove("unset") @click.group(invoke_without_command=True, cls=IgnoreRequiredWithHelp) @click.pass_context @inventory_options -@catalog_options +@catalog_options() @click.option( "--device", "-d", diff --git a/anta/cli/nrfu/utils.py b/anta/cli/nrfu/utils.py index 60c0d29..9227bfe 100644 --- a/anta/cli/nrfu/utils.py +++ b/anta/cli/nrfu/utils.py @@ -50,6 +50,7 @@ def run_tests(ctx: click.Context) -> None: print_settings(inventory, catalog) with anta_progress_bar() as AntaTest.progress: + # TODO: Use AntaRunner directly in ANTA v2.0.0 asyncio.run( main( ctx.obj["result_manager"], @@ -65,9 +66,11 @@ def run_tests(ctx: click.Context) -> None: ctx.exit() -def _get_result_manager(ctx: click.Context) -> ResultManager: +def _get_result_manager(ctx: click.Context, *, apply_hide_filter: bool = True) -> ResultManager: """Get a ResultManager instance based on Click context.""" - return ctx.obj["result_manager"].filter(ctx.obj.get("hide")) if ctx.obj.get("hide") is not None else ctx.obj["result_manager"] + if apply_hide_filter: + return ctx.obj["result_manager"].filter(ctx.obj.get("hide")) if ctx.obj.get("hide") is not None else ctx.obj["result_manager"] + return ctx.obj["result_manager"] def print_settings( @@ -157,7 +160,10 @@ def save_markdown_report(ctx: click.Context, md_output: pathlib.Path) -> None: Path to save the markdown report. """ try: - MDReportGenerator.generate(results=_get_result_manager(ctx).sort(["name", "categories", "test"]), md_filename=md_output) + manager = _get_result_manager(ctx, apply_hide_filter=False).sort(["name", "categories", "test"]) + filtered_manager = _get_result_manager(ctx, apply_hide_filter=True).sort(["name", "categories", "test"]) + sections = [(section, filtered_manager) if section.__name__ == "TestResults" else (section, manager) for section in MDReportGenerator.DEFAULT_SECTIONS] + MDReportGenerator.generate_sections(md_filename=md_output, sections=sections) console.print(f"Markdown report saved to {md_output} ✅", style="cyan") except OSError: console.print(f"Failed to save Markdown report to {md_output} ❌", style="cyan") diff --git a/anta/cli/utils.py b/anta/cli/utils.py index 34b96b3..b5ba69c 100644 --- a/anta/cli/utils.py +++ b/anta/cli/utils.py @@ -163,6 +163,7 @@ def core_options(f: Callable[..., Any]) -> Callable[..., Any]: show_envvar=True, envvar="ANTA_TIMEOUT", show_default=True, + type=float, ) @click.option( "--insecure", @@ -290,50 +291,57 @@ def inventory_options(f: Callable[..., Any]) -> Callable[..., Any]: return wrapper -def catalog_options(f: Callable[..., Any]) -> Callable[..., Any]: +def catalog_options(*, required: bool = True) -> Callable[..., Callable[..., Any]]: """Click common options when requiring a test catalog to execute ANTA tests.""" - @click.option( - "--catalog", - "-c", - envvar="ANTA_CATALOG", - show_envvar=True, - help="Path to the test catalog file", - type=click.Path( - file_okay=True, - dir_okay=False, - exists=True, - readable=True, - path_type=Path, - ), - required=True, - ) - @click.option( - "--catalog-format", - envvar="ANTA_CATALOG_FORMAT", - show_envvar=True, - help="Format of the catalog file, either 'yaml' or 'json'", - default="yaml", - type=click.Choice(["yaml", "json"], case_sensitive=False), - ) - @click.pass_context - @functools.wraps(f) - def wrapper( - ctx: click.Context, - *args: tuple[Any], - catalog: Path, - catalog_format: str, - **kwargs: dict[str, Any], - ) -> Any: - # If help is invoke somewhere, do not parse catalog - if ctx.obj.get("_anta_help"): - return f(*args, catalog=None, **kwargs) - try: - file_format = catalog_format.lower() - c = AntaCatalog.parse(catalog, file_format=file_format) # type: ignore[arg-type] - except (TypeError, ValueError, YAMLError, OSError) as e: - anta_log_exception(e, f"Failed to parse the catalog: {catalog}", logger) - ctx.exit(ExitCode.USAGE_ERROR) - return f(*args, catalog=c, **kwargs) + def wrapper(f: Callable[..., Any]) -> Callable[..., Any]: + """Click common options when requiring a test catalog to execute ANTA tests.""" + + @click.option( + "--catalog", + "-c", + envvar="ANTA_CATALOG", + show_envvar=True, + help="Path to the test catalog file", + type=click.Path( + file_okay=True, + dir_okay=False, + exists=True, + readable=True, + path_type=Path, + ), + required=required, + ) + @click.option( + "--catalog-format", + envvar="ANTA_CATALOG_FORMAT", + show_envvar=True, + help="Format of the catalog file, either 'yaml' or 'json'", + default="yaml", + type=click.Choice(["yaml", "json"], case_sensitive=False), + ) + @click.pass_context + @functools.wraps(f) + def wrapper( + ctx: click.Context, + *args: tuple[Any], + catalog: Path | None, + catalog_format: Literal["yaml", "json"], + **kwargs: dict[str, Any], + ) -> Any: + # If help is invoke somewhere, do not parse catalog + if ctx.obj.get("_anta_help"): + return f(*args, catalog=None, **kwargs) + if not catalog and not required: + return f(*args, catalog=None, **kwargs) + try: + file_format = catalog_format.lower() + c = AntaCatalog.parse(catalog, file_format=file_format) # type: ignore[arg-type] + except (TypeError, ValueError, YAMLError, OSError) as e: + anta_log_exception(e, f"Failed to parse the catalog: {catalog}", logger) + ctx.exit(ExitCode.USAGE_ERROR) + return f(*args, catalog=c, **kwargs) + + return wrapper return wrapper diff --git a/anta/custom_types.py b/anta/custom_types.py index 92edabc..a83756c 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -14,12 +14,14 @@ REGEXP_PATH_MARKERS = r"[\\\/\s]" """Match directory path from string.""" REGEXP_INTERFACE_ID = r"\d+(\/\d+)*(\.\d+)?" """Match Interface ID lilke 1/1.1.""" -REGEXP_TYPE_EOS_INTERFACE = r"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\/[0-9]+)*(\.[0-9]+)?$" +REGEXP_TYPE_EOS_INTERFACE = r"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Recirc-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\/[0-9]+)*(\.[0-9]+)?$" """Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc.""" REGEXP_TYPE_VXLAN_SRC_INTERFACE = r"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$" """Match Vxlan source interface like Loopback10.""" REGEX_TYPE_PORTCHANNEL = r"^Port-Channel[0-9]{1,6}$" """Match Port Channel interface like Port-Channel5.""" +REGEXP_EOS_INTERFACE_TYPE = r"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Recirc-Channel|Tunnel|Vlan|Vxlan)$" +"""Match an EOS interface type like Ethernet or Loopback.""" REGEXP_TYPE_HOSTNAME = r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" """Match hostname like `my-hostname`, `my-hostname-1`, `my-hostname-1-2`.""" @@ -187,9 +189,26 @@ def update_bgp_redistributed_proto_user(value: str) -> str: return value +def convert_reload_cause(value: str) -> str: + """Convert a reload cause abbreviation into its full descriptive string. + + Examples + -------- + ```python + >>> convert_reload_cause("ZTP") + 'System reloaded due to Zero Touch Provisioning' + ``` + """ + reload_causes = {"ZTP": "System reloaded due to Zero Touch Provisioning", "USER": "Reload requested by the user.", "FPGA": "Reload requested after FPGA upgrade"} + if not reload_causes.get(value.upper()): + msg = f"Invalid reload cause: '{value}' - expected causes are {list(reload_causes)}" + raise ValueError(msg) + return reload_causes[value.upper()] + + # AntaTest.Input types AAAAuthMethod = Annotated[str, AfterValidator(aaa_group_prefix)] -Vlan = Annotated[int, Field(ge=0, le=4094)] +VlanId = Annotated[int, Field(ge=0, le=4094)] MlagPriority = Annotated[int, Field(ge=1, le=32767)] Vni = Annotated[int, Field(ge=1, le=16777215)] Interface = Annotated[ @@ -216,6 +235,11 @@ PortChannelInterface = Annotated[ BeforeValidator(interface_autocomplete), BeforeValidator(interface_case_sensitivity), ] +InterfaceType = Annotated[ + str, + Field(pattern=REGEXP_EOS_INTERFACE_TYPE), + BeforeValidator(interface_case_sensitivity), +] Afi = Literal["ipv4", "ipv6", "vpn-ipv4", "vpn-ipv6", "evpn", "rt-membership", "path-selection", "link-state"] Safi = Literal["unicast", "multicast", "labeled-unicast", "sr-te"] EncryptionAlgorithm = Literal["RSA", "ECDSA"] @@ -396,3 +420,10 @@ RedistributedProtocol = Annotated[ ] RedistributedAfiSafi = Annotated[Literal["v4u", "v4m", "v6u", "v6m"], BeforeValidator(bgp_redistributed_route_proto_abbreviations)] NTPStratumLevel = Annotated[int, Field(ge=0, le=16)] +PowerSupplyFanStatus = Literal["failed", "ok", "unknownHwStatus", "powerLoss", "unsupported"] +PowerSupplyStatus = Literal["ok", "unknown", "powerLoss", "failed"] +ReloadCause = Annotated[ + Literal["System reloaded due to Zero Touch Provisioning", "Reload requested by the user.", "Reload requested after FPGA upgrade", "USER", "FPGA", "ZTP"], + BeforeValidator(convert_reload_cause), +] +BgpCommunity = Literal["standard", "extended", "large"] diff --git a/anta/decorators.py b/anta/decorators.py index 0ca2be8..3474e57 100644 --- a/anta/decorators.py +++ b/anta/decorators.py @@ -161,7 +161,7 @@ def skip_on_platforms(platforms: list[str]) -> Callable[[F], F]: return anta_test.result if anta_test.device.hw_model in platforms: - anta_test.result.is_skipped(f"{anta_test.__class__.__name__} test is not supported on {anta_test.device.hw_model}.") + anta_test.result.is_skipped(f"{anta_test.__class__.__name__} test is not supported on {anta_test.device.hw_model}") AntaTest.update_progress() return anta_test.result diff --git a/anta/device.py b/anta/device.py index 7c1e6f6..544f63e 100644 --- a/anta/device.py +++ b/anta/device.py @@ -114,16 +114,19 @@ class AntaDevice(ABC): True if the device IP is reachable and a port can be open. established : bool True if remote command execution succeeds. - hw_model : str + hw_model : str | None Hardware model of the device. tags : set[str] Tags for this device. cache : AntaCache | None In-memory cache for this device (None if cache is disabled). - cache_locks : dict + cache_locks : defaultdict[str, asyncio.Lock] | None Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Deprecated, will be removed in ANTA v2.0.0, use self.cache.locks instead. - + max_connections : int | None + For informational/logging purposes only. Can be used by the runner to verify that + the total potential connections of a run do not exceed the system file descriptor limit. + This does **not** affect the actual device configuration. None if not available. """ def __init__(self, name: str, tags: set[str] | None = None, *, disable_cache: bool = False) -> None: @@ -159,6 +162,11 @@ class AntaDevice(ABC): def _keys(self) -> tuple[Any, ...]: """Read-only property to implement hashing and equality for AntaDevice classes.""" + @property + def max_connections(self) -> int | None: + """Maximum number of concurrent connections allowed by the device. Can be overridden by subclasses, returns None if not available.""" + return None + def __eq__(self, other: object) -> bool: """Implement equality for AntaDevice objects.""" return self._keys == other._keys if isinstance(other, self.__class__) else False @@ -302,9 +310,8 @@ class AntaDevice(ABC): raise NotImplementedError(msg) -# pylint: disable=too-many-instance-attributes class AsyncEOSDevice(AntaDevice): - """Implementation of AntaDevice for EOS using aio-eapi. + """Implementation of AntaDevice for EOS using the `asynceapi` library, which is built on HTTPX. Attributes ---------- @@ -318,7 +325,6 @@ class AsyncEOSDevice(AntaDevice): Hardware model of the device. tags : set[str] Tags for this device. - """ def __init__( # noqa: PLR0913 @@ -329,7 +335,7 @@ class AsyncEOSDevice(AntaDevice): name: str | None = None, enable_password: str | None = None, port: int | None = None, - ssh_port: int | None = 22, + ssh_port: int = 22, tags: set[str] | None = None, timeout: float | None = None, proto: Literal["http", "https"] = "https", @@ -350,8 +356,6 @@ class AsyncEOSDevice(AntaDevice): Password to connect to eAPI and SSH. name Device name. - enable - Collect commands using privileged mode. enable_password Password used to gain privileged access on EOS. port @@ -361,14 +365,15 @@ class AsyncEOSDevice(AntaDevice): tags Tags for this device. timeout - Timeout value in seconds for outgoing API calls. - insecure - Disable SSH Host Key validation. + Global timeout value in seconds for outgoing eAPI calls. None means no timeout. proto eAPI protocol. Value can be 'http' or 'https'. + enable + Collect commands using privileged mode. + insecure + Disable SSH Host Key validation. disable_cache Disable caching for all commands for this device. - """ if host is None: message = "'host' is required to create an AsyncEOSDevice" @@ -417,6 +422,7 @@ class AsyncEOSDevice(AntaDevice): _ssh_opts["kwargs"]["password"] = removed_pw yield ("_session", vars(self._session)) yield ("_ssh_opts", _ssh_opts) + yield ("max_connections", self.max_connections) if self.max_connections is not None else ("max_connections", "N/A") def __repr__(self) -> str: """Return a printable representation of an AsyncEOSDevice.""" @@ -442,6 +448,14 @@ class AsyncEOSDevice(AntaDevice): """ return (self._session.host, self._session.port) + @property + def max_connections(self) -> int | None: + """Maximum number of concurrent connections allowed by the device. Returns None if not available.""" + try: + return self._session._transport._pool._max_connections # type: ignore[attr-defined] # noqa: SLF001 + except AttributeError: + return None + async def _get_semaphore(self) -> asyncio.Semaphore: """Return the semaphore, initializing it if needed. @@ -539,29 +553,36 @@ class AsyncEOSDevice(AntaDevice): """Update attributes of an AsyncEOSDevice instance. This coroutine must update the following attributes of AsyncEOSDevice: - - is_online: When a device IP is reachable and a port can be open + - is_online: When a device eAPI HTTP endpoint is accessible - established: When a command execution succeeds - hw_model: The hardware model of the device """ logger.debug("Refreshing device %s", self.name) - self.is_online = await self._session.check_connection() - if self.is_online: - show_version = AntaCommand(command="show version") - await self._collect(show_version) - if not show_version.collected: - logger.warning("Cannot get hardware information from device %s", self.name) - else: - self.hw_model = show_version.json_output.get("modelName", None) - if self.hw_model is None: - logger.critical("Cannot parse 'show version' returned by device %s", self.name) - # in some cases it is possible that 'modelName' comes back empty - # and it is nice to get a meaninfule error message - elif self.hw_model == "": - logger.critical("Got an empty 'modelName' in the 'show version' returned by device %s", self.name) - else: - logger.warning("Could not connect to device %s: cannot open eAPI port", self.name) + try: + self.is_online = await self._session.check_api_endpoint() + except HTTPError as e: + self.is_online = False + self.established = False + logger.warning("Could not connect to device %s: %s", self.name, e) + return - self.established = bool(self.is_online and self.hw_model) + show_version = AntaCommand(command="show version") + await self._collect(show_version) + if not show_version.collected: + self.established = False + logger.warning("Cannot get hardware information from device %s", self.name) + return + + self.hw_model = show_version.json_output.get("modelName", None) + if self.hw_model is None: + self.established = False + logger.critical("Cannot parse 'show version' returned by device %s", self.name) + # in some cases it is possible that 'modelName' comes back empty + elif self.hw_model == "": + self.established = False + logger.critical("Got an empty 'modelName' in the 'show version' returned by device %s", self.name) + else: + self.established = True async def copy(self, sources: list[Path], destination: Path, direction: Literal["to", "from"] = "from") -> None: """Copy files to and from the device using asyncssh.scp(). diff --git a/anta/input_models/connectivity.py b/anta/input_models/connectivity.py index 464d22a..2d4f0e9 100644 --- a/anta/input_models/connectivity.py +++ b/anta/input_models/connectivity.py @@ -20,8 +20,8 @@ class Host(BaseModel): model_config = ConfigDict(extra="forbid") destination: IPv4Address | IPv6Address """Destination address to ping.""" - source: IPv4Address | IPv6Address | Interface - """Source address IP or egress interface to use.""" + source: IPv4Address | IPv6Address | Interface | None = None + """Source address IP or egress interface to use. Can be provided in the `VerifyReachability` test.""" vrf: str = "default" """VRF context.""" repeat: int = 2 @@ -38,10 +38,15 @@ class Host(BaseModel): Examples -------- - Host: 10.1.1.1 Source: 10.2.2.2 VRF: mgmt + - Host: 10.1.1.1 Source: 10.2.2.2 VRF: mgmt + - Host: 10.1.1.1 VRF: mgmt """ - return f"Host: {self.destination} Source: {self.source} VRF: {self.vrf}" + base_string = f"Host: {self.destination}" + if self.source: + base_string += f" Source: {self.source}" + base_string += f" VRF: {self.vrf}" + return base_string class LLDPNeighbor(BaseModel): diff --git a/anta/input_models/cvx.py b/anta/input_models/cvx.py index e2f5f8e..0e21c89 100644 --- a/anta/input_models/cvx.py +++ b/anta/input_models/cvx.py @@ -16,4 +16,6 @@ class CVXPeers(BaseModel): """Model for a CVX Cluster Peer.""" peer_name: Hostname + """The CVX Peer used communicate with a CVX server.""" registration_state: Literal["Connecting", "Connected", "Registration error", "Registration complete", "Unexpected peer state"] = "Registration complete" + """The CVX registration state.""" diff --git a/anta/input_models/evpn.py b/anta/input_models/evpn.py new file mode 100644 index 0000000..9030cd8 --- /dev/null +++ b/anta/input_models/evpn.py @@ -0,0 +1,65 @@ +# 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 EVPN tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Interface, IPv6Interface +from typing import Literal + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import Vni + + +class EVPNType5Prefix(BaseModel): + """Model for an EVPN Type-5 prefix.""" + + model_config = ConfigDict(extra="forbid") + address: IPv4Interface | IPv6Interface + """IPv4 or IPv6 prefix address to verify.""" + vni: Vni + """VNI associated with the prefix.""" + routes: list[EVPNRoute] | None = None + """Specific EVPN routes to verify for this prefix.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the EVPNType5Prefix for reporting.""" + return f"Prefix: {self.address} VNI: {self.vni}" + + +class EVPNRoute(BaseModel): + """Model for an EVPN Type-5 route for a prefix.""" + + model_config = ConfigDict(extra="forbid") + rd: str + """Expected route distinguisher `:` of the route.""" + domain: Literal["local", "remote"] = "local" + """EVPN domain. Can be remote on gateway nodes in a multi-domain EVPN VXLAN fabric.""" + paths: list[EVPNPath] | None = None + """Specific paths to verify for this route.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the EVPNRoute for reporting.""" + value = f"RD: {self.rd}" + if self.domain == "remote": + value += " Domain: remote" + return value + + +class EVPNPath(BaseModel): + """Model for an EVPN Type-5 path for a route.""" + + model_config = ConfigDict(extra="forbid") + nexthop: str + """Expected next-hop IPv4 or IPv6 address. Can be an empty string for local paths.""" + route_targets: list[str] | None = None + """List of expected RTs following the `ASN(asplain):nn` or `ASN(asdot):nn` or `IP-address:nn` format.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the EVPNPath for reporting.""" + value = f"Nexthop: {self.nexthop}" + if self.route_targets: + value += f" RTs: {', '.join(self.route_targets)}" + return value diff --git a/anta/input_models/routing/bgp.py b/anta/input_models/routing/bgp.py index 09def7f..8c2a4ff 100644 --- a/anta/input_models/routing/bgp.py +++ b/anta/input_models/routing/bgp.py @@ -5,14 +5,14 @@ from __future__ import annotations -from ipaddress import IPv4Address, IPv4Network, IPv6Address +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from typing import TYPE_CHECKING, Any, Literal from warnings import warn from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator from pydantic_extra_types.mac_address import MacAddress -from anta.custom_types import Afi, BgpDropStats, BgpUpdateError, MultiProtocolCaps, RedistributedAfiSafi, RedistributedProtocol, Safi, Vni +from anta.custom_types import Afi, BgpCommunity, BgpDropStats, BgpUpdateError, Interface, MultiProtocolCaps, RedistributedAfiSafi, RedistributedProtocol, Safi, Vni if TYPE_CHECKING: import sys @@ -150,26 +150,30 @@ class BgpAfi(BgpAddressFamily): # pragma: no cover class BgpPeer(BaseModel): """Model for a BGP peer. - Only IPv4 peers are supported for now. + Supports IPv4, IPv6 and IPv6 link-local neighbors. + + Also supports RFC5549 by providing the interface to be used for session establishment. """ model_config = ConfigDict(extra="forbid") - peer_address: IPv4Address - """IPv4 address of the BGP peer.""" + peer_address: IPv4Address | IPv6Address | None = None + """IP address of the BGP peer. Optional only if using `interface` for BGP RFC5549.""" + interface: Interface | None = None + """Interface to be used for BGP RFC5549 session establishment.""" vrf: str = "default" - """Optional VRF for the BGP peer. Defaults to `default`.""" + """VRF for the BGP peer.""" peer_group: str | None = None """Peer group of the BGP peer. Required field in the `VerifyBGPPeerGroup` test.""" - advertised_routes: list[IPv4Network] | None = None + advertised_routes: list[IPv4Network | IPv6Network] | None = None """List of advertised routes in CIDR format. Required field in the `VerifyBGPExchangedRoutes` test.""" - received_routes: list[IPv4Network] | None = None + received_routes: list[IPv4Network | IPv6Network] | None = None """List of received routes in CIDR format. Required field in the `VerifyBGPExchangedRoutes` test.""" capabilities: list[MultiProtocolCaps] | None = None """List of BGP multiprotocol capabilities. Required field in the `VerifyBGPPeerMPCaps`, `VerifyBGPNlriAcceptance` tests.""" strict: bool = False """If True, requires exact match of the provided BGP multiprotocol capabilities. - Optional field in the `VerifyBGPPeerMPCaps` test. Defaults to False.""" + Optional field in the `VerifyBGPPeerMPCaps` test.""" hold_time: int | None = Field(default=None, ge=3, le=7200) """BGP hold time in seconds. Required field in the `VerifyBGPTimers` test.""" keep_alive_time: int | None = Field(default=None, ge=0, le=3600) @@ -183,11 +187,11 @@ class BgpPeer(BaseModel): Optional field in the `VerifyBGPPeerUpdateErrors` test. If not provided, the test will verifies all the update error counters.""" inbound_route_map: str | None = None - """Inbound route map applied, defaults to None. Required field in the `VerifyBgpRouteMaps` test.""" + """Inbound route map applied to the peer. Optional field in the `VerifyBgpRouteMaps` test. If not provided, `outbound_route_map` must be provided.""" outbound_route_map: str | None = None - """Outbound route map applied, defaults to None. Required field in the `VerifyBgpRouteMaps` test.""" + """Outbound route map applied to the peer. Optional field in the `VerifyBgpRouteMaps` test. If not provided, `inbound_route_map` must be provided.""" maximum_routes: int | None = Field(default=None, ge=0, le=4294967294) - """The maximum allowable number of BGP routes. `0` means unlimited. Required field in the `VerifyBGPPeerRouteLimit` test""" + """The maximum allowable number of BGP routes. `0` means unlimited. Required field in the `VerifyBGPPeerRouteLimit` test.""" warning_limit: int | None = Field(default=None, ge=0, le=4294967294) """The warning limit for the maximum routes. `0` means no warning. @@ -196,10 +200,26 @@ class BgpPeer(BaseModel): """The Time-To-Live (TTL). Required field in the `VerifyBGPPeerTtlMultiHops` test.""" max_ttl_hops: int | None = Field(default=None, ge=1, le=255) """The Max TTL hops. Required field in the `VerifyBGPPeerTtlMultiHops` test.""" + advertised_communities: list[BgpCommunity] = Field(default=["standard", "extended", "large"]) + """List of advertised communities to be verified. + + Optional field in the `VerifyBGPAdvCommunities` test. If not provided, the test will verify that all communities are advertised.""" + + @model_validator(mode="after") + def validate_inputs(self) -> Self: + """Validate the inputs provided to the BgpPeer class. + + Either `peer_address` or `interface` must be provided, not both. + """ + if (self.peer_address is None) == (self.interface is None): + msg = "Exactly one of 'peer_address' or 'interface' must be provided" + raise ValueError(msg) + return self def __str__(self) -> str: """Return a human-readable string representation of the BgpPeer for reporting.""" - return f"Peer: {self.peer_address} VRF: {self.vrf}" + identifier = f"Peer: {self.peer_address}" if self.peer_address is not None else f"Interface: {self.interface}" + return f"{identifier} VRF: {self.vrf}" class BgpNeighbor(BgpPeer): # pragma: no cover @@ -344,7 +364,7 @@ class AddressFamilyConfig(BaseModel): Following table shows the supported redistributed routes for each address family. | IPv4 Unicast | IPv6 Unicast | IPv4 Multicast | IPv6 Multicast | - | ------------------------|-------------------------|------------------------|------------------------| + |-------------------------|-------------------------|------------------------|------------------------| | AttachedHost | AttachedHost | AttachedHost | Connected | | Bgp | Bgp | Connected | IS-IS | | Connected | Connected | IS-IS | OSPF Internal | diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index c0e2649..9465487 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -26,6 +26,10 @@ class ISISInstance(BaseModel): """Configured SR data-plane for the IS-IS instance.""" segments: list[Segment] | None = None """List of IS-IS SR segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" + graceful_restart: bool = False + """Graceful restart status.""" + graceful_restart_helper: bool = True + """Graceful restart helper status.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInstance for reporting.""" diff --git a/anta/input_models/vlan.py b/anta/input_models/vlan.py new file mode 100644 index 0000000..2558857 --- /dev/null +++ b/anta/input_models/vlan.py @@ -0,0 +1,26 @@ +# 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 VLAN tests.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import VlanId + + +class Vlan(BaseModel): + """Model for a VLAN.""" + + model_config = ConfigDict(extra="forbid") + vlan_id: VlanId + """The VLAN ID.""" + status: Literal["active", "suspended", "inactive"] + """The VLAN administrative status.""" + + def __str__(self) -> str: + """Representation of the VLAN model.""" + return f"VLAN: Vlan{self.vlan_id}" diff --git a/anta/inventory/__init__.py b/anta/inventory/__init__.py index a74638e..72fca4b 100644 --- a/anta/inventory/__init__.py +++ b/anta/inventory/__init__.py @@ -200,7 +200,7 @@ class AntaInventory(dict[str, AntaDevice]): enable_password Enable password to use if required. timeout - Timeout value in seconds for outgoing API calls. + Global timeout value in seconds for outgoing eAPI calls. None means no timeout. file_format Whether the inventory file is in JSON or YAML. enable @@ -265,6 +265,11 @@ class AntaInventory(dict[str, AntaDevice]): """List of AntaDevice in this inventory.""" return list(self.values()) + @property + def max_potential_connections(self) -> int | None: + """Max potential connections of this inventory.""" + return self._get_potential_connections() + ########################################################################### # Public methods ########################################################################### @@ -305,6 +310,29 @@ class AntaInventory(dict[str, AntaDevice]): result.add_device(device) return result + def _get_potential_connections(self) -> int | None: + """Calculate the total potential concurrent connections for the current inventory. + + This method sums the maximum concurrent connections allowed for each + AntaDevice in the inventory. + + Returns + ------- + int | None + The total sum of the `max_connections` attribute for all AntaDevice objects + in the inventory. Returns None if any AntaDevice does not have a `max_connections` + attribute or if its value is None, as the total count cannot be determined. + """ + potential_connections = 0 + all_have_connections = True + for device in self.devices: + if device.max_connections is None: + all_have_connections = False + logger.debug("Device %s 'max_connections' is not available", device.name) + break + potential_connections += device.max_connections + return None if not all_have_connections else potential_connections + ########################################################################### # SET methods ########################################################################### diff --git a/anta/reporter/__init__.py b/anta/reporter/__init__.py index 5156ea7..b6f061c 100644 --- a/anta/reporter/__init__.py +++ b/anta/reporter/__init__.py @@ -29,7 +29,7 @@ class ReportTable: """TableReport Generate a Table based on TestResult.""" @dataclass - class Headers: # pylint: disable=too-many-instance-attributes + class Headers: """Headers for the table report.""" device: str = "Device" diff --git a/anta/reporter/md_reporter.py b/anta/reporter/md_reporter.py index 2d2d882..1d97f55 100644 --- a/anta/reporter/md_reporter.py +++ b/anta/reporter/md_reporter.py @@ -21,49 +21,10 @@ if TYPE_CHECKING: from anta.result_manager import ResultManager + logger = logging.getLogger(__name__) -# pylint: disable=too-few-public-methods -class MDReportGenerator: - """Class responsible for generating a Markdown report based on the provided `ResultManager` object. - - It aggregates different report sections, each represented by a subclass of `MDReportBase`, - and sequentially generates their content into a markdown file. - - The `generate` class method will loop over all the section subclasses and call their `generate_section` method. - The final report will be generated in the same order as the `sections` list of the method. - """ - - @classmethod - def generate(cls, results: ResultManager, md_filename: Path) -> None: - """Generate and write the various sections of the markdown report. - - Parameters - ---------- - results - The ResultsManager instance containing all test results. - md_filename - The path to the markdown file to write the report into. - """ - try: - with md_filename.open("w", encoding="utf-8") as mdfile: - sections: list[MDReportBase] = [ - ANTAReport(mdfile, results), - TestResultsSummary(mdfile, results), - SummaryTotals(mdfile, results), - SummaryTotalsDeviceUnderTest(mdfile, results), - SummaryTotalsPerCategory(mdfile, results), - TestResults(mdfile, results), - ] - for section in sections: - section.generate_section() - except OSError as exc: - message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'." - anta_log_exception(exc, message, logger) - raise - - class MDReportBase(ABC): """Base class for all sections subclasses. @@ -178,10 +139,7 @@ class MDReportBase(ABC): return "" # Replace newlines with
to preserve line breaks in HTML - text = text.replace("\n", "
") - - # Replace backticks with single quotes - return text.replace("`", "'") + return text.replace("\n", "
") class ANTAReport(MDReportBase): @@ -297,3 +255,68 @@ class TestResults(MDReportBase): """Generate the `## Test Results` section of the markdown report.""" self.write_heading(heading_level=2) self.write_table(table_heading=self.TABLE_HEADING, last_table=True) + + +# pylint: disable=too-few-public-methods +class MDReportGenerator: + """Class responsible for generating a Markdown report based on the provided `ResultManager` object. + + It aggregates different report sections, each represented by a subclass of `MDReportBase`, + and sequentially generates their content into a markdown file. + + This class provides two methods for generating the report: + + - `generate`: Uses a single result manager instance to generate all sections defined in the `DEFAULT_SECTIONS` class variable list. + + - `generate_sections`: A custom list of sections is provided. Each section uses its own dedicated result manager instance, + allowing greater flexibility or isolation between section generations. + """ + + DEFAULT_SECTIONS: ClassVar[list[type[MDReportBase]]] = [ + ANTAReport, + TestResultsSummary, + SummaryTotals, + SummaryTotalsDeviceUnderTest, + SummaryTotalsPerCategory, + TestResults, + ] + + @classmethod + def generate(cls, results: ResultManager, md_filename: Path) -> None: + """Generate the sections of the markdown report defined in DEFAULT_SECTIONS using a single result manager instance for all sections. + + Parameters + ---------- + results + The ResultsManager instance containing all test results. + md_filename + The path to the markdown file to write the report into. + """ + try: + with md_filename.open("w", encoding="utf-8") as mdfile: + for section in cls.DEFAULT_SECTIONS: + section(mdfile, results).generate_section() + except OSError as exc: + message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'." + anta_log_exception(exc, message, logger) + raise + + @classmethod + def generate_sections(cls, sections: list[tuple[type[MDReportBase], ResultManager]], md_filename: Path) -> None: + """Generate the different sections of the markdown report provided in the sections argument with each section using its own result manager instance. + + Parameters + ---------- + sections + A list of tuples, where each tuple contains a subclass of `MDReportBase` and an instance of `ResultManager`. + md_filename + The path to the markdown file to write the report into. + """ + try: + with md_filename.open("w", encoding="utf-8") as md_file: + for section, rm in sections: + section(md_file, rm).generate_section() + except OSError as exc: + message = f"OSError caught while writing the Markdown file '{md_filename.resolve()}'." + anta_log_exception(exc, message, logger) + raise diff --git a/anta/result_manager/__init__.py b/anta/result_manager/__init__.py index 39ed364..32e9da4 100644 --- a/anta/result_manager/__init__.py +++ b/anta/result_manager/__init__.py @@ -21,7 +21,6 @@ from .models import CategoryStats, DeviceStats, TestStats logger = logging.getLogger(__name__) -# pylint: disable=too-many-instance-attributes class ResultManager: """Manager of ANTA Results. @@ -253,7 +252,7 @@ class ResultManager: if not set(sort_by).issubset(set(accepted_fields)): msg = f"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}" raise ValueError(msg) - results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by]) + results = sorted(results, key=lambda result: [getattr(result, field) or "" for field in sort_by]) return results @@ -295,7 +294,7 @@ class ResultManager: if not set(sort_by).issubset(set(accepted_fields)): msg = f"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}" raise ValueError(msg) - self._result_entries.sort(key=lambda result: [getattr(result, field) for field in sort_by]) + self._result_entries.sort(key=lambda result: [getattr(result, field) or "" for field in sort_by]) return self def filter(self, hide: set[AntaTestStatus]) -> ResultManager: @@ -316,6 +315,25 @@ class ResultManager: manager.results = self.get_results(possible_statuses - hide) return manager + @classmethod + def merge_results(cls, results_managers: list[ResultManager]) -> ResultManager: + """Merge multiple ResultManager instances. + + Parameters + ---------- + results_managers + A list of ResultManager instances to merge. + + Returns + ------- + ResultManager + A new ResultManager instance containing the results of all the input ResultManagers. + """ + combined_results = list(chain(*(rm.results for rm in results_managers))) + merged_manager = cls() + merged_manager.results = combined_results + return merged_manager + @deprecated("This method is deprecated. This will be removed in ANTA v2.0.0.", category=DeprecationWarning) def filter_by_tests(self, tests: set[str]) -> ResultManager: """Get a filtered ResultManager that only contains specific tests. diff --git a/anta/result_manager/models.py b/anta/result_manager/models.py index a18ff57..a19c969 100644 --- a/anta/result_manager/models.py +++ b/anta/result_manager/models.py @@ -122,8 +122,6 @@ class TestResult(BaseModel): return f"Test '{self.test}' (on '{self.name}'): Result '{self.result}'\nMessages: {self.messages}" -# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058 -# pylint: disable=too-many-instance-attributes @dataclass class DeviceStats: """Device statistics for a run of tests.""" diff --git a/anta/runner.py b/anta/runner.py index 84e27a1..736da50 100644 --- a/anta/runner.py +++ b/anta/runner.py @@ -5,16 +5,16 @@ from __future__ import annotations -import asyncio import logging import os -import sys from collections import defaultdict from typing import TYPE_CHECKING, Any +from typing_extensions import deprecated + from anta import GITHUB_SUGGESTION +from anta._runner import AntaRunFilters, AntaRunner from anta.logger import anta_log_exception, exc_to_str -from anta.models import AntaTest from anta.tools import Catchtime, cprofile if TYPE_CHECKING: @@ -31,6 +31,7 @@ if os.name == "posix": DEFAULT_NOFILE = 16384 + @deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning) def adjust_rlimit_nofile() -> tuple[int, int]: """Adjust the maximum number of open file descriptors for the ANTA process. @@ -53,13 +54,17 @@ if os.name == "posix": logger.debug("Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1]) nofile = min(limits[1], nofile) logger.debug("Setting soft limit for open file descriptors for the current ANTA process to %s", nofile) - resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1])) + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1])) + except ValueError as exception: + logger.warning("Failed to set soft limit for open file descriptors for the current ANTA process: %s", exc_to_str(exception)) return resource.getrlimit(resource.RLIMIT_NOFILE) logger = logging.getLogger(__name__) +@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning) def log_cache_statistics(devices: list[AntaDevice]) -> None: """Log cache statistics for each device in the inventory. @@ -80,6 +85,7 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None: logger.info("Caching is not enabled on %s", device.name) +@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning) async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None: """Set up the inventory for the ANTA run. @@ -122,6 +128,7 @@ async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devic return selected_inventory +@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning) def prepare_tests( inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None ) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None: @@ -178,6 +185,7 @@ def prepare_tests( return device_to_tests +@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning) def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager | None = None) -> list[Coroutine[Any, Any, TestResult]]: """Get the coroutines for the ANTA run. @@ -250,62 +258,11 @@ async def main( dry_run Build the list of coroutine to run and stop before test execution. """ - if not catalog.tests: - logger.info("The list of tests is empty, exiting") - return - - with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"): - # Setup the inventory - selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only) - if selected_inventory is None: - return - - with Catchtime(logger=logger, message="Preparing the tests"): - selected_tests = prepare_tests(selected_inventory, catalog, tests, tags) - if selected_tests is None: - return - final_tests_count = sum(len(tests) for tests in selected_tests.values()) - - run_info = ( - "--- ANTA NRFU Run Information ---\n" - f"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\n" - f"Total number of selected tests: {final_tests_count}\n" - ) - - if os.name == "posix": - # Adjust the maximum number of open file descriptors for the ANTA process - limits = adjust_rlimit_nofile() - run_info += f"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\n" - else: - # Running on non-Posix system, cannot manage the resource. - limits = (sys.maxsize, sys.maxsize) - run_info += "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.\n" - - run_info += "---------------------------------" - - logger.info(run_info) - - if final_tests_count > limits[0]: - logger.warning( - "The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\n" - "Errors may occur while running the tests.\n" - "Please consult the ANTA FAQ." - ) - - coroutines = get_coroutines(selected_tests, manager if dry_run else None) - - if dry_run: - logger.info("Dry-run mode, exiting before running the tests.") - for coro in coroutines: - coro.close() - return - - if AntaTest.progress is not None: - AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=len(coroutines)) - - with Catchtime(logger=logger, message="Running ANTA tests"): - results = await asyncio.gather(*coroutines) - for result in results: - manager.add(result) - - log_cache_statistics(selected_inventory.devices) + runner = AntaRunner() + filters = AntaRunFilters( + devices=devices, + tests=tests, + tags=tags, + established_only=established_only, + ) + await runner.run(inventory, catalog, manager, filters, dry_run=dry_run) diff --git a/anta/settings.py b/anta/settings.py new file mode 100644 index 0000000..88fdf44 --- /dev/null +++ b/anta/settings.py @@ -0,0 +1,86 @@ +# 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. +"""Settings for ANTA.""" + +from __future__ import annotations + +import logging +import os +import sys +from typing import Any + +from pydantic import Field, PositiveInt +from pydantic_settings import BaseSettings, SettingsConfigDict + +from anta.logger import exc_to_str + +logger = logging.getLogger(__name__) + +DEFAULT_MAX_CONCURRENCY = 50000 +"""Default value for the maximum number of concurrent tests in the event loop.""" + +DEFAULT_NOFILE = 16384 +"""Default value for the maximum number of open file descriptors for the ANTA process.""" + + +class AntaRunnerSettings(BaseSettings): + """Environment variables for configuring the ANTA runner. + + When initialized, relevant environment variables are loaded. If not set, default values are used. + + On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable + while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit. + + On non-POSIX systems (Windows), sets the limit to `sys.maxsize`. + + The adjusted limit is available with the `file_descriptor_limit` property after initialization. + + Attributes + ---------- + nofile : PositiveInt + Environment variable: ANTA_NOFILE + + The maximum number of open file descriptors for the ANTA process. Defaults to 16384. + + max_concurrency : PositiveInt + Environment variable: ANTA_MAX_CONCURRENCY + + The maximum number of concurrent tests that can run in the event loop. Defaults to 50000. + """ + + model_config = SettingsConfigDict(env_prefix="ANTA_") + + nofile: PositiveInt = Field(default=DEFAULT_NOFILE) + max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY) + + # Computed in post-init + _file_descriptor_limit: PositiveInt + + # pylint: disable=arguments-differ + def model_post_init(self, _context: Any) -> None: # noqa: ANN401 + """Post-initialization method to set the file descriptor limit for the current ANTA process.""" + if os.name != "posix": + logger.warning("Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.") + self._file_descriptor_limit = sys.maxsize + return + + import resource + + limits = resource.getrlimit(resource.RLIMIT_NOFILE) + logger.debug("Initial file descriptor limits for the current ANTA process: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1]) + + # Set new soft limit to minimum of requested and hard limit + new_soft_limit = min(limits[1], self.nofile) + logger.debug("Setting file descriptor soft limit to %s", new_soft_limit) + try: + resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, limits[1])) + except ValueError as exception: + logger.warning("Failed to set file descriptor soft limit for the current ANTA process: %s", exc_to_str(exception)) + + self._file_descriptor_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + + @property + def file_descriptor_limit(self) -> PositiveInt: + """The maximum number of file descriptors available to the process.""" + return self._file_descriptor_limit diff --git a/anta/tests/connectivity.py b/anta/tests/connectivity.py index a5ba5ff..17e7eaf 100644 --- a/anta/tests/connectivity.py +++ b/anta/tests/connectivity.py @@ -38,8 +38,7 @@ class VerifyReachability(AntaTest): df_bit: True size: 100 reachable: true - - source: Management0 - destination: 8.8.8.8 + - destination: 8.8.8.8 vrf: MGMT df_bit: True size: 100 @@ -55,7 +54,7 @@ class VerifyReachability(AntaTest): categories: ClassVar[list[str]] = ["connectivity"] # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ - AntaTemplate(template="ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}", revision=1) + AntaTemplate(template="ping vrf {vrf} {destination}{source} size {size}{df_bit} repeat {repeat}", revision=1) ] class Input(AntaTest.Input): @@ -71,7 +70,7 @@ class VerifyReachability(AntaTest): def validate_hosts(cls, hosts: list[T]) -> list[T]: """Validate the 'destination' and 'source' IP address family in each host.""" for host in hosts: - if not isinstance(host.source, str) and host.destination.version != host.source.version: + if host.source and not isinstance(host.source, str) and host.destination.version != host.source.version: msg = f"{host} IP address family for destination does not match source" raise ValueError(msg) return hosts @@ -80,7 +79,12 @@ class VerifyReachability(AntaTest): """Render the template for each host in the input list.""" return [ template.render( - destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=" df-bit" if host.df_bit else "" + destination=host.destination, + source=f" source {host.source}" if host.source else "", + vrf=host.vrf, + repeat=host.repeat, + size=host.size, + df_bit=" df-bit" if host.df_bit else "", ) for host in self.inputs.hosts ] diff --git a/anta/tests/cvx.py b/anta/tests/cvx.py index 25d8245..d80f9b9 100644 --- a/anta/tests/cvx.py +++ b/anta/tests/cvx.py @@ -149,7 +149,7 @@ class VerifyMcsServerMounts(AntaTest): active_count = 0 if not (connections := command_output.get("connections")): - self.result.is_failure("CVX connections are not available.") + self.result.is_failure("CVX connections are not available") return for connection in connections: diff --git a/anta/tests/evpn.py b/anta/tests/evpn.py new file mode 100644 index 0000000..a72ca03 --- /dev/null +++ b/anta/tests/evpn.py @@ -0,0 +1,185 @@ +# 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 related to EVPN tests.""" + +# mypy: disable-error-code=attr-defined +from __future__ import annotations + +from typing import Any, ClassVar + +from anta.input_models.evpn import EVPNPath, EVPNRoute, EVPNType5Prefix +from anta.models import AntaCommand, AntaTemplate, AntaTest + + +class VerifyEVPNType5Routes(AntaTest): + """Verifies EVPN Type-5 routes for given IP prefixes and VNIs. + + It supports multiple levels of verification based on the provided input: + + 1. **Prefix/VNI only:** Verifies there is at least one 'active' and 'valid' path across all + Route Distinguishers (RDs) learning the given prefix and VNI. + 2. **Specific Routes (RD/Domain):** Verifies that routes matching the specified RDs and domains + exist for the prefix/VNI. For each specified route, it checks if at least one of its paths + is 'active' and 'valid'. + 3. **Specific Paths (Nexthop/Route Targets):** Verifies that specific paths exist within a + specified route (RD/Domain). For each specified path criteria (nexthop and optional route targets), + it finds all matching paths received from the peer and checks if at least one of these + matching paths is 'active' and 'valid'. The route targets check ensures all specified RTs + are present in the path's extended communities (subset check). + + Expected Results + ---------------- + * Success: + - If only prefix/VNI is provided: The prefix/VNI exists in the EVPN table + and has at least one active and valid path across all RDs. + - If specific routes are provided: All specified routes (by RD/Domain) are found, + and each has at least one active and valid path (if paths are not specified for the route). + - If specific paths are provided: All specified routes are found, and for each specified path criteria (nexthop/RTs), + at least one matching path exists and is active and valid. + * Failure: + - No EVPN Type-5 routes are found for the given prefix/VNI. + - A specified route (RD/Domain) is not found. + - No active and valid path is found when required (either globally for the prefix, per specified route, or per specified path criteria). + - A specified path criteria (nexthop/RTs) does not match any received paths for the route. + + Examples + -------- + ```yaml + anta.tests.evpn: + - VerifyEVPNType5Routes: + prefixes: + # At least one active/valid path across all RDs + - address: 192.168.10.0/24 + vni: 10 + # Specific routes each has at least one active/valid path + - address: 192.168.20.0/24 + vni: 20 + routes: + - rd: "10.0.0.1:20" + domain: local + - rd: "10.0.0.2:20" + domain: remote + # At least one active/valid path matching the nexthop + - address: 192.168.30.0/24 + vni: 30 + routes: + - rd: "10.0.0.1:30" + domain: local + paths: + - nexthop: 10.1.1.1 + # At least one active/valid path matching nexthop and specific RTs + - address: 192.168.40.0/24 + vni: 40 + routes: + - rd: "10.0.0.1:40" + domain: local + paths: + - nexthop: 10.1.1.1 + route_targets: + - "40:40" + ``` + """ + + categories: ClassVar[list[str]] = ["bgp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show bgp evpn route-type ip-prefix {address} vni {vni}", revision=2)] + + class Input(AntaTest.Input): + """Input model for the VerifyEVPNType5Routes test.""" + + prefixes: list[EVPNType5Prefix] + """List of EVPN Type-5 prefixes to verify.""" + + def render(self, template: AntaTemplate) -> list[AntaCommand]: + """Render the template for each EVPN Type-5 prefix in the input list.""" + return [template.render(address=str(prefix.address), vni=prefix.vni) for prefix in self.inputs.prefixes] + + # NOTE: The following static methods can be moved at the module level if needed for other EVPN tests + @staticmethod + def _get_all_paths(evpn_routes_data: dict[str, Any]) -> list[dict[str, Any]]: + """Extract all 'evpnRoutePaths' from the entire 'evpnRoutes' dictionary.""" + all_paths = [] + for route_data in evpn_routes_data.values(): + all_paths.extend(route_data["evpnRoutePaths"]) + return all_paths + + @staticmethod + def _find_route(evpn_routes_data: dict[str, Any], rd_to_find: str, domain_to_find: str) -> dict[str, Any] | None: + """Find the specific route block for a given RD and domain.""" + for route_data in evpn_routes_data.values(): + if route_data["routeKeyDetail"].get("rd") == rd_to_find and route_data["routeKeyDetail"].get("domain") == domain_to_find: + return route_data + return None + + @staticmethod + def _find_paths(paths: list[dict[str, Any]], nexthop: str, route_targets: list[str] | None = None) -> list[dict[str, Any]]: + """Find all matching paths for a given nexthop and RTs.""" + route_targets = [f"Route-Target-AS:{rt}" for rt in route_targets] if route_targets is not None else [] + return [path for path in paths if path["nextHop"] == nexthop and set(route_targets).issubset(set(path["routeDetail"]["extCommunities"]))] + + @staticmethod + def _has_active_valid_path(paths: list[dict[str, Any]]) -> bool: + """Check if any path in the list is active and valid.""" + return any(path["routeType"]["active"] and path["routeType"]["valid"] for path in paths) + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyEVPNType5Routes.""" + self.result.is_success() + + for command, prefix_input in zip(self.instance_commands, self.inputs.prefixes): + # Verify that the prefix is in the BGP EVPN table + evpn_routes_data = command.json_output.get("evpnRoutes") + if not evpn_routes_data: + self.result.is_failure(f"{prefix_input} - No EVPN Type-5 routes found") + continue + + # Delegate verification logic for this prefix + self._verify_routes_for_prefix(prefix_input, evpn_routes_data) + + def _verify_routes_for_prefix(self, prefix_input: EVPNType5Prefix, evpn_routes_data: dict[str, Any]) -> None: + """Verify EVPN routes for an input prefix.""" + # Case: routes not provided for the prefix, check that at least one EVPN Type-5 route + # has at least one active and valid path across all learned routes from all RDs combined + if prefix_input.routes is None: + all_paths = self._get_all_paths(evpn_routes_data) + if not self._has_active_valid_path(all_paths): + self.result.is_failure(f"{prefix_input} - No active and valid path found across all RDs") + return + + # Case: routes *is* provided, check each specified route + for route_input in prefix_input.routes: + # Try to find a route with matching RD and domain + route_data = self._find_route(evpn_routes_data, route_input.rd, route_input.domain) + if route_data is None: + self.result.is_failure(f"{prefix_input} {route_input} - Route not found") + continue + + # Route found, now check its paths based on route_input criteria + self._verify_paths_for_route(prefix_input, route_input, route_data) + + def _verify_paths_for_route(self, prefix_input: EVPNType5Prefix, route_input: EVPNRoute, route_data: dict[str, Any]) -> None: + """Verify paths for a specific EVPN route (route_data) based on route_input criteria.""" + route_paths = route_data["evpnRoutePaths"] + + # Case: paths not provided for the route, check that at least one path is active/valid + if route_input.paths is None: + if not self._has_active_valid_path(route_paths): + self.result.is_failure(f"{prefix_input} {route_input} - No active and valid path found") + return + + # Case: paths *is* provided, check each specified path criteria + for path_input in route_input.paths: + self._verify_single_path(prefix_input, route_input, path_input, route_paths) + + def _verify_single_path(self, prefix_input: EVPNType5Prefix, route_input: EVPNRoute, path_input: EVPNPath, available_paths: list[dict[str, Any]]) -> None: + """Verify if at least one active/valid path exists among available_paths matching the path_input criteria.""" + # Try to find all paths matching nexthop and RTs criteria from the available paths for this route + matching_paths = self._find_paths(available_paths, path_input.nexthop, path_input.route_targets) + if not matching_paths: + self.result.is_failure(f"{prefix_input} {route_input} {path_input} - Path not found") + return + + # Check that at least one matching path is active/valid + if not self._has_active_valid_path(matching_paths): + self.result.is_failure(f"{prefix_input} {route_input} {path_input} - No active and valid path found") diff --git a/anta/tests/field_notices.py b/anta/tests/field_notices.py index cc7fab9..ed8022f 100644 --- a/anta/tests/field_notices.py +++ b/anta/tests/field_notices.py @@ -38,7 +38,7 @@ class VerifyFieldNotice44Resolution(AntaTest): categories: ClassVar[list[str]] = ["field notices"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyFieldNotice44Resolution.""" @@ -142,7 +142,7 @@ class VerifyFieldNotice72Resolution(AntaTest): categories: ClassVar[list[str]] = ["field notices"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyFieldNotice72Resolution.""" diff --git a/anta/tests/flow_tracking.py b/anta/tests/flow_tracking.py index a115949..57b4254 100644 --- a/anta/tests/flow_tracking.py +++ b/anta/tests/flow_tracking.py @@ -109,7 +109,7 @@ class VerifyHardwareFlowTrackerStatus(AntaTest): 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.") + self.result.is_failure("Hardware flow tracking is not running") return for tracker in self.inputs.trackers: diff --git a/anta/tests/hardware.py b/anta/tests/hardware.py index 7edd41b..8bac413 100644 --- a/anta/tests/hardware.py +++ b/anta/tests/hardware.py @@ -9,6 +9,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, ClassVar +from anta.custom_types import PowerSupplyFanStatus, PowerSupplyStatus from anta.decorators import skip_on_platforms from anta.models import AntaCommand, AntaTest @@ -45,7 +46,7 @@ class VerifyTransceiversManufacturers(AntaTest): manufacturers: list[str] """List of approved transceivers manufacturers.""" - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTransceiversManufacturers.""" @@ -78,7 +79,7 @@ class VerifyTemperature(AntaTest): categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTemperature.""" @@ -108,7 +109,7 @@ class VerifyTransceiversTemperature(AntaTest): categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature transceiver", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTransceiversTemperature.""" @@ -141,7 +142,7 @@ class VerifyEnvironmentSystemCooling(AntaTest): categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment cooling", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyEnvironmentSystemCooling.""" @@ -176,10 +177,10 @@ class VerifyEnvironmentCooling(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyEnvironmentCooling test.""" - states: list[str] + states: list[PowerSupplyFanStatus] """List of accepted states of fan status.""" - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyEnvironmentCooling.""" @@ -225,10 +226,10 @@ class VerifyEnvironmentPower(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyEnvironmentPower test.""" - states: list[str] + states: list[PowerSupplyStatus] """List of accepted states list of power supplies status.""" - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyEnvironmentPower.""" @@ -259,7 +260,7 @@ class VerifyAdverseDrops(AntaTest): categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware counter drop", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyAdverseDrops.""" diff --git a/anta/tests/interfaces.py b/anta/tests/interfaces.py index e291bd6..25a048d 100644 --- a/anta/tests/interfaces.py +++ b/anta/tests/interfaces.py @@ -13,7 +13,7 @@ from typing import ClassVar, TypeVar from pydantic import Field, field_validator from pydantic_extra_types.mac_address import MacAddress -from anta.custom_types import Interface, Percent, PositiveInteger +from anta.custom_types import Interface, InterfaceType, Percent, PortChannelInterface, PositiveInteger from anta.decorators import skip_on_platforms from anta.input_models.interfaces import InterfaceDetail, InterfaceState from anta.models import AntaCommand, AntaTemplate, AntaTest @@ -25,17 +25,62 @@ BPS_GBPS_CONVERSIONS = 1000000000 T = TypeVar("T", bound=InterfaceState) +def _is_interface_ignored(interface: str, ignored_interfaces: list[str] | None = None) -> bool | None: + """Verify if an interface is present in the ignored interfaces list. + + Parameters + ---------- + interface + This is a string containing the interface name. + ignored_interfaces + A list containing the interfaces or interface types to ignore. + + Returns + ------- + bool + True if the interface is in the list of ignored interfaces, false otherwise. + Example + ------- + ```python + >>> _is_interface_ignored(interface="Ethernet1", ignored_interfaces=["Ethernet", "Port-Channel1"]) + True + >>> _is_interface_ignored(interface="Ethernet2", ignored_interfaces=["Ethernet1", "Port-Channel"]) + False + >>> _is_interface_ignored(interface="Port-Channel1", ignored_interfaces=["Ethernet1", "Port-Channel"]) + True + >>> _is_interface_ignored(interface="Ethernet1/1", ignored_interfaces: ["Ethernet1/1", "Port-Channel"]) + True + >>> _is_interface_ignored(interface="Ethernet1/1", ignored_interfaces: ["Ethernet1", "Port-Channel"]) + False + >>> _is_interface_ignored(interface="Ethernet1.100", ignored_interfaces: ["Ethernet1.100", "Port-Channel"]) + True + ``` + """ + interface_prefix = re.findall(r"^[a-zA-Z-]+", interface, re.IGNORECASE)[0] + interface_exact_match = False + if ignored_interfaces: + for ignored_interface in ignored_interfaces: + if interface == ignored_interface: + interface_exact_match = True + break + return bool(any([interface_exact_match, interface_prefix in ignored_interfaces])) + return None + + class VerifyInterfaceUtilization(AntaTest): """Verifies that the utilization of interfaces is below a certain threshold. Load interval (default to 5 minutes) is defined in device configuration. - This test has been implemented for full-duplex interfaces only. + + !!! warning + This test has been implemented for full-duplex interfaces only. Expected Results ---------------- * Success: The test will pass if all interfaces have a usage below the threshold. - * Failure: The test will fail if one or more interfaces have a usage above the threshold. - * Error: The test will error out if the device has at least one non full-duplex interface. + * Failure: If any of the following occur: + - One or more interfaces have a usage above the threshold. + - The device has at least one non full-duplex interface. Examples -------- @@ -43,6 +88,9 @@ class VerifyInterfaceUtilization(AntaTest): anta.tests.interfaces: - VerifyInterfaceUtilization: threshold: 70.0 + ignored_interfaces: + - Ethernet1 + - Port-Channel1 ``` """ @@ -56,7 +104,9 @@ class VerifyInterfaceUtilization(AntaTest): """Input model for the VerifyInterfaceUtilization test.""" threshold: Percent = 75.0 - """Interface utilization threshold above which the test will fail. Defaults to 75%.""" + """Interface utilization threshold above which the test will fail.""" + ignored_interfaces: list[InterfaceType | Interface] | None = None + """A list of interfaces or interface types like Management which will ignore all Management interfaces.""" @AntaTest.anta_test def test(self) -> None: @@ -67,12 +117,23 @@ class VerifyInterfaceUtilization(AntaTest): interfaces = self.instance_commands[1].json_output for intf, rate in rates["interfaces"].items(): + interface_data = [] + # Verification is skipped if the interface is in the ignored interfaces list. + if _is_interface_ignored(intf, self.inputs.ignored_interfaces): + continue + # The utilization logic has been implemented for full-duplex interfaces only - if ((duplex := (interface := interfaces["interfaces"][intf]).get("duplex", None)) is not None and duplex != duplex_full) or ( - (members := interface.get("memberInterfaces", None)) is not None and any(stats["duplex"] != duplex_full for stats in members.values()) - ): - self.result.is_failure(f"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.") - return + if not all([duplex := (interface := interfaces["interfaces"][intf]).get("duplex", None), duplex == duplex_full]): + if (members := interface.get("memberInterfaces", None)) is None: + self.result.is_failure(f"Interface: {intf} - Test not implemented for non-full-duplex interfaces - Expected: {duplex_full} Actual: {duplex}") + continue + interface_data = [(member_interface, state) for member_interface, stats in members.items() if (state := stats["duplex"]) != duplex_full] + + for member_interface in interface_data: + self.result.is_failure( + f"Interface: {intf} Member Interface: {member_interface[0]} - Test not implemented for non-full-duplex interfaces - Expected: {duplex_full}" + f" Actual: {member_interface[1]}" + ) if (bandwidth := interfaces["interfaces"][intf]["bandwidth"]) == 0: self.logger.debug("Interface %s has been ignored due to null bandwidth value", intf) @@ -106,12 +167,21 @@ class VerifyInterfaceErrors(AntaTest): categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters errors", revision=1)] + class Input(AntaTest.Input): + """Input model for the VerifyInterfaceErrors test.""" + + ignored_interfaces: list[InterfaceType | Interface] | None = None + """A list of interfaces or interface types like Management which will ignore all Management interfaces.""" + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceErrors.""" self.result.is_success() command_output = self.instance_commands[0].json_output for interface, counters in command_output["interfaceErrorCounters"].items(): + # Verification is skipped if the interface is in the ignored interfaces list. + if _is_interface_ignored(interface, self.inputs.ignored_interfaces): + continue counters_data = [f"{counter}: {value}" for counter, value in counters.items() if value > 0] if counters_data: self.result.is_failure(f"Interface: {interface} - Non-zero error counter(s) - {', '.join(counters_data)}") @@ -130,18 +200,30 @@ class VerifyInterfaceDiscards(AntaTest): ```yaml anta.tests.interfaces: - VerifyInterfaceDiscards: + ignored_interfaces: + - Ethernet + - Port-Channel1 ``` """ categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters discards", revision=1)] + class Input(AntaTest.Input): + """Input model for the VerifyInterfaceDiscards test.""" + + ignored_interfaces: list[InterfaceType | Interface] | None = None + """A list of interfaces or interface types like Management which will ignore all Management interfaces.""" + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceDiscards.""" self.result.is_success() command_output = self.instance_commands[0].json_output for interface, interface_data in command_output["interfaces"].items(): + # Verification is skipped if the interface is in the ignored interfaces list. + if _is_interface_ignored(interface, self.inputs.ignored_interfaces): + continue counters_data = [f"{counter}: {value}" for counter, value in interface_data.items() if value > 0] if counters_data: self.result.is_failure(f"Interface: {interface} - Non-zero discard counter(s): {', '.join(counters_data)}") @@ -164,16 +246,22 @@ class VerifyInterfaceErrDisabled(AntaTest): """ categories: ClassVar[list[str]] = ["interfaces"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces status", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces status errdisabled", revision=1)] @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceErrDisabled.""" self.result.is_success() command_output = self.instance_commands[0].json_output - for interface, value in command_output["interfaceStatuses"].items(): - if value["linkStatus"] == "errdisabled": - self.result.is_failure(f"Interface: {interface} - Link status Error disabled") + if not (interface_details := get_value(command_output, "interfaceStatuses")): + return + + for interface, value in interface_details.items(): + if causes := value.get("causes"): + msg = f"Interface: {interface} - Error disabled - Causes: {', '.join(causes)}" + self.result.is_failure(msg) + continue + self.result.is_failure(f"Interface: {interface} - Error disabled") class VerifyInterfacesStatus(AntaTest): @@ -276,7 +364,7 @@ class VerifyStormControlDrops(AntaTest): categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show storm-control", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyStormControlDrops.""" @@ -305,18 +393,31 @@ class VerifyPortChannels(AntaTest): ```yaml anta.tests.interfaces: - VerifyPortChannels: + ignored_interfaces: + - Port-Channel1 + - Port-Channel2 + ``` """ categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show port-channel", revision=1)] + class Input(AntaTest.Input): + """Input model for the VerifyPortChannels test.""" + + ignored_interfaces: list[PortChannelInterface] | None = None + """A list of port-channel interfaces to ignore.""" + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPortChannels.""" self.result.is_success() command_output = self.instance_commands[0].json_output for port_channel, port_channel_details in command_output["portChannels"].items(): + # Verification is skipped if the interface is in the ignored interfaces list. + if _is_interface_ignored(port_channel, self.inputs.ignored_interfaces): + continue # Verify that the no inactive ports in all port channels. if inactive_ports := port_channel_details["inactivePorts"]: self.result.is_failure(f"{port_channel} - Inactive port(s) - {', '.join(inactive_ports.keys())}") @@ -335,18 +436,30 @@ class VerifyIllegalLACP(AntaTest): ```yaml anta.tests.interfaces: - VerifyIllegalLACP: + ignored_interfaces: + - Port-Channel1 + - Port-Channel2 ``` """ categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lacp counters all-ports", revision=1)] + class Input(AntaTest.Input): + """Input model for the VerifyIllegalLACP test.""" + + ignored_interfaces: list[PortChannelInterface] | None = None + """A list of port-channel interfaces to ignore.""" + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyIllegalLACP.""" self.result.is_success() command_output = self.instance_commands[0].json_output for port_channel, port_channel_dict in command_output["portChannels"].items(): + # Verification is skipped if the interface is in the ignored interfaces list. + if _is_interface_ignored(port_channel, self.inputs.ignored_interfaces): + continue for interface, interface_details in port_channel_dict["interfaces"].items(): # Verify that the no illegal LACP packets in all port channels. if interface_details["illegalRxCount"] != 0: @@ -431,11 +544,9 @@ class VerifySVI(AntaTest): class VerifyL3MTU(AntaTest): - """Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces. + """Verifies the L3 MTU of routed interfaces. - Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces. - - You can define a global MTU to check, or an MTU per interface and you can also ignored some interfaces. + Test that layer 3 (routed) interfaces are configured with the correct MTU. Expected Results ---------------- @@ -449,9 +560,11 @@ class VerifyL3MTU(AntaTest): - VerifyL3MTU: mtu: 1500 ignored_interfaces: - - Vxlan1 + - Management # Ignore all Management interfaces + - Ethernet2.100 + - Ethernet1/1 specific_mtu: - - Ethernet1: 2500 + - Ethernet10: 9200 ``` """ @@ -463,33 +576,31 @@ class VerifyL3MTU(AntaTest): """Input model for the VerifyL3MTU test.""" mtu: int = 1500 - """Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500.""" - ignored_interfaces: list[str] = Field(default=["Management", "Loopback", "Vxlan", "Tunnel"]) - """A list of L3 interfaces to ignore""" - specific_mtu: list[dict[str, int]] = Field(default=[]) - """A list of dictionary of L3 interfaces with their specific MTU configured""" + """Expected L3 MTU configured on all non-excluded interfaces.""" + ignored_interfaces: list[InterfaceType | Interface] = Field(default=["Dps", "Fabric", "Loopback", "Management", "Recirc-Channel", "Tunnel", "Vxlan"]) + """A list of L3 interfaces or interfaces types like Loopback, Tunnel which will ignore all Loopback and Tunnel interfaces. + + Takes precedence over the `specific_mtu` field.""" + specific_mtu: list[dict[Interface, int]] = Field(default=[]) + """A list of dictionary of L3 interfaces with their expected L3 MTU configured.""" @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyL3MTU.""" self.result.is_success() command_output = self.instance_commands[0].json_output - # Set list of interfaces with specific settings - specific_interfaces: list[str] = [] - if self.inputs.specific_mtu: - for d in self.inputs.specific_mtu: - specific_interfaces.extend(d) - for interface, values in command_output["interfaces"].items(): - if re.findall(r"[a-z]+", interface, re.IGNORECASE)[0] not in self.inputs.ignored_interfaces and values["forwardingModel"] == "routed": - if interface in specific_interfaces: - invalid_mtu = next( - (values["mtu"] for custom_data in self.inputs.specific_mtu if values["mtu"] != (expected_mtu := custom_data[interface])), None - ) - if invalid_mtu: - self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {expected_mtu} Actual: {invalid_mtu}") - # Comparison with generic setting - elif values["mtu"] != self.inputs.mtu: - self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {self.inputs.mtu} Actual: {values['mtu']}") + specific_interfaces = {intf: mtu for intf_mtu in self.inputs.specific_mtu for intf, mtu in intf_mtu.items()} + + for interface, details in command_output["interfaces"].items(): + # Verification is skipped if the interface is in the ignored interfaces list + if _is_interface_ignored(interface, self.inputs.ignored_interfaces) or details["forwardingModel"] != "routed": + continue + + actual_mtu = details["mtu"] + expected_mtu = specific_interfaces.get(interface, self.inputs.mtu) + + if (actual_mtu := details["mtu"]) != expected_mtu: + self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {expected_mtu} Actual: {actual_mtu}") class VerifyIPProxyARP(AntaTest): @@ -536,10 +647,9 @@ class VerifyIPProxyARP(AntaTest): class VerifyL2MTU(AntaTest): - """Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces. + """Verifies the L2 MTU of bridged interfaces. - Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces. - You can define a global MTU to check and also an MTU per interface and also ignored some interfaces. + Test that layer 2 (bridged) interfaces are configured with the correct MTU. Expected Results ---------------- @@ -551,10 +661,10 @@ class VerifyL2MTU(AntaTest): ```yaml anta.tests.interfaces: - VerifyL2MTU: - mtu: 1500 + mtu: 9214 ignored_interfaces: - - Management1 - - Vxlan1 + - Ethernet2/1 + - Port-Channel # Ignore all Port-Channel interfaces specific_mtu: - Ethernet1/1: 1500 ``` @@ -568,28 +678,31 @@ class VerifyL2MTU(AntaTest): """Input model for the VerifyL2MTU test.""" mtu: int = 9214 - """Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214.""" - ignored_interfaces: list[str] = Field(default=["Management", "Loopback", "Vxlan", "Tunnel"]) - """A list of L2 interfaces to ignore. Defaults to ["Management", "Loopback", "Vxlan", "Tunnel"]""" + """Expected L2 MTU configured on all non-excluded interfaces.""" + ignored_interfaces: list[InterfaceType | Interface] = Field(default=["Dps", "Fabric", "Loopback", "Management", "Recirc-Channel", "Tunnel", "Vlan", "Vxlan"]) + """A list of L2 interfaces or interface types like Ethernet, Port-Channel which will ignore all Ethernet and Port-Channel interfaces. + + Takes precedence over the `specific_mtu` field.""" specific_mtu: list[dict[Interface, int]] = Field(default=[]) - """A list of dictionary of L2 interfaces with their specific MTU configured""" + """A list of dictionary of L2 interfaces with their expected L2 MTU configured.""" @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyL2MTU.""" self.result.is_success() interface_output = self.instance_commands[0].json_output["interfaces"] - specific_interfaces = {key: value for details in self.inputs.specific_mtu for key, value in details.items()} + specific_interfaces = {intf: mtu for intf_mtu in self.inputs.specific_mtu for intf, mtu in intf_mtu.items()} for interface, details in interface_output.items(): - catch_interface = re.findall(r"^[e,p][a-zA-Z]+[-,a-zA-Z]*\d+\/*\d*", interface, re.IGNORECASE) - if catch_interface and catch_interface not in self.inputs.ignored_interfaces and details["forwardingModel"] == "bridged": - if interface in specific_interfaces: - if (mtu := specific_interfaces[interface]) != (act_mtu := details["mtu"]): - self.result.is_failure(f"Interface: {interface} - Incorrect MTU configured - Expected: {mtu} Actual: {act_mtu}") + # Verification is skipped if the interface is in the ignored interfaces list + if _is_interface_ignored(interface, self.inputs.ignored_interfaces) or details["forwardingModel"] != "bridged": + continue - elif (act_mtu := details["mtu"]) != self.inputs.mtu: - self.result.is_failure(f"Interface: {interface} - Incorrect MTU configured - Expected: {self.inputs.mtu} Actual: {act_mtu}") + actual_mtu = details["mtu"] + expected_mtu = specific_interfaces.get(interface, self.inputs.mtu) + + if (actual_mtu := details["mtu"]) != expected_mtu: + self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {expected_mtu} Actual: {actual_mtu}") class VerifyInterfaceIPv4(AntaTest): diff --git a/anta/tests/lanz.py b/anta/tests/lanz.py index 33e5472..0ace171 100644 --- a/anta/tests/lanz.py +++ b/anta/tests/lanz.py @@ -34,7 +34,7 @@ class VerifyLANZ(AntaTest): categories: ClassVar[list[str]] = ["lanz"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show queue-monitor length status", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyLANZ.""" diff --git a/anta/tests/logging.py b/anta/tests/logging.py index f13860e..cbc19bf 100644 --- a/anta/tests/logging.py +++ b/anta/tests/logging.py @@ -433,7 +433,7 @@ class VerifyLoggingErrors(AntaTest): if len(command_output) == 0: self.result.is_success() else: - self.result.is_failure("Device has reported syslog messages with a severity of ERRORS or higher") + self.result.is_failure(f"Device has reported syslog messages with a severity of ERRORS or higher:\n{command_output}") class VerifyLoggingEntries(AntaTest): @@ -450,10 +450,10 @@ class VerifyLoggingEntries(AntaTest): anta.tests.logging: - VerifyLoggingEntries: logging_entries: - - regex_match: ".ACCOUNTING-5-EXEC: cvpadmin ssh." + - regex_match: ".*ACCOUNTING-5-EXEC: cvpadmin ssh.*" last_number_messages: 30 severity_level: alerts - - regex_match: ".SPANTREE-6-INTERFACE_ADD:." + - regex_match: ".*SPANTREE-6-INTERFACE_ADD:.*" last_number_messages: 10 severity_level: critical ``` @@ -482,5 +482,5 @@ class VerifyLoggingEntries(AntaTest): output = command_output.text_output if not re.search(logging_entry.regex_match, output): self.result.is_failure( - f"Pattern: {logging_entry.regex_match} - Not found in last {logging_entry.last_number_messages} {logging_entry.severity_level} log entries" + f"Pattern: `{logging_entry.regex_match}` - Not found in last {logging_entry.last_number_messages} {logging_entry.severity_level} log entries" ) diff --git a/anta/tests/multicast.py b/anta/tests/multicast.py index fe09c94..0f8a278 100644 --- a/anta/tests/multicast.py +++ b/anta/tests/multicast.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, ClassVar -from anta.custom_types import Vlan +from anta.custom_types import VlanId from anta.models import AntaCommand, AntaTest if TYPE_CHECKING: @@ -41,7 +41,7 @@ class VerifyIGMPSnoopingVlans(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyIGMPSnoopingVlans test.""" - vlans: dict[Vlan, bool] + vlans: dict[VlanId, bool] """Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False).""" @AntaTest.anta_test diff --git a/anta/tests/profiles.py b/anta/tests/profiles.py index 1279948..7a22b7a 100644 --- a/anta/tests/profiles.py +++ b/anta/tests/profiles.py @@ -43,7 +43,7 @@ class VerifyUnifiedForwardingTableMode(AntaTest): mode: Literal[0, 1, 2, 3, 4, "flexible"] """Expected UFT mode. Valid values are 0, 1, 2, 3, 4, or "flexible".""" - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyUnifiedForwardingTableMode.""" @@ -81,7 +81,7 @@ class VerifyTcamProfile(AntaTest): profile: str """Expected TCAM profile.""" - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTcamProfile.""" diff --git a/anta/tests/ptp.py b/anta/tests/ptp.py index 309871b..ef8c026 100644 --- a/anta/tests/ptp.py +++ b/anta/tests/ptp.py @@ -36,7 +36,7 @@ class VerifyPtpModeStatus(AntaTest): categories: ClassVar[list[str]] = ["ptp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPtpModeStatus.""" @@ -81,7 +81,7 @@ class VerifyPtpGMStatus(AntaTest): categories: ClassVar[list[str]] = ["ptp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPtpGMStatus.""" @@ -116,7 +116,7 @@ class VerifyPtpLockStatus(AntaTest): categories: ClassVar[list[str]] = ["ptp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPtpLockStatus.""" @@ -155,7 +155,7 @@ class VerifyPtpOffset(AntaTest): categories: ClassVar[list[str]] = ["ptp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp monitor", revision=1)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPtpOffset.""" @@ -196,7 +196,7 @@ class VerifyPtpPortModeStatus(AntaTest): categories: ClassVar[list[str]] = ["ptp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)] - @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab", "vEOS"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPtpPortModeStatus.""" diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index bfbcb7f..018946d 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -42,6 +42,34 @@ def _check_bgp_neighbor_capability(capability_status: dict[str, bool]) -> bool: return all(capability_status.get(state, False) for state in ("advertised", "received", "enabled")) +def _get_bgp_peer_data(peer: BgpPeer, command_output: dict[str, Any]) -> dict[str, Any] | None: + """Retrieve BGP peer data for the given peer from the command output. + + Parameters + ---------- + peer + The BgpPeer object to look up. + command_output + Parsed output of the command. + + Returns + ------- + dict | None + The peer data dictionary if found, otherwise None. + """ + if peer.interface is not None: + # RFC5549 + identity = peer.interface + lookup_key = "ifName" + else: + identity = str(peer.peer_address) + lookup_key = "peerAddress" + + peer_list = get_value(command_output, f"vrfs.{peer.vrf}.peerList", default=[]) + + return get_item(peer_list, lookup_key, identity) + + class VerifyBGPPeerCount(AntaTest): """Verifies the count of BGP peers for given address families. @@ -348,14 +376,14 @@ class VerifyBGPSpecificPeers(AntaTest): class VerifyBGPPeerSession(AntaTest): - """Verifies the session state of BGP IPv4 peer(s). + """Verifies the session state of BGP peers. This test performs the following checks for each specified peer: 1. Verifies that the peer is found in its VRF in the BGP configuration. 2. Verifies that the BGP session is `Established` and, if specified, has remained established for at least the duration given by `minimum_established_time`. 3. Ensures that both input and output TCP message queues are empty. - Can be disabled by setting `check_tcp_queues` global flag to `False`. + Can be disabled by setting `check_tcp_queues` input flag to `False`. Expected Results ---------------- @@ -387,6 +415,13 @@ class VerifyBGPPeerSession(AntaTest): vrf: DEV - peer_address: 10.1.255.4 vrf: DEV + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default + - interface: Vlan3499 + vrf: PROD ``` """ @@ -397,11 +432,11 @@ class VerifyBGPPeerSession(AntaTest): """Input model for the VerifyBGPPeerSession test.""" minimum_established_time: PositiveInt | None = None - """Minimum established time (seconds) for all the BGP sessions.""" + """Minimum established time (seconds) for all BGP sessions.""" check_tcp_queues: bool = True - """Flag to check if the TCP session queues are empty for all BGP peers. Defaults to `True`.""" + """Flag to check if the TCP session queues are empty for all BGP peers.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" @AntaTest.anta_test def test(self) -> None: @@ -411,11 +446,8 @@ class VerifyBGPPeerSession(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -440,20 +472,20 @@ class VerifyBGPPeerSession(AntaTest): class VerifyBGPExchangedRoutes(AntaTest): """Verifies the advertised and received routes of BGP IPv4 peer(s). - This test performs the following checks for each specified peer: + This test performs the following checks for each advertised and received route for each peer: - For each advertised and received route: - - Confirms that the route exists in the BGP route table. - - Verifies that the route is in an 'active' and 'valid' state. + - Confirms that the route exists in the BGP route table. + - If `check_active` input flag is True, verifies that the route is 'valid' and 'active'. + - If `check_active` input flag is False, verifies that the route is 'valid'. Expected Results ---------------- * Success: If all of the following conditions are met: - All specified advertised/received routes are found in the BGP route table. - - All routes are in both 'active' and 'valid' states. + - All routes are 'active' and 'valid' or 'valid' only per the `check_active` input flag. * Failure: If any of the following occur: - An advertised/received route is not found in the BGP route table. - - Any route is not in an 'active' or 'valid' state. + - Any route is not 'active' and 'valid' or 'valid' only per `check_active` input flag. Examples -------- @@ -461,6 +493,7 @@ class VerifyBGPExchangedRoutes(AntaTest): anta.tests.routing: bgp: - VerifyBGPExchangedRoutes: + check_active: True bgp_peers: - peer_address: 172.30.255.5 vrf: default @@ -485,8 +518,10 @@ class VerifyBGPExchangedRoutes(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyBGPExchangedRoutes test.""" + check_active: bool = True + """Flag to check if the provided prefixes must be active and valid. If False, checks if the prefixes are valid only. """ bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpNeighbor: ClassVar[type[BgpNeighbor]] = BgpNeighbor @field_validator("bgp_peers") @@ -503,7 +538,7 @@ class VerifyBGPExchangedRoutes(AntaTest): """Render the template for each BGP peer in the input list.""" return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers] - def _validate_bgp_route_paths(self, peer: str, route_type: str, route: str, entries: dict[str, Any]) -> str | None: + def _validate_bgp_route_paths(self, peer: str, route_type: str, route: str, entries: dict[str, Any], *, active_flag: bool = True) -> str | None: """Validate the BGP route paths.""" # Check if the route is found if route in entries: @@ -511,8 +546,11 @@ class VerifyBGPExchangedRoutes(AntaTest): route_paths = entries[route]["bgpRoutePaths"][0]["routeType"] is_active = route_paths["active"] is_valid = route_paths["valid"] - if not is_active or not is_valid: - return f"{peer} {route_type} route: {route} - Valid: {is_valid} Active: {is_active}" + if active_flag: + if not is_active or not is_valid: + return f"{peer} {route_type} route: {route} - Valid: {is_valid} Active: {is_active}" + elif not is_valid: + return f"{peer} {route_type} route: {route} - Valid: {is_valid}" return None return f"{peer} {route_type} route: {route} - Not found" @@ -544,14 +582,14 @@ class VerifyBGPExchangedRoutes(AntaTest): entries = command_output[route_type] for route in routes: - # Check if the route is found. If yes then checks the route is active and valid - failure_msg = self._validate_bgp_route_paths(str(peer), route_type, str(route), entries) + # Check if the route is found. If yes then checks the route is active/valid + failure_msg = self._validate_bgp_route_paths(str(peer), route_type, str(route), entries, active_flag=self.inputs.check_active) if failure_msg: self.result.is_failure(failure_msg) class VerifyBGPPeerMPCaps(AntaTest): - """Verifies the multiprotocol capabilities of BGP IPv4 peer(s). + """Verifies the multiprotocol capabilities of BGP peers. This test performs the following checks for each specified peer: @@ -588,6 +626,19 @@ class VerifyBGPPeerMPCaps(AntaTest): capabilities: - ipv4 labeled-Unicast - ipv4MplsVpn + - peer_address: fd00:dc:1::1 + vrf: default + strict: False + capabilities: + - ipv4 labeled-Unicast + - ipv4MplsVpn + # RFC5549 + - interface: Ethernet1 + vrf: default + strict: False + capabilities: + - ipv4 labeled-Unicast + - ipv4MplsVpn ``` """ @@ -598,7 +649,7 @@ class VerifyBGPPeerMPCaps(AntaTest): """Input model for the VerifyBGPPeerMPCaps test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @field_validator("bgp_peers") @@ -619,16 +670,15 @@ class VerifyBGPPeerMPCaps(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue - # Fetching the multiprotocol capabilities - act_mp_caps = get_value(peer_data, "neighborCapabilities.multiprotocolCaps") + # Check if the multiprotocol capabilities are found + if (act_mp_caps := get_value(peer_data, "neighborCapabilities.multiprotocolCaps")) is None: + self.result.is_failure(f"{peer} - Multiprotocol capabilities not found") + continue # If strict is True, check if only the specified capabilities are configured if peer.strict and sorted(peer.capabilities) != sorted(act_mp_caps): @@ -647,7 +697,7 @@ class VerifyBGPPeerMPCaps(AntaTest): class VerifyBGPPeerASNCap(AntaTest): - """Verifies the four octet ASN capability of BGP IPv4 peer(s). + """Verifies the four octet ASN capability of BGP peers. This test performs the following checks for each specified peer: @@ -675,6 +725,11 @@ class VerifyBGPPeerASNCap(AntaTest): bgp_peers: - peer_address: 172.30.11.1 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT ``` """ @@ -685,7 +740,7 @@ class VerifyBGPPeerASNCap(AntaTest): """Input model for the VerifyBGPPeerASNCap test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -696,11 +751,8 @@ class VerifyBGPPeerASNCap(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -715,7 +767,7 @@ class VerifyBGPPeerASNCap(AntaTest): class VerifyBGPPeerRouteRefreshCap(AntaTest): - """Verifies the route refresh capabilities of IPv4 BGP peer(s) in a specified VRF. + """Verifies the route refresh capabilities of BGP peers. This test performs the following checks for each specified peer: @@ -743,6 +795,11 @@ class VerifyBGPPeerRouteRefreshCap(AntaTest): bgp_peers: - peer_address: 172.30.11.1 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT ``` """ @@ -753,7 +810,7 @@ class VerifyBGPPeerRouteRefreshCap(AntaTest): """Input model for the VerifyBGPPeerRouteRefreshCap test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -764,11 +821,8 @@ class VerifyBGPPeerRouteRefreshCap(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -783,7 +837,7 @@ class VerifyBGPPeerRouteRefreshCap(AntaTest): class VerifyBGPPeerMD5Auth(AntaTest): - """Verifies the MD5 authentication and state of IPv4 BGP peer(s) in a specified VRF. + """Verifies the MD5 authentication and state of BGP peers. This test performs the following checks for each specified peer: @@ -813,6 +867,11 @@ class VerifyBGPPeerMD5Auth(AntaTest): vrf: default - peer_address: 172.30.11.5 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default ``` """ @@ -823,7 +882,7 @@ class VerifyBGPPeerMD5Auth(AntaTest): """Input model for the VerifyBGPPeerMD5Auth test.""" bgp_peers: list[BgpPeer] - """List of IPv4 BGP peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -834,11 +893,8 @@ class VerifyBGPPeerMD5Auth(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -921,24 +977,21 @@ class VerifyEVPNType2Route(AntaTest): class VerifyBGPAdvCommunities(AntaTest): - """Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s). + """Verifies the advertised communities of BGP peers. This test performs the following checks for each specified peer: 1. Verifies that the peer is found in its VRF in the BGP configuration. - 2. Validates that all required community types are advertised: - - Standard communities - - Extended communities - - Large communities + 2. Validates that given community types are advertised. If not provided, validates that all communities (standard, extended, large) are advertised. Expected Results ---------------- * Success: If all of the following conditions are met: - All specified peers are found in the BGP configuration. - - Each peer advertises standard, extended and large communities. + - Each peer advertises the given community types. * Failure: If any of the following occur: - A specified peer is not found in the BGP configuration. - - A peer does not advertise standard, extended or large communities. + - A peer does not advertise any of the given community types. Examples -------- @@ -950,7 +1003,14 @@ class VerifyBGPAdvCommunities(AntaTest): - peer_address: 172.30.11.17 vrf: default - peer_address: 172.30.11.21 + vrf: MGMT + advertised_communities: ["standard", "extended"] + - peer_address: fd00:dc:1::1 vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default + advertised_communities: ["standard", "extended"] ``` """ @@ -961,7 +1021,7 @@ class VerifyBGPAdvCommunities(AntaTest): """Input model for the VerifyBGPAdvCommunities test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -972,21 +1032,18 @@ class VerifyBGPAdvCommunities(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue # Check BGP peer advertised communities - if not all(get_value(peer_data, f"advertisedCommunities.{community}") is True for community in ["standard", "extended", "large"]): + if not all(get_value(peer_data, f"advertisedCommunities.{community}") is True for community in peer.advertised_communities): self.result.is_failure(f"{peer} - {format_data(peer_data['advertisedCommunities'])}") class VerifyBGPTimers(AntaTest): - """Verifies the timers of BGP IPv4 peer(s). + """Verifies the timers of BGP peers. This test performs the following checks for each specified peer: @@ -1017,6 +1074,15 @@ class VerifyBGPTimers(AntaTest): vrf: default hold_time: 180 keep_alive_time: 60 + - peer_address: fd00:dc:1::1 + vrf: default + hold_time: 180 + keep_alive_time: 60 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + hold_time: 180 + keep_alive_time: 60 ``` """ @@ -1027,7 +1093,7 @@ class VerifyBGPTimers(AntaTest): """Input model for the VerifyBGPTimers test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @field_validator("bgp_peers") @@ -1048,11 +1114,8 @@ class VerifyBGPTimers(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1064,7 +1127,7 @@ class VerifyBGPTimers(AntaTest): class VerifyBGPPeerDropStats(AntaTest): - """Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). + """Verifies BGP NLRI drop statistics of BGP peers. This test performs the following checks for each specified peer: @@ -1096,6 +1159,17 @@ class VerifyBGPPeerDropStats(AntaTest): drop_stats: - inDropAsloop - prefixEvpnDroppedUnsupportedRouteType + - peer_address: fd00:dc:1::1 + vrf: default + drop_stats: + - inDropAsloop + - prefixEvpnDroppedUnsupportedRouteType + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + drop_stats: + - inDropAsloop + - prefixEvpnDroppedUnsupportedRouteType ``` """ @@ -1106,7 +1180,7 @@ class VerifyBGPPeerDropStats(AntaTest): """Input model for the VerifyBGPPeerDropStats test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -1117,12 +1191,9 @@ class VerifyBGPPeerDropStats(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) drop_stats_input = peer.drop_stats - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1140,7 +1211,7 @@ class VerifyBGPPeerDropStats(AntaTest): class VerifyBGPPeerUpdateErrors(AntaTest): - """Verifies BGP update error counters for the provided BGP IPv4 peer(s). + """Verifies BGP update error counters of BGP peers. This test performs the following checks for each specified peer: @@ -1173,6 +1244,15 @@ class VerifyBGPPeerUpdateErrors(AntaTest): vrf: default update_errors: - inUpdErrWithdraw + - peer_address: fd00:dc:1::1 + vrf: default + update_errors: + - inUpdErrWithdraw + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + update_errors: + - inUpdErrWithdraw ``` """ @@ -1183,7 +1263,7 @@ class VerifyBGPPeerUpdateErrors(AntaTest): """Input model for the VerifyBGPPeerUpdateErrors test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @AntaTest.anta_test @@ -1194,12 +1274,9 @@ class VerifyBGPPeerUpdateErrors(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) update_errors_input = peer.update_errors - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1217,7 +1294,7 @@ class VerifyBGPPeerUpdateErrors(AntaTest): class VerifyBgpRouteMaps(AntaTest): - """Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). + """Verifies BGP inbound and outbound route-maps of BGP peers. This test performs the following checks for each specified peer: @@ -1244,6 +1321,15 @@ class VerifyBgpRouteMaps(AntaTest): vrf: default inbound_route_map: RM-MLAG-PEER-IN outbound_route_map: RM-MLAG-PEER-OUT + - peer_address: fd00:dc:1::1 + vrf: default + inbound_route_map: RM-MLAG-PEER-IN + outbound_route_map: RM-MLAG-PEER-OUT + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + inbound_route_map: RM-MLAG-PEER-IN + outbound_route_map: RM-MLAG-PEER-OUT ``` """ @@ -1254,7 +1340,7 @@ class VerifyBgpRouteMaps(AntaTest): """Input model for the VerifyBgpRouteMaps test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @field_validator("bgp_peers") @@ -1275,13 +1361,11 @@ class VerifyBgpRouteMaps(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) inbound_route_map = peer.inbound_route_map outbound_route_map = peer.outbound_route_map - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1295,7 +1379,7 @@ class VerifyBgpRouteMaps(AntaTest): class VerifyBGPPeerRouteLimit(AntaTest): - """Verifies maximum routes and warning limit for BGP IPv4 peer(s). + """Verifies maximum routes and warning limit for BGP peers. This test performs the following checks for each specified peer: @@ -1322,6 +1406,15 @@ class VerifyBGPPeerRouteLimit(AntaTest): vrf: default maximum_routes: 12000 warning_limit: 10000 + - peer_address: fd00:dc:1::1 + vrf: default + maximum_routes: 12000 + warning_limit: 10000 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + maximum_routes: 12000 + warning_limit: 10000 ``` """ @@ -1332,7 +1425,7 @@ class VerifyBGPPeerRouteLimit(AntaTest): """Input model for the VerifyBGPPeerRouteLimit test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" BgpPeer: ClassVar[type[BgpPeer]] = BgpPeer @field_validator("bgp_peers") @@ -1353,13 +1446,11 @@ class VerifyBGPPeerRouteLimit(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) maximum_routes = peer.maximum_routes warning_limit = peer.warning_limit - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1373,7 +1464,7 @@ class VerifyBGPPeerRouteLimit(AntaTest): class VerifyBGPPeerGroup(AntaTest): - """Verifies BGP peer group of BGP IPv4 peer(s). + """Verifies BGP peer group of BGP peers. This test performs the following checks for each specified peer: @@ -1399,6 +1490,13 @@ class VerifyBGPPeerGroup(AntaTest): - peer_address: 172.30.11.1 vrf: default peer_group: IPv4-UNDERLAY-PEERS + - peer_address: fd00:dc:1::1 + vrf: default + peer_group: IPv4-UNDERLAY-PEERS + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + peer_group: IPv4-UNDERLAY-PEERS ``` """ @@ -1409,7 +1507,7 @@ class VerifyBGPPeerGroup(AntaTest): """Input model for the VerifyBGPPeerGroup test.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" @field_validator("bgp_peers") @classmethod @@ -1429,11 +1527,8 @@ class VerifyBGPPeerGroup(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1442,7 +1537,7 @@ class VerifyBGPPeerGroup(AntaTest): class VerifyBGPPeerSessionRibd(AntaTest): - """Verifies the session state of BGP IPv4 peer(s). + """Verifies the session state of BGP peers. Compatible with EOS operating in `ribd` routing protocol model. @@ -1451,7 +1546,7 @@ class VerifyBGPPeerSessionRibd(AntaTest): 1. Verifies that the peer is found in its VRF in the BGP configuration. 2. Verifies that the BGP session is `Established` and, if specified, has remained established for at least the duration given by `minimum_established_time`. 3. Ensures that both input and output TCP message queues are empty. - Can be disabled by setting `check_tcp_queues` global flag to `False`. + Can be disabled by setting `check_tcp_queues` input flag to `False`. Expected Results ---------------- @@ -1477,12 +1572,13 @@ class VerifyBGPPeerSessionRibd(AntaTest): bgp_peers: - peer_address: 10.1.0.1 vrf: default - - peer_address: 10.1.0.2 - vrf: default - - peer_address: 10.1.255.2 - vrf: DEV - peer_address: 10.1.255.4 vrf: DEV + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT ``` """ @@ -1497,7 +1593,7 @@ class VerifyBGPPeerSessionRibd(AntaTest): check_tcp_queues: bool = True """Flag to check if the TCP session queues are empty for all BGP peers. Defaults to `True`.""" bgp_peers: list[BgpPeer] - """List of BGP IPv4 peers.""" + """List of BGP peers.""" @AntaTest.anta_test def test(self) -> None: @@ -1507,11 +1603,8 @@ class VerifyBGPPeerSessionRibd(AntaTest): output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_address = str(peer.peer_address) - peers = get_value(output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_data := get_item(peers, "peerAddress", peer_address)) is None: + if (peer_data := _get_bgp_peer_data(peer, output)) is None: self.result.is_failure(f"{peer} - Not found") continue @@ -1534,7 +1627,7 @@ class VerifyBGPPeerSessionRibd(AntaTest): class VerifyBGPPeersHealthRibd(AntaTest): - """Verifies the health of all the BGP IPv4 peer(s). + """Verifies the health of all the BGP peers. Compatible with EOS operating in `ribd` routing protocol model. @@ -1542,7 +1635,7 @@ class VerifyBGPPeersHealthRibd(AntaTest): 1. Verifies that the BGP session is in the `Established` state. 2. Checks that both input and output TCP message queues are empty. - Can be disabled by setting `check_tcp_queues` global flag to `False`. + Can be disabled by setting `check_tcp_queues` input flag to `False`. Expected Results ---------------- @@ -1594,7 +1687,7 @@ class VerifyBGPPeersHealthRibd(AntaTest): class VerifyBGPNlriAcceptance(AntaTest): - """Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP IPv4 peer(s). + """Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers. This test performs the following checks for each specified peer: @@ -1619,11 +1712,27 @@ class VerifyBGPNlriAcceptance(AntaTest): vrf: default capabilities: - ipv4Unicast + - peer_address: 2001:db8:1::2 + vrf: default + capabilities: + - ipv6Unicast + - peer_address: fe80::2%Et1 + vrf: default + capabilities: + - ipv6Unicast + # RFC 5549 + - peer_address: fe80::2%Et1 + vrf: default + capabilities: + - ipv6Unicast ``` """ categories: ClassVar[list[str]] = ["bgp"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bgp summary vrf all", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [ + AntaCommand(command="show bgp summary vrf all", revision=1), + AntaCommand(command="show bgp neighbors vrf all", revision=3), + ] class Input(AntaTest.Input): """Input model for the VerifyBGPNlriAcceptance test.""" @@ -1641,16 +1750,50 @@ class VerifyBGPNlriAcceptance(AntaTest): raise ValueError(msg) return bgp_peers + @staticmethod + def _get_peer_address(peer: BgpPeer, command_output: dict[str, Any]) -> str | None: + """Retrieve the peer address for the given BGP peer data. + + If an interface is specified, the address is extracted from the command output; + otherwise, it is retrieved directly from the peer object. + + Parameters + ---------- + peer + The BGP peer object to look up. + command_output + Parsed output from the relevant command. + + Returns + ------- + str | None + The peer address if found, otherwise None. + """ + if peer.interface is not None: + # RFC5549 + interface = str(peer.interface) + lookup_key = "ifName" + + peer_list = get_value(command_output, f"vrfs.{peer.vrf}.peerList", default=[]) + # Check if the peer is found + if (peer_details := get_item(peer_list, lookup_key, interface)) is not None: + return str(peer_details.get("peerAddress")) + return None + + return str(peer.peer_address) + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyBGPNlriAcceptance.""" self.result.is_success() output = self.instance_commands[0].json_output + peer_output = self.instance_commands[1].json_output for peer in self.inputs.bgp_peers: + identity = self._get_peer_address(peer, peer_output) # Check if the peer is found - if not (peer_data := get_value(output, f"vrfs..{peer.vrf}..peers..{peer.peer_address}", separator="..")): + if not (peer_data := get_value(output, f"vrfs..{peer.vrf}..peers..{identity}", separator="..")): self.result.is_failure(f"{peer} - Not found") continue @@ -1931,7 +2074,7 @@ class VerifyBGPRedistribution(AntaTest): class VerifyBGPPeerTtlMultiHops(AntaTest): - """Verifies BGP TTL and max-ttl-hops count for BGP IPv4 peer(s). + """Verifies BGP TTL and max-ttl-hops count for BGP peers. This test performs the following checks for each specified BGP peer: @@ -1960,6 +2103,15 @@ class VerifyBGPPeerTtlMultiHops(AntaTest): vrf: test ttl: 30 max_ttl_hops: 30 + - peer_address: fd00:dc:1::1 + vrf: default + ttl: 30 + max_ttl_hops: 30 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + ttl: 30 + max_ttl_hops: 30 ``` """ @@ -1970,7 +2122,7 @@ class VerifyBGPPeerTtlMultiHops(AntaTest): """Input model for the VerifyBGPPeerTtlMultiHops test.""" bgp_peers: list[BgpPeer] - """List of IPv4 peer(s).""" + """List of peer(s).""" @field_validator("bgp_peers") @classmethod @@ -1993,11 +2145,8 @@ class VerifyBGPPeerTtlMultiHops(AntaTest): command_output = self.instance_commands[0].json_output for peer in self.inputs.bgp_peers: - peer_ip = str(peer.peer_address) - peer_list = get_value(command_output, f"vrfs.{peer.vrf}.peerList", default=[]) - # Check if the peer is found - if (peer_details := get_item(peer_list, "peerAddress", peer_ip)) is None: + if (peer_details := _get_bgp_peer_data(peer, command_output)) is None: self.result.is_failure(f"{peer} - Not found") continue diff --git a/anta/tests/routing/generic.py b/anta/tests/routing/generic.py index 066d39c..fe8e047 100644 --- a/anta/tests/routing/generic.py +++ b/anta/tests/routing/generic.py @@ -9,7 +9,7 @@ from __future__ import annotations from functools import cache from ipaddress import IPv4Address, IPv4Interface -from typing import TYPE_CHECKING, ClassVar, Literal +from typing import TYPE_CHECKING, Any, ClassVar, Literal from pydantic import field_validator, model_validator @@ -349,3 +349,58 @@ class VerifyIPv4RouteNextHops(AntaTest): for nexthop in entry.nexthops: if not get_item(route_data["vias"], "nexthopAddr", str(nexthop)): self.result.is_failure(f"{entry} Nexthop: {nexthop} - Route not found") + + +class VerifyRoutingStatus(AntaTest): + """Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549). + + Expected Results + ---------------- + * Success: The test will pass if the routing status is correct. + * Failure: The test will fail if the routing status doesn't match the expected configuration. + + Examples + -------- + ```yaml + anta.tests.routing: + generic: + - VerifyRoutingStatus: + ipv4_unicast: True + ipv6_unicast: True + ``` + """ + + categories: ClassVar[list[str]] = ["routing"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyRoutingStatus test.""" + + ipv4_unicast: bool = False + """IPv4 unicast routing status.""" + ipv6_unicast: bool = False + """IPv6 unicast routing status.""" + ipv4_multicast: bool = False + """IPv4 multicast routing status.""" + ipv6_multicast: bool = False + """IPv6 multicast routing status.""" + ipv6_interfaces: bool = False + """IPv6 interface forwarding status.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyRoutingStatus.""" + self.result.is_success() + command_output = self.instance_commands[0].json_output + actual_routing_status: dict[str, Any] = { + "ipv4_unicast": command_output["v4RoutingEnabled"], + "ipv6_unicast": command_output["v6RoutingEnabled"], + "ipv4_multicast": command_output["multicastRouting"]["ipMulticastEnabled"], + "ipv6_multicast": command_output["multicastRouting"]["ip6MulticastEnabled"], + "ipv6_interfaces": command_output.get("v6IntfForwarding", False), + } + + for input_key, value in self.inputs: + if input_key in actual_routing_status and value != actual_routing_status[input_key]: + route_type = " ".join([{"ipv4": "IPv4", "ipv6": "IPv6"}.get(part, part) for part in input_key.split("_")]) + self.result.is_failure(f"{route_type} routing enabled status mismatch - Expected: {value} Actual: {actual_routing_status[input_key]}") diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 6a73e40..cd9dbb6 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -442,3 +442,78 @@ class VerifyISISSegmentRoutingTunnels(AntaTest): and (via_input.interface is None or via_input.interface == eos_via.get("interface")) and (via_input.tunnel_id is None or via_input.tunnel_id.upper() == get_value(eos_via, "tunnelId.type", default="").upper()) ) + + +class VerifyISISGracefulRestart(AntaTest): + """Verifies the IS-IS graceful restart feature. + + This test performs the following checks for each IS-IS instance: + + 1. Verifies that the specified IS-IS instance is configured on the device. + 2. Verifies the statuses of the graceful restart and graceful restart helper functionalities. + + Expected Results + ---------------- + * Success: The test will pass if all of the following conditions are met: + - The specified IS-IS instance is configured on the device. + - Expected and actual IS-IS graceful restart and graceful restart helper values match. + * Failure: The test will fail if any of the following conditions is met: + - The specified IS-IS instance is not configured on the device. + - Expected and actual IS-IS graceful restart and graceful restart helper values do not match. + * Skipped: The test will skip if IS-IS is not configured on the device. + + Examples + -------- + ```yaml + anta.tests.routing: + isis: + - VerifyISISGracefulRestart: + instances: + - name: '1' + vrf: default + graceful_restart: True + graceful_restart_helper: False + - name: '2' + vrf: default + - name: '11' + vrf: test + graceful_restart: True + - name: '12' + vrf: test + graceful_restart_helper: False + ``` + """ + + categories: ClassVar[list[str]] = ["isis"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis graceful-restart vrf all", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyISISGracefulRestart test.""" + + instances: list[ISISInstance] + """List of IS-IS instance entries.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyISISGracefulRestart.""" + self.result.is_success() + + # Verify if IS-IS is configured + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("IS-IS not configured") + return + + # If IS-IS instance is not found or GR and GR helpers are not matching with the expected values, test fails. + for instance in self.inputs.instances: + graceful_restart = "enabled" if instance.graceful_restart else "disabled" + graceful_restart_helper = "enabled" if instance.graceful_restart_helper else "disabled" + + if (instance_details := get_value(command_output, f"{instance.vrf}..isisInstances..{instance.name}", separator="..")) is None: + self.result.is_failure(f"{instance} - Not configured") + continue + + if (act_state := instance_details.get("gracefulRestart")) != graceful_restart: + self.result.is_failure(f"{instance} - Incorrect graceful restart state - Expected: {graceful_restart} Actual: {act_state}") + + if (act_helper_state := instance_details.get("gracefulRestartHelper")) != graceful_restart_helper: + self.result.is_failure(f"{instance} - Incorrect graceful restart helper state - Expected: {graceful_restart_helper} Actual: {act_helper_state}") diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 1d02252..9a9acde 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -320,7 +320,7 @@ class VerifySnmpErrorCounters(AntaTest): # Verify SNMP PDU counters. if not (snmp_counters := get_value(command_output, "counters")): - self.result.is_failure("SNMP counters not found.") + self.result.is_failure("SNMP counters not found") return # In case SNMP error counters not provided, It will check all the error counters. diff --git a/anta/tests/stp.py b/anta/tests/stp.py index 47dfb9f..6b3d440 100644 --- a/anta/tests/stp.py +++ b/anta/tests/stp.py @@ -11,7 +11,7 @@ from typing import ClassVar, Literal from pydantic import Field -from anta.custom_types import Vlan +from anta.custom_types import VlanId from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -44,7 +44,7 @@ class VerifySTPMode(AntaTest): mode: Literal["mstp", "rstp", "rapidPvst"] = "mstp" """STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp.""" - vlans: list[Vlan] + vlans: list[VlanId] """List of VLAN on which to verify STP mode.""" def render(self, template: AntaTemplate) -> list[AntaCommand]: @@ -157,7 +157,7 @@ class VerifySTPForwardingPorts(AntaTest): class Input(AntaTest.Input): """Input model for the VerifySTPForwardingPorts test.""" - vlans: list[Vlan] + vlans: list[VlanId] """List of VLAN on which to verify forwarding states.""" def render(self, template: AntaTemplate) -> list[AntaCommand]: @@ -213,7 +213,7 @@ class VerifySTPRootPriority(AntaTest): priority: int """STP root priority to verify.""" - instances: list[Vlan] = Field(default=[]) + instances: list[VlanId] = Field(default=[]) """List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified.""" @AntaTest.anta_test @@ -299,9 +299,9 @@ class VerifySTPDisabledVlans(AntaTest): This test performs the following checks: - 1. Verifies that the STP is configured. - 2. Verifies that the specified VLAN(s) exist on the device. - 3. Verifies that the STP is disabled for the specified VLAN(s). + 1. Verifies that the STP is configured. + 2. Verifies that the specified VLAN(s) exist on the device. + 3. Verifies that the STP is disabled for the specified VLAN(s). Expected Results ---------------- @@ -331,7 +331,7 @@ class VerifySTPDisabledVlans(AntaTest): class Input(AntaTest.Input): """Input model for the VerifySTPDisabledVlans test.""" - vlans: list[Vlan] + vlans: list[VlanId] """List of STP disabled VLAN(s).""" @AntaTest.anta_test diff --git a/anta/tests/system.py b/anta/tests/system.py index 048f987..e37aa7e 100644 --- a/anta/tests/system.py +++ b/anta/tests/system.py @@ -10,9 +10,9 @@ from __future__ import annotations import re from typing import TYPE_CHECKING, Any, ClassVar -from pydantic import model_validator +from pydantic import Field, model_validator -from anta.custom_types import Hostname, PositiveInteger +from anta.custom_types import Hostname, PositiveInteger, ReloadCause from anta.input_models.system import NTPPool, NTPServer from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -73,8 +73,8 @@ class VerifyReloadCause(AntaTest): Expected Results ---------------- - * Success: The test will pass if there are NO reload causes or if the last reload was caused by the user or after an FPGA upgrade. - * Failure: The test will fail if the last reload was NOT caused by the user or after an FPGA upgrade. + * Success: The test passes if there is no reload cause, or if the last reload cause was one of the provided inputs. + * Failure: The test will fail if the last reload cause was NOT one of the provided inputs. * Error: The test will report an error if the reload cause is NOT available. Examples @@ -82,12 +82,22 @@ class VerifyReloadCause(AntaTest): ```yaml anta.tests.system: - VerifyReloadCause: + allowed_causes: + - USER + - FPGA + - ZTP ``` """ categories: ClassVar[list[str]] = ["system"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show reload cause", revision=1)] + class Input(AntaTest.Input): + """Input model for the VerifyReloadCause test.""" + + allowed_causes: list[ReloadCause] = Field(default=["USER", "FPGA"], validate_default=True) + """A list of allowed system reload causes.""" + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyReloadCause.""" @@ -96,15 +106,14 @@ class VerifyReloadCause(AntaTest): # No reload causes self.result.is_success() return + reset_causes = command_output["resetCauses"] command_output_data = reset_causes[0].get("description") - if command_output_data in [ - "Reload requested by the user.", - "Reload requested after FPGA upgrade", - ]: + if command_output_data in self.inputs.allowed_causes: self.result.is_success() else: - self.result.is_failure(f"Reload cause is: {command_output_data}") + causes = ", ".join(f"'{c}'" for c in self.inputs.allowed_causes) + self.result.is_failure(f"Invalid reload cause - Expected: {causes} Actual: '{command_output_data}'") class VerifyCoredump(AntaTest): @@ -451,7 +460,7 @@ class VerifyMaintenance(AntaTest): ``` """ - categories: ClassVar[list[str]] = ["Maintenance"] + categories: ClassVar[list[str]] = ["system"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show maintenance", revision=1)] @AntaTest.anta_test @@ -476,8 +485,8 @@ class VerifyMaintenance(AntaTest): # Building the error message. if units_under_maintenance: - self.result.is_failure(f"Units under maintenance: '{', '.join(units_under_maintenance)}'.") + self.result.is_failure(f"Units under maintenance: '{', '.join(units_under_maintenance)}'") if units_entering_maintenance: - self.result.is_failure(f"Units entering maintenance: '{', '.join(units_entering_maintenance)}'.") + self.result.is_failure(f"Units entering maintenance: '{', '.join(units_entering_maintenance)}'") if causes: - self.result.is_failure(f"Possible causes: '{', '.join(sorted(causes))}'.") + self.result.is_failure(f"Possible causes: '{', '.join(sorted(causes))}'") diff --git a/anta/tests/vlan.py b/anta/tests/vlan.py index 25fc9d5..461a215 100644 --- a/anta/tests/vlan.py +++ b/anta/tests/vlan.py @@ -9,7 +9,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, ClassVar, Literal -from anta.custom_types import DynamicVlanSource, Vlan +from anta.custom_types import DynamicVlanSource, VlanId +from anta.input_models.vlan import Vlan from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -47,9 +48,9 @@ class VerifyVlanInternalPolicy(AntaTest): policy: Literal["ascending", "descending"] """The VLAN internal allocation policy. Supported values: ascending, descending.""" - start_vlan_id: Vlan + start_vlan_id: VlanId """The starting VLAN ID in the range.""" - end_vlan_id: Vlan + end_vlan_id: VlanId """The ending VLAN ID in the range.""" @AntaTest.anta_test @@ -145,3 +146,48 @@ class VerifyDynamicVlanSource(AntaTest): unexpected_sources = sources_with_vlans - expected_sources if unexpected_sources: self.result.is_failure(f"Strict mode enabled: Unexpected sources have VLANs allocated: {', '.join(sorted(unexpected_sources))}") + + +class VerifyVlanStatus(AntaTest): + """Verifies the administrative status of specified VLANs. + + Expected Results + ---------------- + * Success: The test will pass if all specified VLANs exist in the configuration and their administrative status is correct. + * Failure: The test will fail if any of the specified VLANs is not found in the configuration or if its administrative status is incorrect. + + Examples + -------- + ```yaml + anta.tests.vlan: + - VerifyVlanStatus: + vlans: + - vlan_id: 10 + status: suspended + - vlan_id: 4094 + status: active + ``` + """ + + categories: ClassVar[list[str]] = ["vlan"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyVlanStatus test.""" + + vlans: list[Vlan] + """List of VLAN details.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyVlanStatus.""" + self.result.is_success() + command_output = self.instance_commands[0].json_output + + for vlan in self.inputs.vlans: + if (vlan_detail := get_value(command_output, f"vlans.{vlan.vlan_id}")) is None: + self.result.is_failure(f"{vlan} - Not configured") + continue + + if (act_status := vlan_detail["status"]) != vlan.status: + self.result.is_failure(f"{vlan} - Incorrect administrative status - Expected: {vlan.status} Actual: {act_status}") diff --git a/anta/tests/vxlan.py b/anta/tests/vxlan.py index 04c3994..8c04d64 100644 --- a/anta/tests/vxlan.py +++ b/anta/tests/vxlan.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, ClassVar from pydantic import Field -from anta.custom_types import Vlan, Vni, VxlanSrcIntf +from anta.custom_types import VlanId, Vni, VxlanSrcIntf from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -102,11 +102,11 @@ class VerifyVxlanConfigSanity(AntaTest): class VerifyVxlanVniBinding(AntaTest): - """Verifies the VNI-VLAN bindings of the Vxlan1 interface. + """Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface. Expected Results ---------------- - * Success: The test will pass if the VNI-VLAN bindings provided are properly configured. + * Success: The test will pass if the VNI-VLAN and VNI-VRF bindings provided are properly configured. * Failure: The test will fail if any VNI lacks bindings or if any bindings are incorrect. * Skipped: The test will be skipped if the Vxlan1 interface is not configured. @@ -118,6 +118,7 @@ class VerifyVxlanVniBinding(AntaTest): bindings: 10010: 10 10020: 20 + 500: PROD ``` """ @@ -127,8 +128,8 @@ class VerifyVxlanVniBinding(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyVxlanVniBinding test.""" - bindings: dict[Vni, Vlan] - """VNI to VLAN bindings to verify.""" + bindings: dict[Vni, VlanId | str] + """VNI-VLAN or VNI-VRF bindings to verify.""" @AntaTest.anta_test def test(self) -> None: @@ -136,26 +137,32 @@ class VerifyVxlanVniBinding(AntaTest): self.result.is_success() if (vxlan1 := get_value(self.instance_commands[0].json_output, "vxlanIntfs.Vxlan1")) is None: - self.result.is_skipped("Vxlan1 interface is not configured") + self.result.is_skipped("Interface: Vxlan1 - Not configured") return - for vni, vlan in self.inputs.bindings.items(): + for vni, vlan_vrf in self.inputs.bindings.items(): str_vni = str(vni) retrieved_vlan = "" - if str_vni in vxlan1["vniBindings"]: + retrieved_vrf = "" + if all([str_vni in vxlan1["vniBindings"], isinstance(vlan_vrf, int)]): retrieved_vlan = get_value(vxlan1, f"vniBindings..{str_vni}..vlan", separator="..") elif str_vni in vxlan1["vniBindingsToVrf"]: - retrieved_vlan = get_value(vxlan1, f"vniBindingsToVrf..{str_vni}..vlan", separator="..") - - if not retrieved_vlan: + if isinstance(vlan_vrf, int): + retrieved_vlan = get_value(vxlan1, f"vniBindingsToVrf..{str_vni}..vlan", separator="..") + else: + retrieved_vrf = get_value(vxlan1, f"vniBindingsToVrf..{str_vni}..vrfName", separator="..") + if not any([retrieved_vlan, retrieved_vrf]): self.result.is_failure(f"Interface: Vxlan1 VNI: {str_vni} - Binding not found") - elif vlan != retrieved_vlan: - self.result.is_failure(f"Interface: Vxlan1 VNI: {str_vni} VLAN: {vlan} - Wrong VLAN binding - Actual: {retrieved_vlan}") + elif retrieved_vlan and vlan_vrf != retrieved_vlan: + self.result.is_failure(f"Interface: Vxlan1 VNI: {str_vni} - Wrong VLAN binding - Expected: {vlan_vrf} Actual: {retrieved_vlan}") + + elif retrieved_vrf and vlan_vrf != retrieved_vrf: + self.result.is_failure(f"Interface: Vxlan1 VNI: {str_vni} - Wrong VRF binding - Expected: {vlan_vrf} Actual: {retrieved_vrf}") class VerifyVxlanVtep(AntaTest): - """Verifies the VTEP peers of the Vxlan1 interface. + """Verifies Vxlan1 VTEP peers. Expected Results ---------------- @@ -191,7 +198,7 @@ class VerifyVxlanVtep(AntaTest): inputs_vteps = [str(input_vtep) for input_vtep in self.inputs.vteps] if (vxlan1 := get_value(self.instance_commands[0].json_output, "interfaces.Vxlan1")) is None: - self.result.is_skipped("Vxlan1 interface is not configured") + self.result.is_skipped("Interface: Vxlan1 - Not configured") return difference1 = set(inputs_vteps).difference(set(vxlan1["vteps"])) @@ -205,7 +212,7 @@ class VerifyVxlanVtep(AntaTest): class VerifyVxlan1ConnSettings(AntaTest): - """Verifies the interface vxlan1 source interface and UDP port. + """Verifies Vxlan1 source interface and UDP port. Expected Results ---------------- @@ -243,7 +250,7 @@ class VerifyVxlan1ConnSettings(AntaTest): # Skip the test case if vxlan1 interface is not configured vxlan_output = get_value(command_output, "interfaces.Vxlan1") if not vxlan_output: - self.result.is_skipped("Vxlan1 interface is not configured.") + self.result.is_skipped("Interface: Vxlan1 - Not configured") return src_intf = vxlan_output.get("srcIpIntf") diff --git a/anta/tools.py b/anta/tools.py index cbcfe0b..45efb13 100644 --- a/anta/tools.py +++ b/anta/tools.py @@ -290,7 +290,7 @@ class Catchtime: """__enter__ method.""" self.start = perf_counter() if self.logger and self.message: - self.logger.info("%s ...", self.message) + self.logger.debug("%s ...", self.message) return self def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: @@ -298,7 +298,7 @@ class Catchtime: self.raw_time = perf_counter() - self.start self.time = format_td(self.raw_time, 3) if self.logger and self.message: - self.logger.info("%s completed in: %s.", self.message, self.time) + self.logger.debug("%s completed in: %s.", self.message, self.time) def cprofile(sort_by: str = "cumtime") -> Callable[[F], F]: diff --git a/asynceapi/_models.py b/asynceapi/_models.py index 0572a2f..21ad661 100644 --- a/asynceapi/_models.py +++ b/asynceapi/_models.py @@ -21,7 +21,6 @@ if TYPE_CHECKING: LOGGER = getLogger(__name__) -# pylint: disable=too-many-instance-attributes @dataclass(frozen=True) class EapiRequest: """Model for an eAPI request. diff --git a/asynceapi/device.py b/asynceapi/device.py index a7702da..504d072 100644 --- a/asynceapi/device.py +++ b/asynceapi/device.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Literal, overload # Public Imports # ----------------------------------------------------------------------------- import httpx +from typing_extensions import deprecated # ----------------------------------------------------------------------------- # Private Imports @@ -51,6 +52,7 @@ class Device(httpx.AsyncClient): """ auth = None + EAPI_COMMAND_API_URL = "/command-api" EAPI_OFMT_OPTIONS = ("json", "text") EAPI_DEFAULT_OFMT = "json" @@ -109,6 +111,7 @@ class Device(httpx.AsyncClient): super().__init__(**kwargs) self.headers["Content-Type"] = "application/json-rpc" + @deprecated("This method is deprecated, use `Device.check_api_endpoint` method instead. This will be removed in ANTA v2.0.0.", category=DeprecationWarning) async def check_connection(self) -> bool: """Check the target device to ensure that the eAPI port is open and accepting connections. @@ -122,6 +125,22 @@ class Device(httpx.AsyncClient): """ return await port_check_url(self.base_url) + async def check_api_endpoint(self) -> bool: + """Check the target device eAPI HTTP endpoint with a HEAD request. + + It is recommended that a Caller checks the connection before involving cli commands, + but this step is not required. + + Returns + ------- + bool + True when the device eAPI HTTP endpoint is accessible (2xx status code), + otherwise an HTTPStatusError exception is raised. + """ + response = await self.head(self.EAPI_COMMAND_API_URL, timeout=5) + response.raise_for_status() + return True + # Single command, JSON output, no suppression @overload async def cli( @@ -416,7 +435,7 @@ class Device(httpx.AsyncClient): The list of command results; either dict or text depending on the JSON-RPC format parameter. """ - res = await self.post("/command-api", json=jsonrpc) + res = await self.post(self.EAPI_COMMAND_API_URL, json=jsonrpc) res.raise_for_status() body = res.json() diff --git a/docs/advanced_usages/as-python-lib.md b/docs/advanced_usages/as-python-lib.md index 1a7dedb..271c4d8 100644 --- a/docs/advanced_usages/as-python-lib.md +++ b/docs/advanced_usages/as-python-lib.md @@ -7,7 +7,7 @@ ANTA is a Python library that can be used in user applications. This section describes how you can leverage ANTA Python modules to help you create your own NRFU solution. > [!TIP] -> If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html +> If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - ## [AntaDevice](../api/device.md#anta.device.AntaDevice) Abstract Class @@ -24,7 +24,7 @@ The [copy()](../api/device.md#anta.device.AntaDevice.copy) coroutine is used to The [AsyncEOSDevice](../api/device.md#anta.device.AsyncEOSDevice) class is an implementation of [AntaDevice](../api/device.md#anta.device.AntaDevice) for Arista EOS. It uses the [aio-eapi](https://github.com/jeremyschulman/aio-eapi) eAPI client and the [AsyncSSH](https://github.com/ronf/asyncssh) library. -- The [\_collect()](../api/device.md#anta.device.AsyncEOSDevice._collect) coroutine collects [AntaCommand](../api/models.md#anta.models.AntaCommand) outputs using eAPI. +- The [\_collect()](../api/device.md#anta.device.AsyncEOSDevice._collect) coroutine collects [AntaCommand](../api/commands.md#anta.models.AntaCommand) outputs using eAPI. - The [refresh()](../api/device.md#anta.device.AsyncEOSDevice.refresh) coroutine tries to open a TCP connection on the eAPI port and update the `is_online` attribute accordingly. If the TCP connection succeeds, it sends a `show version` command to gather the hardware model of the device and updates the `established` and `hw_model` attributes. - The [copy()](../api/device.md#anta.device.AsyncEOSDevice.copy) coroutine copies files to and from the device using the SCP protocol. diff --git a/docs/api/settings.md b/docs/api/settings.md new file mode 100644 index 0000000..bbc45ca --- /dev/null +++ b/docs/api/settings.md @@ -0,0 +1,13 @@ +--- +anta_title: ANTA Settings +--- + + +### ::: anta.settings + + options: + show_root_full_path: true diff --git a/docs/api/tests.md b/docs/api/tests.md index 0d0deab..ff7a19e 100644 --- a/docs/api/tests.md +++ b/docs/api/tests.md @@ -19,6 +19,7 @@ Here are the tests that we currently provide: - [Configuration](tests/configuration.md) - [Connectivity](tests/connectivity.md) - [CVX](tests/cvx.md) +- [EVPN](tests/evpn.md) - [Field Notices](tests/field_notices.md) - [Flow Tracking](tests/flow_tracking.md) - [GreenT](tests/greent.md) diff --git a/docs/api/tests/aaa.md b/docs/api/tests/aaa.md index 3b4d61a..9c512a0 100644 --- a/docs/api/tests/aaa.md +++ b/docs/api/tests/aaa.md @@ -1,5 +1,5 @@ --- -anta_title: ANTA catalog for AAA tests +anta_title: ANTA Tests for AAA --- +# Tests + ::: anta.tests.cvx options: - anta_hide_test_module_description: true + extra: + anta_hide_test_module_description: true + filters: + - "!test" + - "!render" + merge_init_into_class: false + show_bases: false + show_labels: true + show_root_heading: false + show_root_toc_entry: false + show_symbol_type_heading: false + show_symbol_type_toc: false + +# Input models + +::: anta.input_models.cvx + + options: + extra: + anta_hide_test_module_description: true filters: - "!test" - "!render" diff --git a/docs/api/tests/routing.isis.md b/docs/api/tests/evpn.md similarity index 78% copy from docs/api/tests/routing.isis.md copy to docs/api/tests/evpn.md index 23fd851..79a45d8 100644 --- a/docs/api/tests/routing.isis.md +++ b/docs/api/tests/evpn.md @@ -1,5 +1,5 @@ --- -anta_title: ANTA catalog for IS-IS tests +anta_title: ANTA Tests for EVPN --- +# Tests + ::: anta.tests.vlan options: - anta_hide_test_module_description: true + extra: + anta_hide_test_module_description: true filters: - "!test" - "!render" @@ -22,3 +25,20 @@ anta_title: ANTA catalog for VLAN tests show_root_toc_entry: false show_symbol_type_heading: false show_symbol_type_toc: false + +# Input models + +::: anta.input_models.vlan + + options: + extra: + anta_hide_test_module_description: true + filters: + - "!^__str__" + merge_init_into_class: false + show_bases: false + show_labels: true + show_root_heading: false + show_root_toc_entry: false + show_symbol_type_heading: false + show_symbol_type_toc: false diff --git a/docs/api/tests/vxlan.md b/docs/api/tests/vxlan.md index a79a0b1..ccd11c3 100644 --- a/docs/api/tests/vxlan.md +++ b/docs/api/tests/vxlan.md @@ -1,5 +1,5 @@ --- -anta_title: ANTA catalog for VXLAN tests +anta_title: ANTA Tests for VXLAN --- -`anta get tests` commands help you discover available tests in ANTA. +## `anta get tests` + +`anta get tests` commands help you discover the available tests in ANTA. ### Command overview ```bash -Usage: anta get tests [OPTIONS] - - Show all builtin ANTA tests with an example output retrieved from each test - documentation. - -Options: - --module TEXT Filter tests by module name. [default: anta.tests] - --test TEXT Filter by specific test name. If module is specified, - searches only within that module. - --short Display test names without their inputs. - --count Print only the number of tests found. - --help Show this message and exit. +--8<-- "anta_get_tests_help.txt" ``` > [!TIP] -> By default, `anta get tests` will retrieve all tests available in ANTA. +> By default, `anta get tests` retrieves all the tests available in ANTA. ### Examples @@ -60,7 +51,7 @@ anta.tests.aaa: [...] ``` -#### Module usage +#### Filtering using `--module` To retrieve all the tests from `anta.tests.stun`. @@ -81,7 +72,7 @@ anta.tests.stun: # Verifies the STUN server status is enabled and running. ``` -#### Test usage +#### Filtering using `--test` ``` yaml title="anta get tests --test VerifyTacacsSourceIntf" anta.tests.aaa: @@ -118,3 +109,132 @@ anta.tests.aaa: ```bash title="anta get tests --count" There are 155 tests available in `anta.tests`. ``` + +## `anta get commands` + +`anta get commands` returns the EOS commands used by the targeted tests, if no filter is provided, the targeted tests are all the built-in ANTA tests. + +### Command overview + +```bash +--8<-- "anta_get_commands_help.txt" +``` + +> [!TIP] +> By default, `anta get commands` returns the commands from every tests builtin in ANTA. + +### Examples + +#### Default usage + +``` yaml title="anta get commands" +anta.tests.aaa: + - VerifyAcctConsoleMethods: + - show aaa methods accounting + - VerifyAcctDefaultMethods: + - show aaa methods accounting + - VerifyAuthenMethods: + - show aaa methods authentication + - VerifyAuthzMethods: + - show aaa methods authorization + - VerifyTacacsServerGroups: + - show tacacs + - VerifyTacacsServers: + - show tacacs + - VerifyTacacsSourceIntf: + - show tacacs +anta.tests.avt: + - VerifyAVTPathHealth: + - show adaptive-virtual-topology path + - VerifyAVTRole: + - show adaptive-virtual-topology path + - VerifyAVTSpecificPath: + - show adaptive-virtual-topology path +[...] +``` + +#### Filtering using `--module` + +To retrieve all the commands from the tests in `anta.tests.stun`. + +``` yaml title="anta get commands --module anta.tests.stun" +anta.tests.stun: + - VerifyStunClient: + - show stun client translations {source_address} {source_port} + - VerifyStunClientTranslation: + - show stun client translations {source_address} {source_port} + - VerifyStunServer: + - show stun server status +``` + +#### Filtering using `--test` + +``` yaml title="anta get commands --test VerifyBGPExchangedRoutes" +anta.tests.routing.bgp: + - VerifyBGPExchangedRoutes: + - show bgp neighbors {peer} advertised-routes vrf {vrf} + - show bgp neighbors {peer} routes vrf {vrf} + vrf: MGMT +``` + +> [!TIP] +> You can filter tests by providing a prefix - ANTA will return all tests that start with your specified string. + +```yaml title="anta get tests --test VerifyTacacs" +anta.tests.aaa: + - VerifyTacacsServerGroups: + - show tacacs + - VerifyTacacsServers: + - show tacacs + - VerifyTacacsSourceIntf: + - show tacacs +``` + +#### Filtering using `--catalog` + +To retrieve all the commands from the tests in a catalog: + +``` yaml title="anta get commands --catalog my-catalog.yml" +anta.tests.interfaces: + - VerifyL3MTU: + - show interfaces +anta.tests.mlag: + - VerifyMlagStatus: + - show mlag +anta.tests.system: + - VerifyAgentLogs: + - show agent logs crash + - VerifyCPUUtilization: + - show processes top once + - VerifyCoredump: + - show system coredump + - VerifyFileSystemUtilization: + - bash timeout 10 df -h + - VerifyMemoryUtilization: + - show version + - VerifyNTP: + - show ntp status + - VerifyReloadCause: + - show reload cause + - VerifyUptime: + - show uptime +``` + +#### Output using `--unique` + +Using the `--unique` flag will output only the list of unique commands that will be run which can be useful to configure a AAA system. + +For instance with the previous catalog, the output would be: + +``` yaml title="anta get commands --catalog my-catalog.yml --unique" +show processes top once +bash timeout 10 df -h +show system coredump +show agent logs crash +show interfaces +show uptime +show ntp status +show version +show reload cause +show mlag +``` diff --git a/docs/cli/inv-from-ansible.md b/docs/cli/inv-from-ansible.md index 7d5848e..b3e0e0e 100644 --- a/docs/cli/inv-from-ansible.md +++ b/docs/cli/inv-from-ansible.md @@ -12,23 +12,7 @@ In large setups, it might be beneficial to construct your inventory based on you ## Command overview ```bash -$ anta get from-ansible --help -Usage: anta get from-ansible [OPTIONS] - - Build ANTA inventory from an ansible inventory YAML file. - - NOTE: This command does not support inline vaulted variables. Make sure to - comment them out. - -Options: - -o, --output FILE Path to save inventory file [env var: - ANTA_INVENTORY; required] - --overwrite Do not prompt when overriding current inventory - [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE] - -g, --ansible-group TEXT Ansible group to filter - --ansible-inventory FILE Path to your ansible inventory file to read - [required] - --help Show this message and exit. +--8<-- "anta_get_fromansible_help.txt" ``` > [!WARNING] diff --git a/docs/cli/inv-from-cvp.md b/docs/cli/inv-from-cvp.md index 0b4c715..6bfbefe 100644 --- a/docs/cli/inv-from-cvp.md +++ b/docs/cli/inv-from-cvp.md @@ -15,26 +15,7 @@ In large setups, it might be beneficial to construct your inventory based on Clo ## Command overview ```bash -Usage: anta get from-cvp [OPTIONS] - - Build ANTA inventory from CloudVision. - - NOTE: Only username/password authentication is supported for on-premises CloudVision instances. - Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported. - -Options: - -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY; - required] - --overwrite Do not prompt when overriding current inventory [env - var: ANTA_GET_FROM_CVP_OVERWRITE] - -host, --host TEXT CloudVision instance FQDN or IP [required] - -u, --username TEXT CloudVision username [required] - -p, --password TEXT CloudVision password [required] - -c, --container TEXT CloudVision container where devices are configured - --ignore-cert By default connection to CV will use HTTPS - certificate, set this flag to disable it [env var: - ANTA_GET_FROM_CVP_IGNORE_CERT] - --help Show this message and exit. +--8<-- "anta_get_fromcvp_help.txt" ``` The output is an inventory where the name of the container is added as a tag for each host: diff --git a/docs/contribution.md b/docs/contribution.md index f09aa3e..b1cc478 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -29,9 +29,22 @@ $ pip install -e .[dev,cli] $ pip list -e Package Version Editable project location ------- ------- ------------------------- -anta 1.3.0 /mnt/lab/projects/anta +anta 1.4.0 /mnt/lab/projects/anta ``` +!!! info "Installation Note" + 1. If you are using a terminal such as zsh, ensure that commands involving shell expansions within editable installs (like specifying development dependencies) are enclosed in double quotes. For example: `pip install -e ."[dev]"` + 2. If you do not see any output when running the verification command (`pip list -e`), it is likely because the command needs to be executed from within the inner `anta` directory. Navigate to this directory and then verify the installation: + + ``` + $ cd anta/anta + # Verify installation + $ pip list -e + Package Version Editable project location + ------- ------- -------------------------- + anta 1.4.0 /mnt/lab/projects/anta + ``` + Then, [`tox`](https://tox.wiki/) is configured with few environments to run CI locally: ```bash @@ -103,14 +116,19 @@ The `pytest_generate_tests` function will parametrize the generic test function See https://docs.pytest.org/en/7.3.x/how-to/parametrize.html#basic-pytest-generate-tests-example -The `DATA` structure is a list of dictionaries used to parametrize the test. The list elements have the following keys: +The `DATA` structure is a dictionary where: + +- Each key is a tuple of size 2 containing: + - An AntaTest subclass imported in the test module as first element - e.g. VerifyUptime. + - A string used as name displayed by pytest as second element. +- Each value is an instance of AntaUnitTest, which is a Python TypedDict. + +And AntaUnitTest have the following keys: -- `name` (str): Test name as displayed by Pytest. -- `test` (AntaTest): An AntaTest subclass imported in the test module - e.g. VerifyUptime. - `eos_data` (list[dict]): List of data mocking EOS returned data to be passed to the test. - `inputs` (dict): Dictionary to instantiate the `test` inputs as defined in the class from `test`. - `expected` (dict): Expected test result structure, a dictionary containing a key - `result` containing one of the allowed status (`Literal['success', 'failure', 'unset', 'skipped', 'error']`) and optionally a key `messages` which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object. + `result` containing one of the allowed status (`Literal[AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.SKIPPED]`) and optionally a key `messages` which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object. In order for your unit tests to be correctly collected, you need to import the generic test function even if not used in the Python module. @@ -124,29 +142,24 @@ from tests.units.anta_tests import test from anta.tests.system import VerifyUptime # Define test parameters -DATA: list[dict[str, Any]] = [ - { - # Arbitrary test name - "name": "success", - # Must be an AntaTest definition - "test": VerifyUptime, - # Data returned by EOS on which the AntaTest is tested - "eos_data": [{"upTime": 1186689.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], - # Dictionary to instantiate VerifyUptime.Input - "inputs": {"minimum": 666}, - # Expected test result - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyUptime, - "eos_data": [{"upTime": 665.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], - "inputs": {"minimum": 666}, - # If the test returns messages, it needs to be expected otherwise test will fail. - # NB: expected messages only needs to be included in messages returned by the test. Exact match is not required. - "expected": {"result": "failure", "messages": ["Device uptime is 665.15 seconds"]}, - }, -] +DATA: dict[tuple[type[AntaTest], str], AntaUnitTest] = { + (VerifyUptime, "success"): { + # Data returned by EOS on which the AntaTest is tested + "eos_data": [{"upTime": 1186689.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], + # Dictionary to instantiate VerifyUptime.Input + "inputs": {"minimum": 666}, + # Expected test result + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyUptime, "failure"): { + # Data returned by EOS on which the AntaTest is tested + "eos_data": [{"upTime": 665.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], + "inputs": {"minimum": 666}, + # If the test returns messages, it needs to be expected otherwise test will fail. + # NB: expected messages only needs to be included in messages returned by the test. Exact match is not required. + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device uptime is 665.15 seconds"]}, + } +} ``` ## Git Pre-commit hook diff --git a/docs/faq.md b/docs/faq.md index 2049724..3249442 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -30,7 +30,7 @@ anta_title: Frequently Asked Questions (FAQ) This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. - At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case. + At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the potential connections of all devices, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case. ### Solution @@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ) The `user` is the one with which the ANTA process is started. The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again. +## Tests throttling WARNING in the logs + +???+ faq "Tests throttling `WARNING` in the logs" + + ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs. + + By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more. + + ### Solution + + You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits. + + !!! warning + + Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly. + + !!! info "Device Connection Limits" + + Each EOS device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device. + + !!! tip + If you run ANTA on a large fabric or encounter issues related to resource limits, consider tuning `ANTA_MAX_CONCURRENCY`. + Test different values to find the optimal setting for your environment. + ## `Timeout` error in the logs ???+ faq "`Timeout` error in the logs" - When running ANTA, you can receive `Timeout` errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. + When running ANTA, you can receive `Timeout` errors in the logs (could be `ReadTimeout`, `WriteTimeout`, `ConnectTimeout` or `PoolTimeout`). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, ...) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests). @@ -59,8 +83,7 @@ anta_title: Frequently Asked Questions (FAQ) anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text ``` - The previous command set a couple of options for ANTA NRFU, one them being the `timeout` command, by default, when running ANTA from CLI, it is set to 30s. - The timeout is increased to 50s to allow ANTA to wait for API calls a little longer. + In this command, ANTA NRFU is configured with several options. Notably, the `--timeout` parameter is set to 50 seconds (instead of the default 30 seconds) to allow extra time for API calls to complete. ## `ImportError` related to `urllib3` @@ -154,6 +177,8 @@ anta_title: Frequently Asked Questions (FAQ) ``` You can then add other commands if they are required for your test catalog (`ping` for example) and then tighten down the show commands to only those required for your tests. + To figure out the full list of commands used by your catalog or ANTA in general, you can use [`anta get commands`](./cli/get-tests.md#anta-get-commands) + 2. Configure the following authorization (You may need to adapt depending on your AAA setup). diff --git a/docs/requirements-and-installation.md b/docs/requirements-and-installation.md index e6b02bf..419a1b2 100644 --- a/docs/requirements-and-installation.md +++ b/docs/requirements-and-installation.md @@ -84,7 +84,7 @@ which anta ```bash # Check ANTA version anta --version -anta, version v1.3.0 +anta, version v1.4.0 ``` ## EOS Requirements diff --git a/docs/scripts/generate_doc_snippets.py b/docs/scripts/generate_doc_snippets.py index 6ffa4ed..cab681f 100755 --- a/docs/scripts/generate_doc_snippets.py +++ b/docs/scripts/generate_doc_snippets.py @@ -22,6 +22,11 @@ COMMANDS = [ "anta nrfu tpl-report --help", "anta nrfu md-report --help", "anta get tags --help", + "anta get inventory --help", + "anta get tests --help", + "anta get from-cvp --help", + "anta get from-ansible --help", + "anta get commands --help", ] for command in COMMANDS: diff --git a/docs/snippets/anta_get_commands_help.txt b/docs/snippets/anta_get_commands_help.txt new file mode 100644 index 0000000..2c991a4 --- /dev/null +++ b/docs/snippets/anta_get_commands_help.txt @@ -0,0 +1,19 @@ +$ anta get commands --help +Usage: anta get commands [OPTIONS] + + Print all EOS commands used by the selected ANTA tests. + + It can be filtered by module, test or using a catalog. If no filter is + given, all built-in ANTA tests commands are retrieved. + +Options: + --module TEXT Filter commands by module name. [default: + anta.tests] + --test TEXT Filter by specific test name. If module is + specified, searches only within that module. + -c, --catalog FILE Path to the test catalog file [env var: + ANTA_CATALOG] + --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or + 'json' [env var: ANTA_CATALOG_FORMAT] + --unique Print only the unique commands. + --help Show this message and exit. diff --git a/docs/snippets/anta_get_fromansible_help.txt b/docs/snippets/anta_get_fromansible_help.txt new file mode 100644 index 0000000..05de2d4 --- /dev/null +++ b/docs/snippets/anta_get_fromansible_help.txt @@ -0,0 +1,17 @@ +$ anta get from-ansible --help +Usage: anta get from-ansible [OPTIONS] + + Build ANTA inventory from an ansible inventory YAML file. + + NOTE: This command does not support inline vaulted variables. Make sure to + comment them out. + +Options: + -o, --output FILE Path to save inventory file [env var: + ANTA_INVENTORY; required] + --overwrite Do not prompt when overriding current inventory + [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE] + -g, --ansible-group TEXT Ansible group to filter + --ansible-inventory FILE Path to your ansible inventory file to read + [required] + --help Show this message and exit. diff --git a/docs/snippets/anta_get_fromcvp_help.txt b/docs/snippets/anta_get_fromcvp_help.txt new file mode 100644 index 0000000..fa93c7a --- /dev/null +++ b/docs/snippets/anta_get_fromcvp_help.txt @@ -0,0 +1,22 @@ +$ anta get from-cvp --help +Usage: anta get from-cvp [OPTIONS] + + Build ANTA inventory from CloudVision. + + NOTE: Only username/password authentication is supported for on-premises + CloudVision instances. Token authentication for both on-premises and + CloudVision as a Service (CVaaS) is not supported. + +Options: + -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY; + required] + --overwrite Do not prompt when overriding current inventory [env + var: ANTA_GET_FROM_CVP_OVERWRITE] + -host, --host TEXT CloudVision instance FQDN or IP [required] + -u, --username TEXT CloudVision username [required] + -p, --password TEXT CloudVision password [required] + -c, --container TEXT CloudVision container where devices are configured + --ignore-cert Ignore verifying the SSL certificate when connecting + to CloudVision [env var: + ANTA_GET_FROM_CVP_IGNORE_CERT] + --help Show this message and exit. diff --git a/docs/snippets/anta_get_inventory_help.txt b/docs/snippets/anta_get_inventory_help.txt new file mode 100644 index 0000000..d9e9433 --- /dev/null +++ b/docs/snippets/anta_get_inventory_help.txt @@ -0,0 +1,37 @@ +$ anta get inventory --help +Usage: anta get inventory [OPTIONS] + + Show inventory loaded in ANTA. + +Options: + -u, --username TEXT Username to connect to EOS [env var: + ANTA_USERNAME; required] + -p, --password TEXT Password to connect to EOS that must be + provided. It can be prompted using '-- + prompt' option. [env var: ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. + It can be prompted using '--prompt' option. + Requires '--enable' option. [env var: + ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged + EXEC mode. This option tries to access this + mode before sending a command to the device. + [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not + provided. [env var: ANTA_PROMPT] + --timeout FLOAT Global API timeout. This value will be used + for all devices. [env var: ANTA_TIMEOUT; + default: 30.0] + --insecure Disable SSH Host Key validation. [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally. [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file. [env var: + ANTA_INVENTORY; required] + --inventory-format [yaml|json] Format of the inventory file, either 'yaml' + or 'json' [env var: ANTA_INVENTORY_FORMAT] + --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3. [env var: ANTA_TAGS] + --connected / --not-connected Display inventory after connection has been + created + --help Show this message and exit. diff --git a/docs/snippets/anta_get_tests_help.txt b/docs/snippets/anta_get_tests_help.txt new file mode 100644 index 0000000..34d2814 --- /dev/null +++ b/docs/snippets/anta_get_tests_help.txt @@ -0,0 +1,13 @@ +$ anta get tests --help +Usage: anta get tests [OPTIONS] + + Show all builtin ANTA tests with an example output retrieved from each test + documentation. + +Options: + --module TEXT Filter tests by module name. [default: anta.tests] + --test TEXT Filter by specific test name. If module is specified, + searches only within that module. + --short Display test names without their inputs. + --count Print only the number of tests found. + --help Show this message and exit. diff --git a/examples/tests.yaml b/examples/tests.yaml index d53150e..a25957f 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -131,8 +131,7 @@ anta.tests.connectivity: df_bit: True size: 100 reachable: true - - source: Management0 - destination: 8.8.8.8 + - destination: 8.8.8.8 vrf: MGMT df_bit: True size: 100 @@ -162,6 +161,39 @@ anta.tests.cvx: - VerifyMcsServerMounts: # Verify if all MCS server mounts are in a MountComplete state. connections_count: 100 +anta.tests.evpn: + - VerifyEVPNType5Routes: + # Verifies EVPN Type-5 routes for given IP prefixes and VNIs. + prefixes: + # At least one active/valid path across all RDs + - address: 192.168.10.0/24 + vni: 10 + # Specific routes each has at least one active/valid path + - address: 192.168.20.0/24 + vni: 20 + routes: + - rd: "10.0.0.1:20" + domain: local + - rd: "10.0.0.2:20" + domain: remote + # At least one active/valid path matching the nexthop + - address: 192.168.30.0/24 + vni: 30 + routes: + - rd: "10.0.0.1:30" + domain: local + paths: + - nexthop: 10.1.1.1 + # At least one active/valid path matching nexthop and specific RTs + - address: 192.168.40.0/24 + vni: 40 + routes: + - rd: "10.0.0.1:40" + domain: local + paths: + - nexthop: 10.1.1.1 + route_targets: + - "40:40" anta.tests.field_notices: - VerifyFieldNotice44Resolution: # Verifies that the device is using the correct Aboot version per FN0044. @@ -215,8 +247,14 @@ anta.tests.interfaces: - Ethernet2 - VerifyIllegalLACP: # Verifies there are no illegal LACP packets in all port channels. + ignored_interfaces: + - Port-Channel1 + - Port-Channel2 - VerifyInterfaceDiscards: # Verifies that the interfaces packet discard counters are equal to zero. + ignored_interfaces: + - Ethernet + - Port-Channel1 - VerifyInterfaceErrDisabled: # Verifies there are no interfaces in the errdisabled state. - VerifyInterfaceErrors: @@ -232,6 +270,9 @@ anta.tests.interfaces: - VerifyInterfaceUtilization: # Verifies that the utilization of interfaces is below a certain threshold. threshold: 70.0 + ignored_interfaces: + - Ethernet1 + - Port-Channel1 - VerifyInterfacesSpeed: # Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. interfaces: @@ -261,19 +302,21 @@ anta.tests.interfaces: mac_address: 00:1c:73:00:dc:01 - VerifyL2MTU: # Verifies the global L2 MTU of all L2 interfaces. - mtu: 1500 + mtu: 9214 ignored_interfaces: - - Management1 - - Vxlan1 + - Ethernet2/1 + - Port-Channel # Ignore all Port-Channel interfaces specific_mtu: - Ethernet1/1: 1500 - VerifyL3MTU: # Verifies the global L3 MTU of all L3 interfaces. mtu: 1500 ignored_interfaces: - - Vxlan1 + - Management # Ignore all Management interfaces + - Ethernet2.100 + - Ethernet1/1 specific_mtu: - - Ethernet1: 2500 + - Ethernet10: 9200 - VerifyLACPInterfacesStatus: # Verifies the Link Aggregation Control Protocol (LACP) status of the interface. interfaces: @@ -284,6 +327,10 @@ anta.tests.interfaces: number: 3 - VerifyPortChannels: # Verifies there are no inactive ports in all port channels. + ignored_interfaces: + - Port-Channel1 + - Port-Channel2 + - VerifySVI: # Verifies the status of all SVIs. - VerifyStormControlDrops: @@ -297,10 +344,10 @@ anta.tests.logging: - VerifyLoggingEntries: # Verifies that the expected log string is present in the last specified log messages. logging_entries: - - regex_match: ".ACCOUNTING-5-EXEC: cvpadmin ssh." + - regex_match: ".*ACCOUNTING-5-EXEC: cvpadmin ssh.*" last_number_messages: 30 severity_level: alerts - - regex_match: ".SPANTREE-6-INTERFACE_ADD:." + - regex_match: ".*SPANTREE-6-INTERFACE_ADD:.*" last_number_messages: 10 severity_level: critical - VerifyLoggingErrors: @@ -388,14 +435,22 @@ anta.tests.ptp: # Verifies the PTP interfaces state. anta.tests.routing.bgp: - VerifyBGPAdvCommunities: - # Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s). + # Verifies the advertised communities of BGP peers. bgp_peers: - peer_address: 172.30.11.17 vrf: default - peer_address: 172.30.11.21 + vrf: MGMT + advertised_communities: ["standard", "extended"] + - peer_address: fd00:dc:1::1 vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default + advertised_communities: ["standard", "extended"] - VerifyBGPExchangedRoutes: # Verifies the advertised and received routes of BGP IPv4 peer(s). + check_active: True bgp_peers: - peer_address: 172.30.255.5 vrf: default @@ -409,17 +464,35 @@ anta.tests.routing.bgp: - 192.0.255.1/32 - 192.0.254.5/32 - VerifyBGPNlriAcceptance: - # Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP IPv4 peer(s). + # Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers. bgp_peers: - peer_address: 10.100.0.128 vrf: default capabilities: - ipv4Unicast + - peer_address: 2001:db8:1::2 + vrf: default + capabilities: + - ipv6Unicast + - peer_address: fe80::2%Et1 + vrf: default + capabilities: + - ipv6Unicast + # RFC 5549 + - peer_address: fe80::2%Et1 + vrf: default + capabilities: + - ipv6Unicast - VerifyBGPPeerASNCap: - # Verifies the four octet ASN capability of BGP IPv4 peer(s). + # Verifies the four octet ASN capability of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT - VerifyBGPPeerCount: # Verifies the count of BGP peers for given address families. address_families: @@ -438,28 +511,51 @@ anta.tests.routing.bgp: vrf: "DEV" num_peers: 3 - VerifyBGPPeerDropStats: - # Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). + # Verifies BGP NLRI drop statistics of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default drop_stats: - inDropAsloop - prefixEvpnDroppedUnsupportedRouteType + - peer_address: fd00:dc:1::1 + vrf: default + drop_stats: + - inDropAsloop + - prefixEvpnDroppedUnsupportedRouteType + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + drop_stats: + - inDropAsloop + - prefixEvpnDroppedUnsupportedRouteType - VerifyBGPPeerGroup: - # Verifies BGP peer group of BGP IPv4 peer(s). + # Verifies BGP peer group of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default peer_group: IPv4-UNDERLAY-PEERS + - peer_address: fd00:dc:1::1 + vrf: default + peer_group: IPv4-UNDERLAY-PEERS + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + peer_group: IPv4-UNDERLAY-PEERS - VerifyBGPPeerMD5Auth: - # Verifies the MD5 authentication and state of IPv4 BGP peer(s) in a specified VRF. + # Verifies the MD5 authentication and state of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default - peer_address: 172.30.11.5 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default - VerifyBGPPeerMPCaps: - # Verifies the multiprotocol capabilities of BGP IPv4 peer(s). + # Verifies the multiprotocol capabilities of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default @@ -467,20 +563,47 @@ anta.tests.routing.bgp: capabilities: - ipv4 labeled-Unicast - ipv4MplsVpn + - peer_address: fd00:dc:1::1 + vrf: default + strict: False + capabilities: + - ipv4 labeled-Unicast + - ipv4MplsVpn + # RFC5549 + - interface: Ethernet1 + vrf: default + strict: False + capabilities: + - ipv4 labeled-Unicast + - ipv4MplsVpn - VerifyBGPPeerRouteLimit: - # Verifies maximum routes and warning limit for BGP IPv4 peer(s). + # Verifies maximum routes and warning limit for BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default maximum_routes: 12000 warning_limit: 10000 + - peer_address: fd00:dc:1::1 + vrf: default + maximum_routes: 12000 + warning_limit: 10000 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + maximum_routes: 12000 + warning_limit: 10000 - VerifyBGPPeerRouteRefreshCap: - # Verifies the route refresh capabilities of IPv4 BGP peer(s) in a specified VRF. + # Verifies the route refresh capabilities of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT - VerifyBGPPeerSession: - # Verifies the session state of BGP IPv4 peer(s). + # Verifies the session state of BGP peers. minimum_established_time: 10000 check_tcp_queues: false bgp_peers: @@ -492,21 +615,29 @@ anta.tests.routing.bgp: vrf: DEV - peer_address: 10.1.255.4 vrf: DEV + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: default + - interface: Vlan3499 + vrf: PROD - VerifyBGPPeerSessionRibd: - # Verifies the session state of BGP IPv4 peer(s). + # Verifies the session state of BGP peers. minimum_established_time: 10000 check_tcp_queues: false bgp_peers: - peer_address: 10.1.0.1 vrf: default - - peer_address: 10.1.0.2 - vrf: default - - peer_address: 10.1.255.2 - vrf: DEV - peer_address: 10.1.255.4 vrf: DEV + - peer_address: fd00:dc:1::1 + vrf: default + # RFC5549 + - interface: Ethernet1 + vrf: MGMT - VerifyBGPPeerTtlMultiHops: - # Verifies BGP TTL and max-ttl-hops count for BGP IPv4 peer(s). + # Verifies BGP TTL and max-ttl-hops count for BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default @@ -516,13 +647,31 @@ anta.tests.routing.bgp: vrf: test ttl: 30 max_ttl_hops: 30 + - peer_address: fd00:dc:1::1 + vrf: default + ttl: 30 + max_ttl_hops: 30 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + ttl: 30 + max_ttl_hops: 30 - VerifyBGPPeerUpdateErrors: - # Verifies BGP update error counters for the provided BGP IPv4 peer(s). + # Verifies BGP update error counters of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default update_errors: - inUpdErrWithdraw + - peer_address: fd00:dc:1::1 + vrf: default + update_errors: + - inUpdErrWithdraw + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + update_errors: + - inUpdErrWithdraw - VerifyBGPPeersHealth: # Verifies the health of BGP peers for given address families. minimum_established_time: 10000 @@ -536,7 +685,7 @@ anta.tests.routing.bgp: vrf: "DEV" check_tcp_queues: false - VerifyBGPPeersHealthRibd: - # Verifies the health of all the BGP IPv4 peer(s). + # Verifies the health of all the BGP peers. check_tcp_queues: True - VerifyBGPRedistribution: # Verifies BGP redistribution. @@ -590,7 +739,7 @@ anta.tests.routing.bgp: - 10.1.255.2 - 10.1.255.4 - VerifyBGPTimers: - # Verifies the timers of BGP IPv4 peer(s). + # Verifies the timers of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default @@ -600,13 +749,31 @@ anta.tests.routing.bgp: vrf: default hold_time: 180 keep_alive_time: 60 + - peer_address: fd00:dc:1::1 + vrf: default + hold_time: 180 + keep_alive_time: 60 + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + hold_time: 180 + keep_alive_time: 60 - VerifyBgpRouteMaps: - # Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). + # Verifies BGP inbound and outbound route-maps of BGP peers. bgp_peers: - peer_address: 172.30.11.1 vrf: default inbound_route_map: RM-MLAG-PEER-IN outbound_route_map: RM-MLAG-PEER-OUT + - peer_address: fd00:dc:1::1 + vrf: default + inbound_route_map: RM-MLAG-PEER-IN + outbound_route_map: RM-MLAG-PEER-OUT + # RFC5549 + - interface: Ethernet1 + vrf: MGMT + inbound_route_map: RM-MLAG-PEER-IN + outbound_route_map: RM-MLAG-PEER-OUT - VerifyEVPNType2Route: # Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI. vxlan_endpoints: @@ -639,6 +806,10 @@ anta.tests.routing.generic: - VerifyRoutingProtocolModel: # Verifies the configured routing protocol model. model: multi-agent + - VerifyRoutingStatus: + # Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549). + ipv4_unicast: True + ipv6_unicast: True - VerifyRoutingTableEntry: # Verifies that the provided routes are present in the routing table of a specified VRF. vrf: default @@ -650,6 +821,21 @@ anta.tests.routing.generic: minimum: 2 maximum: 20 anta.tests.routing.isis: + - VerifyISISGracefulRestart: + # Verifies the IS-IS graceful restart feature. + instances: + - name: '1' + vrf: default + graceful_restart: True + graceful_restart_helper: False + - name: '2' + vrf: default + - name: '11' + vrf: test + graceful_restart: True + - name: '12' + vrf: test + graceful_restart_helper: False - VerifyISISInterfaceMode: # Verifies IS-IS interfaces are running in the correct mode. interfaces: @@ -997,6 +1183,10 @@ anta.tests.system: preferred_stratum_range: [1,3] - VerifyReloadCause: # Verifies the last reload cause of the device. + allowed_causes: + - USER + - FPGA + - ZTP - VerifyUptime: # Verifies the device uptime. minimum: 86400 @@ -1012,9 +1202,16 @@ anta.tests.vlan: policy: ascending start_vlan_id: 1006 end_vlan_id: 4094 + - VerifyVlanStatus: + # Verifies the administrative status of specified VLANs. + vlans: + - vlan_id: 10 + status: suspended + - vlan_id: 4094 + status: active anta.tests.vxlan: - VerifyVxlan1ConnSettings: - # Verifies the interface vxlan1 source interface and UDP port. + # Verifies Vxlan1 source interface and UDP port. source_interface: Loopback1 udp_port: 4789 - VerifyVxlan1Interface: @@ -1022,12 +1219,13 @@ anta.tests.vxlan: - VerifyVxlanConfigSanity: # Verifies there are no VXLAN config-sanity inconsistencies. - VerifyVxlanVniBinding: - # Verifies the VNI-VLAN bindings of the Vxlan1 interface. + # Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface. bindings: 10010: 10 10020: 20 + 500: PROD - VerifyVxlanVtep: - # Verifies the VTEP peers of the Vxlan1 interface. + # Verifies Vxlan1 VTEP peers. vteps: - 10.1.1.5 - 10.1.1.6 diff --git a/mkdocs.yml b/mkdocs.yml index 5b3c6dc..ee29a92 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,7 +2,7 @@ site_name: Arista Network Test Automation - ANTA site_author: Khelil Sator site_description: Arista Network Test Automation -copyright: Copyright © 2019 - 2024 Arista Networks +copyright: Copyright © 2019 - 2025 Arista Networks # Repository repo_name: ANTA on Github @@ -185,9 +185,9 @@ nav: - Debug commands: cli/debug.md - Tag Management: cli/tag-management.md - Advanced Usages: - - Caching in ANTA: advanced_usages/caching.md - - Developing ANTA tests: advanced_usages/custom-tests.md - - ANTA as a Python Library: advanced_usages/as-python-lib.md + - Caching in ANTA: advanced_usages/caching.md + - Developing ANTA tests: advanced_usages/custom-tests.md + - ANTA as a Python Library: advanced_usages/as-python-lib.md - Tests Documentation: - Overview: api/tests.md - AAA: api/tests/aaa.md @@ -196,6 +196,7 @@ nav: - Configuration: api/tests/configuration.md - Connectivity: api/tests/connectivity.md - CVX: api/tests/cvx.md + - EVPN: api/tests/evpn.md - Field Notices: api/tests/field_notices.md - Flow Tracking: api/tests/flow_tracking.md - GreenT: api/tests/greent.md @@ -239,6 +240,7 @@ nav: - CSV: api/reporter/csv.md - Jinja: api/reporter/jinja.md - Runner: api/runner.md + - Settings: api/settings.md - Troubleshooting ANTA: troubleshooting.md - Contributions: contribution.md - FAQ: faq.md diff --git a/pyproject.toml b/pyproject.toml index 69a72a4..20281f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "anta" -version = "v1.3.0" +version = "v1.4.0" readme = "docs/README.md" authors = [{ name = "Arista Networks ANTA maintainers", email = "anta-dev@arista.com" }] maintainers = [ @@ -17,7 +17,6 @@ maintainers = [ { name = "Carl Baillargeon", email = "carl.baillargeon@arista.com" }, ] description = "Arista Network Test Automation (ANTA) Framework" -license = { file = "LICENSE" } dependencies = [ "asyncssh>=2.16", "cvprac>=1.3.1", @@ -26,14 +25,16 @@ dependencies = [ "Jinja2>=3.1.2", "pydantic>=2.7", "pydantic-extra-types>=2.3.0", + "pydantic-settings>=2.6.0", "PyYAML>=6.0", "requests>=2.31.0", - "rich>=13.5.2,<14", + "rich>=13.5.2,<15", "typing_extensions>=4.12" # required for deprecated before Python 3.13 ] keywords = ["test", "anta", "Arista", "network", "automation", "networking", "devops", "netdevops"] +license = "Apache-2.0" +license-files = ["LICENSE"] classifiers = [ - "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -56,7 +57,6 @@ requires-python = ">=3.9" [project.optional-dependencies] cli = [ "click~=8.1.6", - "click-help-colors>=0.9", ] dev = [ "bumpver>=2023.1129", @@ -75,7 +75,7 @@ dev = [ "pytest-metadata>=3.0.0", "pytest>=7.4.0", "respx>=0.22.0", - "ruff>=0.5.4,<0.11.0", + "ruff>=0.5.4,<0.12.0", "tox>=4.10.0,<5.0.0", "types-PyYAML", "types-pyOpenSSL", @@ -121,7 +121,7 @@ namespaces = false # Version ################################ [tool.bumpver] -current_version = "1.3.0" +current_version = "1.4.0" version_pattern = "MAJOR.MINOR.PATCH" commit_message = "bump: Version {old_version} -> {new_version}" commit = true @@ -493,3 +493,5 @@ min-similarity-lines=10 signature-mutators="click.decorators.option" load-plugins="pylint_pydantic" extension-pkg-whitelist="pydantic" +# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058 +max-attributes=15 diff --git a/sonar-project.properties b/sonar-project.properties index 0cc0d7c..8e8a87a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -17,7 +17,7 @@ sonar.tests=tests/ #sonar.sourceEncoding=UTF-8 # Python version (for python projects only) -sonar.python.version=3.9,3.10,3.11,3.12 +sonar.python.version=3.9,3.10,3.11,3.12,3.13 # Exclusions for copy-paste detection #sonar.cpd.exclusions=, diff --git a/tests/benchmark/test_anta.py b/tests/benchmark/test_anta.py index 1daf7f3..fdf6fcb 100644 --- a/tests/benchmark/test_anta.py +++ b/tests/benchmark/test_anta.py @@ -37,6 +37,7 @@ def test_anta_dry_run( results = session_results[request.node.callspec.id] + # TODO: Use AntaRunner directly in ANTA v2.0.0 @benchmark def _() -> None: results.reset() @@ -69,6 +70,7 @@ def test_anta( results = session_results[request.node.callspec.id] + # TODO: Use AntaRunner directly in ANTA v2.0.0 @benchmark def _() -> None: results.reset() @@ -77,21 +79,6 @@ def test_anta( logging.disable(logging.NOTSET) - if len(catalog.tests) * len(inventory) != len(results.results): - # This could mean duplicates exist. - # TODO: consider removing this code and refactor unit test data as a dictionary with tuple keys instead of a list - seen = set() - dupes = [] - for test in catalog.tests: - if test in seen: - dupes.append(test) - else: - seen.add(test) - if dupes: - for test in dupes: - msg = f"Found duplicate in test catalog: {test}" - logger.error(msg) - pytest.fail(f"Expected {len(catalog.tests) * len(inventory)} tests but got {len(results.results)}", pytrace=False) bench_info = ( "\n--- ANTA NRFU Benchmark Information ---\n" f"Test results: {len(results.results)}\n" diff --git a/tests/benchmark/test_reporter.py b/tests/benchmark/test_reporter.py index c6d0b29..0a5b708 100644 --- a/tests/benchmark/test_reporter.py +++ b/tests/benchmark/test_reporter.py @@ -67,5 +67,6 @@ def test_csv(results: ResultManager, tmp_path: Path) -> None: @pytest.mark.benchmark @pytest.mark.dependency(depends=["anta_benchmark"], scope="package") def test_markdown(results: ResultManager, tmp_path: Path) -> None: - """Benchmark MDReportGenerator.generate().""" - MDReportGenerator.generate(results=results, md_filename=tmp_path / "report.md") + """Benchmark MDReportGenerator.generate_sections().""" + sections = [(section, results) for section in MDReportGenerator.DEFAULT_SECTIONS] + MDReportGenerator.generate_sections(sections=sections, md_filename=tmp_path / "report.md") diff --git a/tests/benchmark/test_runner.py b/tests/benchmark/test_runner.py index 9aa54df..f3b713e 100644 --- a/tests/benchmark/test_runner.py +++ b/tests/benchmark/test_runner.py @@ -7,6 +7,9 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +import pytest + +from anta._runner import AntaRunContext, AntaRunFilters, AntaRunner from anta.result_manager import ResultManager from anta.runner import get_coroutines, prepare_tests @@ -22,6 +25,8 @@ if TYPE_CHECKING: from anta.result_manager.models import TestResult +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_prepare_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None: """Benchmark `anta.runner.prepare_tests`.""" @@ -36,6 +41,8 @@ def test_prepare_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, invent assert sum(len(tests) for tests in selected_tests.values()) == len(inventory) * len(catalog.tests) +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_get_coroutines(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None: """Benchmark `anta.runner.get_coroutines`.""" selected_tests = prepare_tests(inventory=inventory, catalog=catalog, tests=None, tags=None) @@ -52,3 +59,38 @@ def test_get_coroutines(benchmark: BenchmarkFixture, catalog: AntaCatalog, inven count = sum(len(tests) for tests in selected_tests.values()) assert count == len(coroutines) + + +def test__setup_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None: + """Benchmark `anta._runner.AntaRunner._setup_tests`.""" + runner = AntaRunner() + ctx = AntaRunContext(inventory=inventory, catalog=catalog, manager=ResultManager(), filters=AntaRunFilters(), selected_inventory=inventory) + + def bench() -> None: + catalog.clear_indexes() + runner._setup_tests(ctx) + + benchmark(bench) + + assert ctx.total_tests_scheduled != 0 + assert ctx.total_devices_selected_for_testing == len(inventory) + assert ctx.total_tests_scheduled == len(inventory) * len(catalog.tests) + + +def test__get_test_coroutines(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None: + """Benchmark `anta._runner.AntaRunner._get_test_coroutines`.""" + runner = AntaRunner() + ctx = AntaRunContext(inventory=inventory, catalog=catalog, manager=ResultManager(), filters=AntaRunFilters(), selected_inventory=inventory) + runner._setup_tests(ctx) + + assert ctx.selected_tests is not None + + def bench() -> list[Coroutine[Any, Any, TestResult]]: + coros = runner._get_test_coroutines(ctx) + for c in coros: + c.close() + return coros + + coroutines = benchmark(bench) + + assert ctx.total_tests_scheduled == len(coroutines) diff --git a/tests/benchmark/utils.py b/tests/benchmark/utils.py index 2a84430..27e00fc 100644 --- a/tests/benchmark/utils.py +++ b/tests/benchmark/utils.py @@ -50,13 +50,21 @@ class AntaMockEnvironment: # pylint: disable=too-few-public-methods Also provide the attribute 'eos_data_catalog` with the output of all the commands used in the test catalog. Each module in `tests.units.anta_tests` has a `DATA` constant. - The `DATA` structure is a list of dictionaries used to parametrize the test. The list elements have the following keys: - - `name` (str): Test name as displayed by Pytest. - - `test` (AntaTest): An AntaTest subclass imported in the test module - e.g. VerifyUptime. - - `eos_data` (list[dict]): List of data mocking EOS returned data to be passed to the test. - - `inputs` (dict): Dictionary to instantiate the `test` inputs as defined in the class from `test`. - The keys of `eos_data_catalog` is the tuple (DATA['test'], DATA['name']). The values are `eos_data`. + The `DATA` structure is a dictionary where: + - Each key is a tuple of size 2 containing: + - An AntaTest subclass imported in the test module as first element - e.g. VerifyUptime. + - A string used as name displayed by pytest as second element. + - Each value is an instance of AntaUnitTest, which is a Python TypedDict. + + And AntaUnitTest have the following keys: + - `eos_data` (list[dict]): List of data mocking EOS returned data to be passed to the test. + - `inputs` (dict): Dictionary to instantiate the `test` inputs as defined in the class from `test`. + - `expected` (dict): Expected test result structure, a dictionary containing a key `result` containing one of the allowed status + (`Literal[AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.SKIPPED]`) and + optionally a key `messages` which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object. + + The keys of `eos_data_catalog` is the tuple (AntaTest subclass, A string used as name displayed by pytest). The values are `eos_data`. """ def __init__(self) -> None: @@ -87,10 +95,11 @@ class AntaMockEnvironment: # pylint: disable=too-few-public-methods test_definitions = [] eos_data_catalog = {} for module in import_test_modules(): - for test_data in module.DATA: - test = test_data["test"] - result_overwrite = AntaTest.Input.ResultOverwrite(custom_field=test_data["name"]) - if test_data["inputs"] is None: + for (test, name), test_data in module.DATA.items(): + # Extract the test class, name and test data from a nested tuple structure: + # unit test: Tuple[Tuple[Type[AntaTest], str], AntaUnitTest] + result_overwrite = AntaTest.Input.ResultOverwrite(custom_field=name) + if test_data.get("inputs") is None: inputs = test.Input(result_overwrite=result_overwrite) else: inputs = test.Input(**test_data["inputs"], result_overwrite=result_overwrite) @@ -98,7 +107,7 @@ class AntaMockEnvironment: # pylint: disable=too-few-public-methods test=test, inputs=inputs, ) - eos_data_catalog[(test.__name__, test_data["name"])] = test_data["eos_data"] + eos_data_catalog[(test.__name__, name)] = test_data["eos_data"] test_definitions.append(test_definition) return (AntaCatalog(tests=test_definitions), eos_data_catalog) diff --git a/tests/conftest.py b/tests/conftest.py index 362d9d0..9086266 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,10 +3,8 @@ # that can be found in the LICENSE file. """See https://docs.pytest.org/en/stable/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files.""" -import asyncio from collections.abc import Iterator from pathlib import Path -from unittest.mock import AsyncMock, Mock, patch import pytest import respx @@ -42,7 +40,8 @@ def inventory(request: pytest.FixtureRequest) -> Iterator[AntaInventory]: ) if reachable: # This context manager makes all devices reachable - with patch("asyncio.open_connection", AsyncMock(spec=asyncio.open_connection, return_value=(Mock(), Mock()))), respx.mock: + with respx.mock: + respx.head(path="/command-api") respx.post(path="/command-api", headers={"Content-Type": "application/json-rpc"}, json__params__cmds__0__cmd="show version").respond( json={ "result": [ @@ -54,5 +53,6 @@ def inventory(request: pytest.FixtureRequest) -> Iterator[AntaInventory]: ) yield inv else: - with patch("asyncio.open_connection", AsyncMock(spec=asyncio.open_connection, side_effect=TimeoutError)): + with respx.mock: + respx.head(path="/command-api").respond(status_code=401) yield inv diff --git a/tests/data/syntax_error.py b/tests/data/syntax_error.py index e3cec34..9b8a94a 100644 --- a/tests/data/syntax_error.py +++ b/tests/data/syntax_error.py @@ -1,7 +1,7 @@ # 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. +"""Syntax Error in file.""" + # pylint: skip-file -# flake8: noqa -# type: ignore -typo +typo # type: ignore[name-defined] # noqa: B018 diff --git a/tests/data/test_md_report.md b/tests/data/test_md_report.md index 1b1acd1..7a9db72 100644 --- a/tests/data/test_md_report.md +++ b/tests/data/test_md_report.md @@ -15,78 +15,233 @@ | Total Tests | Total Tests Success | Total Tests Skipped | Total Tests Failure | Total Tests Error | | ----------- | ------------------- | ------------------- | ------------------- | ------------------| -| 30 | 4 | 9 | 15 | 2 | +| 181 | 43 | 34 | 103 | 1 | ### Summary Totals Device Under Test | Device Under Test | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | Categories Skipped | Categories Failed | | ------------------| ----------- | ------------- | ------------- | ------------- | ----------- | -------------------| ------------------| -| s1-spine1 | 30 | 4 | 9 | 15 | 2 | AVT, Field Notices, Hardware, ISIS, LANZ, OSPF, PTP, Path-Selection, Profiles | AAA, BFD, BGP, Connectivity, Cvx, Interfaces, Logging, MLAG, SNMP, STUN, Security, Services, Software, System, VLAN | +| s1-spine1 | 181 | 43 | 34 | 103 | 1 | AVT, Field Notices, Flow Tracking, Hardware, ISIS, Interfaces, LANZ, OSPF, PTP, Path-Selection, Profiles, Segment-Routing | AAA, BFD, BGP, Configuration, Connectivity, Cvx, Greent, Interfaces, Logging, MLAG, Multicast, Routing, SNMP, STP, STUN, Security, Services, Software, VLAN, VXLAN | ### Summary Totals Per Category | Test Category | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | | ------------- | ----------- | ------------- | ------------- | ------------- | ----------- | -| AAA | 1 | 0 | 0 | 1 | 0 | -| AVT | 1 | 0 | 1 | 0 | 0 | -| BFD | 1 | 0 | 0 | 1 | 0 | -| BGP | 1 | 0 | 0 | 0 | 1 | -| Configuration | 1 | 1 | 0 | 0 | 0 | -| Connectivity | 1 | 0 | 0 | 1 | 0 | -| Cvx | 1 | 0 | 0 | 0 | 1 | -| Field Notices | 1 | 0 | 1 | 0 | 0 | -| Hardware | 1 | 0 | 1 | 0 | 0 | -| Interfaces | 1 | 0 | 0 | 1 | 0 | -| ISIS | 1 | 0 | 1 | 0 | 0 | +| AAA | 7 | 0 | 0 | 7 | 0 | +| AVT | 3 | 0 | 3 | 0 | 0 | +| BFD | 4 | 1 | 0 | 3 | 0 | +| BGP | 25 | 3 | 0 | 21 | 1 | +| Configuration | 3 | 1 | 0 | 2 | 0 | +| Connectivity | 2 | 1 | 0 | 1 | 0 | +| Cvx | 5 | 0 | 0 | 5 | 0 | +| Field Notices | 2 | 0 | 2 | 0 | 0 | +| Flow Tracking | 1 | 0 | 1 | 0 | 0 | +| Greent | 2 | 0 | 0 | 2 | 0 | +| Hardware | 7 | 0 | 7 | 0 | 0 | +| Interfaces | 16 | 7 | 1 | 8 | 0 | +| ISIS | 7 | 0 | 7 | 0 | 0 | | LANZ | 1 | 0 | 1 | 0 | 0 | -| Logging | 1 | 0 | 0 | 1 | 0 | -| MLAG | 1 | 0 | 0 | 1 | 0 | -| OSPF | 1 | 0 | 1 | 0 | 0 | -| Path-Selection | 1 | 0 | 1 | 0 | 0 | -| Profiles | 1 | 0 | 1 | 0 | 0 | -| PTP | 1 | 0 | 1 | 0 | 0 | -| Routing | 1 | 1 | 0 | 0 | 0 | -| Security | 2 | 0 | 0 | 2 | 0 | -| Services | 1 | 0 | 0 | 1 | 0 | -| SNMP | 1 | 0 | 0 | 1 | 0 | -| Software | 1 | 0 | 0 | 1 | 0 | -| STP | 1 | 1 | 0 | 0 | 0 | -| STUN | 2 | 0 | 0 | 2 | 0 | -| System | 1 | 0 | 0 | 1 | 0 | -| VLAN | 1 | 0 | 0 | 1 | 0 | -| VXLAN | 1 | 1 | 0 | 0 | 0 | +| Logging | 10 | 3 | 0 | 7 | 0 | +| MLAG | 6 | 4 | 0 | 2 | 0 | +| Multicast | 2 | 1 | 0 | 1 | 0 | +| OSPF | 3 | 0 | 3 | 0 | 0 | +| Path-Selection | 2 | 0 | 2 | 0 | 0 | +| Profiles | 2 | 0 | 2 | 0 | 0 | +| PTP | 5 | 0 | 5 | 0 | 0 | +| Routing | 6 | 2 | 0 | 4 | 0 | +| Security | 15 | 3 | 0 | 12 | 0 | +| Segment-Routing | 3 | 0 | 3 | 0 | 0 | +| Services | 4 | 1 | 0 | 3 | 0 | +| SNMP | 12 | 0 | 0 | 12 | 0 | +| Software | 3 | 1 | 0 | 2 | 0 | +| STP | 7 | 4 | 0 | 3 | 0 | +| STUN | 3 | 0 | 0 | 3 | 0 | +| System | 8 | 8 | 0 | 0 | 0 | +| VLAN | 3 | 0 | 0 | 3 | 0 | +| VXLAN | 5 | 3 | 0 | 2 | 0 | ## Test Results | Device Under Test | Categories | Test | Description | Custom Field | Result | Messages | | ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- | | s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x | -| s1-spine1 | AVT | VerifyAVTPathHealth | Verifies the status of all AVT paths for all VRFs. | - | skipped | VerifyAVTPathHealth test is not supported on cEOSLab. | -| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | failure | No IPv4 BFD peers are configured for any VRF. | -| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s). | - | error | show bgp neighbors vrf all has failed: The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model. | -| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | success | - | +| s1-spine1 | AAA | VerifyAcctDefaultMethods | Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA default accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AAA | VerifyAuthenMethods | Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x). | - | failure | AAA authentication methods are not configured for login console | +| s1-spine1 | AAA | VerifyAuthzMethods | Verifies the AAA authorization method lists for different authorization types (commands, exec). | - | failure | AAA authorization methods local, none, logging are not matching for commands, exec | +| s1-spine1 | AAA | VerifyTacacsServerGroups | Verifies if the provided TACACS server group(s) are configured. | - | failure | No TACACS server group(s) are configured | +| s1-spine1 | AAA | VerifyTacacsServers | Verifies TACACS servers are configured for a specified VRF. | - | failure | No TACACS servers are configured | +| s1-spine1 | AAA | VerifyTacacsSourceIntf | Verifies TACACS source-interface for a specified VRF. | - | failure | VRF: MGMT Source Interface: Management0 - Not configured | +| s1-spine1 | AVT | VerifyAVTPathHealth | Verifies the status of all AVT paths for all VRFs. | - | skipped | VerifyAVTPathHealth test is not supported on cEOSLab | +| s1-spine1 | AVT | VerifyAVTRole | Verifies the AVT role of a device. | - | skipped | VerifyAVTRole test is not supported on cEOSLab | +| s1-spine1 | AVT | VerifyAVTSpecificPath | Verifies the Adaptive Virtual Topology (AVT) path. | - | skipped | VerifyAVTSpecificPath test is not supported on cEOSLab | +| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | success | - | +| s1-spine1 | BFD | VerifyBFDPeersIntervals | Verifies the timers of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDPeersRegProtocols | Verifies the registered routing protocol of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDSpecificPeers | Verifies the state of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies the advertised communities of BGP peers. | - | failure | Peer: 172.30.11.17 VRF: default - Not found
Peer: 172.30.11.21 VRF: MGMT - Not found
Peer: fd00:dc:1::1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPExchangedRoutes | Verifies the advertised and received routes of BGP IPv4 peer(s). | - | failure | Peer: 172.30.255.5 VRF: default Advertised route: 192.0.254.5/32 - Not found
Peer: 172.30.255.5 VRF: default Received route: 192.0.255.4/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.255.1/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.254.5/32 - Not found | +| s1-spine1 | BGP | VerifyBGPNlriAcceptance | Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers. | - | failure | Peer: 10.100.0.128 VRF: default - Not found
Peer: 2001:db8:1::2 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPPeerASNCap | Verifies the four octet ASN capability of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerCount | Verifies the count of BGP peers for given address families. | - | failure | AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured
AFI: ipv4 SAFI: multicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPPeerDropStats | Verifies BGP NLRI drop statistics of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerGroup | Verifies BGP peer group of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerMD5Auth | Verifies the MD5 authentication and state of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - Session does not have MD5 authentication enabled | +| s1-spine1 | BGP | VerifyBGPPeerMPCaps | Verifies the multiprotocol capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - ipv4MplsLabels not found
Interface: Ethernet1 VRF: default - ipv4MplsVpn not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteLimit | Verifies maximum routes and warning limit for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteRefreshCap | Verifies the route refresh capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSession | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.0.2 VRF: default - Not found
Peer: 10.1.255.2 VRF: DEV - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Vlan3499 VRF: PROD - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSessionRibd | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerTtlMultiHops | Verifies BGP TTL and max-ttl-hops count for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.2 VRF: test - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerUpdateErrors | Verifies BGP update error counters of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeersHealth | Verifies the health of BGP peers for given address families. | - | failure | AFI: ipv6 SAFI: unicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPPeersHealthRibd | Verifies the health of all the BGP peers. | - | success | - | +| s1-spine1 | BGP | VerifyBGPRedistribution | Verifies BGP redistribution. | - | error | show bgp instance vrf all has failed: Invalid requested version for ModelMetaClass: no such revision 4, most current is 3 | +| s1-spine1 | BGP | VerifyBGPRouteECMP | Verifies BGP IPv4 route ECMP paths. | - | success | - | +| s1-spine1 | BGP | VerifyBGPRoutePaths | Verifies BGP IPv4 route paths. | - | success | - | +| s1-spine1 | BGP | VerifyBGPSpecificPeers | Verifies the health of specific BGP peer(s) for given address families. | - | failure | AFI: evpn Peer: 10.1.0.1 - Not configured
AFI: evpn Peer: 10.1.0.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.254.1 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.0 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.4 - Not configured | +| s1-spine1 | BGP | VerifyBGPTimers | Verifies the timers of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBgpRouteMaps | Verifies BGP inbound and outbound route-maps of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyEVPNType2Route | Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI. | - | failure | Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route
Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No EVPN Type-2 route | +| s1-spine1 | BGP | VerifyEVPNType5Routes | Verifies EVPN Type-5 routes for given IP prefixes and VNIs. | - | failure | Prefix: 192.168.10.0/24 VNI: 10 - No EVPN Type-5 routes found
Prefix: 192.168.20.0/24 VNI: 20 - No EVPN Type-5 routes found
Prefix: 192.168.30.0/24 VNI: 30 - No EVPN Type-5 routes found
Prefix: 192.168.40.0/24 VNI: 40 - No EVPN Type-5 routes found | +| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | failure | --- flash:/startup-config
+++ system:/running-config
@@ -18,6 +18,7 @@
hostname leaf1-dc1
ip name-server vrf MGMT 10.14.0.1
dns domain fun.aristanetworks.com
+ip host vitthal 192.168.66.220
!
platform tfa
personality arfa
| +| s1-spine1 | Configuration | VerifyRunningConfigLines | Search the Running-Config for the given RegEx patterns. | - | failure | Following patterns were not found: '^enable password.*$', 'bla bla' | +| s1-spine1 | Configuration | VerifyZeroTouch | Verifies ZeroTouch is disabled. | - | success | - | | s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3
Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 | -| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | error | show cvx connections brief has failed: Unavailable command (controller not ready) (at token 2: 'connections') | -| s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab. | -| s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab. | +| s1-spine1 | Connectivity | VerifyReachability | Test network reachability to one or many destination IP(s). | - | success | - | +| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | failure | 'show cvx connections brief' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'connections') | +| s1-spine1 | Cvx | VerifyCVXClusterStatus | Verifies the CVX Server Cluster status. | - | failure | CVX Server status is not enabled
CVX Server is not a cluster | +| s1-spine1 | Cvx | VerifyManagementCVX | Verifies the management CVX global status. | - | failure | Management CVX status is not valid: Expected: enabled Actual: disabled | +| s1-spine1 | Cvx | VerifyMcsClientMounts | Verify if all MCS client mounts are in mountStateMountComplete. | - | failure | MCS Client mount states are not present | +| s1-spine1 | Cvx | VerifyMcsServerMounts | Verify if all MCS server mounts are in a MountComplete state. | - | failure | 'show cvx mounts' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'mounts') | +| s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab | +| s1-spine1 | Field Notices | VerifyFieldNotice72Resolution | Verifies if the device is exposed to FN0072, and if the issue has been mitigated. | - | skipped | VerifyFieldNotice72Resolution test is not supported on cEOSLab | +| s1-spine1 | Flow Tracking | VerifyHardwareFlowTrackerStatus | Verifies the hardware flow tracking state. | - | skipped | VerifyHardwareFlowTrackerStatus test is not supported on cEOSLab | +| s1-spine1 | Greent | VerifyGreenT | Verifies if a GreenT policy other than the default is created. | - | failure | No GreenT policy is created | +| s1-spine1 | Greent | VerifyGreenTCounters | Verifies if the GreenT counters are incremented. | - | failure | GreenT counters are not incremented | +| s1-spine1 | Hardware | VerifyAdverseDrops | Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches. | - | skipped | VerifyAdverseDrops test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentCooling | Verifies the status of power supply fans and all fan trays. | - | skipped | VerifyEnvironmentCooling test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentPower | Verifies the power supplies status. | - | skipped | VerifyEnvironmentPower test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentSystemCooling | Verifies the device's system cooling status. | - | skipped | VerifyEnvironmentSystemCooling test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTransceiversManufacturers | Verifies if all the transceivers come from approved manufacturers. | - | skipped | VerifyTransceiversManufacturers test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTransceiversTemperature | Verifies if all the transceivers are operating at an acceptable temperature. | - | skipped | VerifyTransceiversTemperature test is not supported on cEOSLab | | s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled
Interface: Ethernet2 - Proxy-ARP disabled | +| s1-spine1 | Interfaces | VerifyIllegalLACP | Verifies there are no illegal LACP packets in all port channels. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceErrDisabled | Verifies there are no interfaces in the errdisabled state. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceErrors | Verifies that the interfaces error counters are equal to zero. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceIPv4 | Verifies the interface IPv4 addresses. | - | failure | Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.1/31 Actual: 10.100.0.11/31
Interface: Ethernet2 - Secondary IP address is not configured | +| s1-spine1 | Interfaces | VerifyInterfaceUtilization | Verifies that the utilization of interfaces is below a certain threshold. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfacesSpeed | Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. | - | failure | Interface: Ethernet2 - Bandwidth mismatch - Expected: 10.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Bandwidth mismatch - Expected: 100.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Auto-negotiation mismatch - Expected: success Actual: unknown
Interface: Ethernet3 - Data lanes count mismatch - Expected: 1 Actual: 0
Interface: Ethernet2 - Bandwidth mismatch - Expected: 2.5Gbps Actual: 1Gbps | +| s1-spine1 | Interfaces | VerifyInterfacesStatus | Verifies the operational states of specified interfaces to ensure they match expected configurations. | interface | failure | Port-Channel100 - Not configured
Ethernet49/1 - Not configured | +| s1-spine1 | Interfaces | VerifyIpVirtualRouterMac | Verifies the IP virtual router MAC address. | - | success | - | +| s1-spine1 | Interfaces | VerifyL2MTU | Verifies the global L2 MTU of all L2 interfaces. | - | failure | Interface: Ethernet3 - Incorrect MTU configured - Expected: 1500 Actual: 9214
Interface: Port-Channel5 - Incorrect MTU configured - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyL3MTU | Verifies the global L3 MTU of all L3 interfaces. | - | failure | Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Ethernet1 - Incorrect MTU - Expected: 2500 Actual: 9214
Interface: Loopback0 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Loopback1 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Vlan1006 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan1199 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan4093 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan4094 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3019 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3009 - Incorrect MTU - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyLACPInterfacesStatus | Verifies the Link Aggregation Control Protocol (LACP) status of the interface. | - | failure | Interface: Ethernet1 Port-Channel: Port-Channel100 - Not configured | +| s1-spine1 | Interfaces | VerifyLoopbackCount | Verifies the number of loopback interfaces and their status. | - | failure | Loopback interface(s) count mismatch: Expected 3 Actual: 2 | +| s1-spine1 | Interfaces | VerifyPortChannels | Verifies there are no inactive ports in all port channels. | - | success | - | +| s1-spine1 | Interfaces | VerifySVI | Verifies the status of all SVIs. | - | success | - | +| s1-spine1 | Interfaces | VerifyStormControlDrops | Verifies there are no interface storm-control drop counters. | - | skipped | VerifyStormControlDrops test is not supported on cEOSLab | +| s1-spine1 | ISIS | VerifyISISGracefulRestart | Verifies the IS-IS graceful restart feature. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS | VerifyISISInterfaceMode | Verifies IS-IS interfaces are running in the correct mode. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS | VerifyISISNeighborCount | Verifies the number of IS-IS neighbors per interface and level. | - | skipped | IS-IS not configured | | s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | - | skipped | IS-IS not configured | -| s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab. | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingAdjacencySegments | Verifies IS-IS segment routing adjacency segments. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingDataplane | Verifies IS-IS segment routing data-plane configuration. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingTunnels | Verify ISIS-SR tunnels computed by device. | - | skipped | IS-IS-SR not configured | +| s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab | +| s1-spine1 | Logging | VerifyLoggingAccounting | Verifies if AAA accounting logs are generated. | - | success | - | +| s1-spine1 | Logging | VerifyLoggingEntries | Verifies that the expected log string is present in the last specified log messages. | - | failure | Pattern: `.*ACCOUNTING-5-EXEC: cvpadmin ssh.*` - Not found in last 30 alerts log entries
Pattern: `.*SPANTREE-6-INTERFACE_ADD:.*` - Not found in last 10 critical log entries | +| s1-spine1 | Logging | VerifyLoggingErrors | Verifies there are no syslog messages with a severity of ERRORS or higher. | - | failure | Device has reported syslog messages with a severity of ERRORS or higher:
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF data AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:29 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: received from neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes

| +| s1-spine1 | Logging | VerifyLoggingHostname | Verifies if logs are generated with the device FQDN. | - | failure | Logs are not generated with the device FQDN | | s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default | +| s1-spine1 | Logging | VerifyLoggingLogsGeneration | Verifies if logs are generated. | - | success | - | +| s1-spine1 | Logging | VerifyLoggingPersistent | Verifies if logging persistent is enabled and logs are saved in flash. | - | failure | Persistent logging is disabled | +| s1-spine1 | Logging | VerifyLoggingSourceIntf | Verifies logging source-interface for a specified VRF. | - | failure | Source-interface: Management0 VRF: default - Not configured | +| s1-spine1 | Logging | VerifyLoggingTimestamp | Verifies if logs are generated with the appropriate timestamp. | - | failure | Logs are not generated with the appropriate timestamp format | +| s1-spine1 | Logging | VerifySyslogLogging | Verifies if syslog logging is enabled. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagConfigSanity | Verifies there are no MLAG config-sanity inconsistencies. | - | success | - | | s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled | -| s1-spine1 | OSPF | VerifyOSPFMaxLSA | Verifies all OSPF instances did not cross the maximum LSA threshold. | - | skipped | No OSPF instance found. | -| s1-spine1 | Path-Selection | VerifyPathsHealth | Verifies the path and telemetry state of all paths under router path-selection. | - | skipped | VerifyPathsHealth test is not supported on cEOSLab. | -| s1-spine1 | Profiles | VerifyTcamProfile | Verifies the device TCAM profile. | - | skipped | VerifyTcamProfile test is not supported on cEOSLab. | -| s1-spine1 | PTP | VerifyPtpGMStatus | Verifies that the device is locked to a valid PTP Grandmaster. | - | skipped | VerifyPtpGMStatus test is not supported on cEOSLab. | +| s1-spine1 | MLAG | VerifyMlagInterfaces | Verifies there are no inactive or active-partial MLAG ports. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagPrimaryPriority | Verifies the configuration of the MLAG primary priority. | - | failure | MLAG primary priority mismatch - Expected: 3276 Actual: 32767 | +| s1-spine1 | MLAG | VerifyMlagReloadDelay | Verifies the reload-delay parameters of the MLAG configuration. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagStatus | Verifies the health status of the MLAG configuration. | - | success | - | +| s1-spine1 | Multicast | VerifyIGMPSnoopingGlobal | Verifies the IGMP snooping global status. | - | success | - | +| s1-spine1 | Multicast | VerifyIGMPSnoopingVlans | Verifies the IGMP snooping status for the provided VLANs. | - | failure | VLAN10 - Incorrect IGMP state - Expected: disabled Actual: enabled
Supplied vlan 12 is not present on the device | +| s1-spine1 | OSPF | VerifyOSPFMaxLSA | Verifies all OSPF instances did not cross the maximum LSA threshold. | - | skipped | OSPF not configured | +| s1-spine1 | OSPF | VerifyOSPFNeighborCount | Verifies the number of OSPF neighbors in FULL state is the one we expect. | - | skipped | OSPF not configured | +| s1-spine1 | OSPF | VerifyOSPFNeighborState | Verifies all OSPF neighbors are in FULL state. | - | skipped | OSPF not configured | +| s1-spine1 | Path-Selection | VerifyPathsHealth | Verifies the path and telemetry state of all paths under router path-selection. | - | skipped | VerifyPathsHealth test is not supported on cEOSLab | +| s1-spine1 | Path-Selection | VerifySpecificPath | Verifies the DPS path and telemetry state of an IPv4 peer. | - | skipped | VerifySpecificPath test is not supported on cEOSLab | +| s1-spine1 | Profiles | VerifyTcamProfile | Verifies the device TCAM profile. | - | skipped | VerifyTcamProfile test is not supported on cEOSLab | +| s1-spine1 | Profiles | VerifyUnifiedForwardingTableMode | Verifies the device is using the expected UFT mode. | - | skipped | VerifyUnifiedForwardingTableMode test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpGMStatus | Verifies that the device is locked to a valid PTP Grandmaster. | - | skipped | VerifyPtpGMStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpLockStatus | Verifies that the device was locked to the upstream PTP GM in the last minute. | - | skipped | VerifyPtpLockStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpModeStatus | Verifies that the device is configured as a PTP Boundary Clock. | - | skipped | VerifyPtpModeStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpOffset | Verifies that the PTP timing offset is within +/- 1000ns from the master clock. | - | skipped | VerifyPtpOffset test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpPortModeStatus | Verifies the PTP interfaces state. | - | skipped | VerifyPtpPortModeStatus test is not supported on cEOSLab | | s1-spine1 | Routing | VerifyIPv4RouteNextHops | Verifies the next-hops of the IPv4 prefixes. | - | success | - | -| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Expected '# 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.
' as the login banner, but found '' instead. | -| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | Expected '# 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.
' as the motd banner, but found '' instead. | +| s1-spine1 | Routing | VerifyIPv4RouteType | Verifies the route-type of the IPv4 prefixes. | - | failure | Prefix: 10.100.0.12/31 VRF: default - Route not found
Prefix: 10.100.1.5/32 VRF: default - Incorrect route type - Expected: iBGP Actual: connected | +| s1-spine1 | Routing | VerifyRoutingProtocolModel | Verifies the configured routing protocol model. | - | success | - | +| s1-spine1 | Routing | VerifyRoutingStatus | Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549). | - | failure | IPv6 unicast routing enabled status mismatch - Expected: True Actual: False | +| s1-spine1 | Routing | VerifyRoutingTableEntry | Verifies that the provided routes are present in the routing table of a specified VRF. | - | failure | The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.2 | +| s1-spine1 | Routing | VerifyRoutingTableSize | Verifies the size of the IP routing table of the default VRF. | - | failure | Routing table routes are outside the routes range - Expected: 2 <= to >= 20 Actual: 35 | +| s1-spine1 | Security | VerifyAPIHttpStatus | Verifies if eAPI HTTP server is disabled globally. | - | success | - | +| s1-spine1 | Security | VerifyAPIHttpsSSL | Verifies if the eAPI has a valid SSL profile. | - | failure | eAPI HTTPS server SSL profile default is not configured | +| s1-spine1 | Security | VerifyAPIIPv4Acl | Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyAPIIPv6Acl | Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyAPISSLCertificate | Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size. | - | success | - | +| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Login banner is not configured | +| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | MOTD banner is not configured | +| s1-spine1 | Security | VerifyHardwareEntropy | Verifies hardware entropy generation is enabled on device. | - | failure | Hardware entropy generation is disabled | +| s1-spine1 | Security | VerifyIPSecConnHealth | Verifies all IPv4 security connections. | - | failure | No IPv4 security connection configured | +| s1-spine1 | Security | VerifyIPv4ACL | Verifies the configuration of IPv4 ACLs. | - | failure | ACL name: LabTest - Not configured | +| s1-spine1 | Security | VerifySSHIPv4Acl | Verifies if the SSHD agent has IPv4 ACL(s) configured. | - | failure | VRF: default - SSH IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHIPv6Acl | Verifies if the SSHD agent has IPv6 ACL(s) configured. | - | failure | VRF: default - SSH IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHStatus | Verifies if the SSHD agent is disabled in the default VRF. | - | failure | SSHD status for Default VRF is enabled | +| s1-spine1 | Security | VerifySpecificIPSecConn | Verifies the IPv4 security connections. | - | failure | Peer: 10.255.0.1 VRF: default - Not configured
Peer: 10.255.0.2 VRF: default - Not configured | +| s1-spine1 | Security | VerifyTelnetStatus | Verifies if Telnet is disabled in the default VRF. | - | success | - | +| s1-spine1 | Services | VerifyDNSLookup | Verifies the DNS name to IP address resolution. | - | success | - | +| s1-spine1 | Services | VerifyDNSServers | Verifies if the DNS (Domain Name Service) servers are correctly configured. | - | failure | Server 10.14.0.1 VRF: default Priority: 1 - Not configured
Server 10.14.0.11 VRF: MGMT Priority: 0 - Not configured | +| s1-spine1 | Services | VerifyErrdisableRecovery | Verifies the error disable recovery functionality. | - | failure | Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300
Reason: bpduguard Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300 | | s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 | -| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured. | +| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured | +| s1-spine1 | SNMP | VerifySnmpErrorCounters | Verifies the SNMP error counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpGroup | Verifies the SNMP group configurations for specified version(s). | - | failure | Group: Group1 Version: v1 - Not configured
Group: Group2 Version: v3 - Not configured | +| s1-spine1 | SNMP | VerifySnmpHostLogging | Verifies SNMP logging configurations. | - | failure | SNMP logging is disabled | +| s1-spine1 | SNMP | VerifySnmpIPv4Acl | Verifies if the SNMP agent has IPv4 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv4 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpIPv6Acl | Verifies if the SNMP agent has IPv6 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv6 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpLocation | Verifies the SNMP location of a device. | - | failure | SNMP location is not configured | +| s1-spine1 | SNMP | VerifySnmpNotificationHost | Verifies the SNMP notification host(s) (SNMP manager) configurations. | - | failure | No SNMP host is configured | +| s1-spine1 | SNMP | VerifySnmpPDUCounters | Verifies the SNMP PDU counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpSourceInterface | Verifies SNMP source interfaces. | - | failure | SNMP source interface(s) not configured | +| s1-spine1 | SNMP | VerifySnmpStatus | Verifies if the SNMP agent is enabled. | - | failure | VRF: default - SNMP agent disabled | +| s1-spine1 | SNMP | VerifySnmpUser | Verifies the SNMP user configurations. | - | failure | User: test Group: test_group Version: v3 - Not found | +| s1-spine1 | Software | VerifyEOSExtensions | Verifies that all EOS extensions installed on the device are enabled for boot persistence. | - | success | - | | s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F | +| s1-spine1 | Software | VerifyTerminAttrVersion | Verifies the TerminAttr version of the device. | - | failure | TerminAttr version mismatch - Actual: v1.29.0 not in Expected: v1.13.6, v1.8.0 | | s1-spine1 | STP | VerifySTPBlockedPorts | Verifies there is no STP blocked ports. | - | success | - | -| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found. | -| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found.
Client 100.64.3.2 Port: 4500 - STUN client translation not found. | -| s1-spine1 | System | VerifyNTPAssociations | Verifies the Network Time Protocol (NTP) associations. | - | failure | NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured
NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured
NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured | +| s1-spine1 | STP | VerifySTPCounters | Verifies there is no errors in STP BPDU packets. | - | success | - | +| s1-spine1 | STP | VerifySTPDisabledVlans | Verifies the STP disabled VLAN(s). | - | failure | VLAN: 6 - Not configured | +| s1-spine1 | STP | VerifySTPForwardingPorts | Verifies that all interfaces are forwarding for a provided list of VLAN(s). | - | success | - | +| s1-spine1 | STP | VerifySTPMode | Verifies the configured STP mode for a provided list of VLAN(s). | - | failure | VLAN 10 - Incorrect STP mode - Expected: rapidPvst Actual: mstp
VLAN 20 - Incorrect STP mode - Expected: rapidPvst Actual: mstp | +| s1-spine1 | STP | VerifySTPRootPriority | Verifies the STP root priority for a provided list of VLAN or MST instance ID(s). | - | failure | Instance: MST10 - Not configured
Instance: MST20 - Not configured | +| s1-spine1 | STP | VerifyStpTopologyChanges | Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. | - | success | - | +| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found
Client 100.64.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunServer | Verifies the STUN server status is enabled and running. | - | failure | STUN server status is disabled and not running | +| s1-spine1 | System | VerifyAgentLogs | Verifies there are no agent crash reports. | - | success | - | +| s1-spine1 | System | VerifyCPUUtilization | Verifies whether the CPU utilization is below 75%. | - | success | - | +| s1-spine1 | System | VerifyCoredump | Verifies there are no core dump files. | - | success | - | +| s1-spine1 | System | VerifyFileSystemUtilization | Verifies that no partition is utilizing more than 75% of its disk space. | - | success | - | +| s1-spine1 | System | VerifyMaintenance | Verifies that the device is not currently under or entering maintenance. | - | success | - | +| s1-spine1 | System | VerifyNTP | Verifies if NTP is synchronised. | - | success | - | +| s1-spine1 | System | VerifyReloadCause | Verifies the last reload cause of the device. | reload-cause | success | - | +| s1-spine1 | System | VerifyUptime | Verifies the device uptime. | - | success | - | | s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync | -| s1-spine1 | VXLAN | VerifyVxlan1ConnSettings | Verifies the interface vxlan1 source interface and UDP port. | - | success | - | +| s1-spine1 | VLAN | VerifyVlanInternalPolicy | Verifies the VLAN internal allocation policy and the range of VLANs. | - | failure | VLAN internal allocation policy: ascending - Incorrect end VLAN id configured - Expected: 4094 Actual: 1199 | +| s1-spine1 | VLAN | VerifyVlanStatus | Verifies the administrative status of specified VLANs. | - | failure | VLAN: Vlan10 - Incorrect administrative status - Expected: suspended Actual: active | +| s1-spine1 | VXLAN | VerifyVxlan1ConnSettings | Verifies Vxlan1 source interface and UDP port. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlan1Interface | Verifies the Vxlan1 interface status. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlanConfigSanity | Verifies there are no VXLAN config-sanity inconsistencies. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlanVniBinding | Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface. | - | failure | Interface: Vxlan1 VNI: 500 - Binding not found | +| s1-spine1 | VXLAN | VerifyVxlanVtep | Verifies Vxlan1 VTEP peers. | - | failure | The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6
Unexpected VTEP peer(s) on Vxlan1 interface: 10.100.2.3 | diff --git a/tests/data/test_md_report_custom_sections.md b/tests/data/test_md_report_custom_sections.md new file mode 100644 index 0000000..dbc1623 --- /dev/null +++ b/tests/data/test_md_report_custom_sections.md @@ -0,0 +1,355 @@ +# ANTA Report + +**Table of Contents:** + +- [ANTA Report](#anta-report) + - [Test Results Summary](#test-results-summary) + - [Summary Totals](#summary-totals) + - [Summary Totals Device Under Test](#summary-totals-device-under-test) + - [Summary Totals Per Category](#summary-totals-per-category) + - [Test Results](#test-results) + +## Test Results Summary + +### Summary Totals + +| Total Tests | Total Tests Success | Total Tests Skipped | Total Tests Failure | Total Tests Error | +| ----------- | ------------------- | ------------------- | ------------------- | ------------------| +| 181 | 43 | 34 | 103 | 1 | + +### Summary Totals Device Under Test + +| Device Under Test | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | Categories Skipped | Categories Failed | +| ------------------| ----------- | ------------- | ------------- | ------------- | ----------- | -------------------| ------------------| +| s1-spine1 | 181 | 43 | 34 | 103 | 1 | AVT, Field Notices, Flow Tracking, Hardware, ISIS, Interfaces, LANZ, OSPF, PTP, Path-Selection, Profiles, Segment-Routing | AAA, BFD, BGP, Configuration, Connectivity, Cvx, Greent, Interfaces, Logging, MLAG, Multicast, Routing, SNMP, STP, STUN, Security, Services, Software, VLAN, VXLAN | + +### Summary Totals Per Category + +| Test Category | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | +| ------------- | ----------- | ------------- | ------------- | ------------- | ----------- | +| AAA | 7 | 0 | 0 | 7 | 0 | +| AVT | 3 | 0 | 3 | 0 | 0 | +| BFD | 4 | 1 | 0 | 3 | 0 | +| BGP | 25 | 3 | 0 | 21 | 1 | +| Configuration | 3 | 1 | 0 | 2 | 0 | +| Connectivity | 2 | 1 | 0 | 1 | 0 | +| Cvx | 5 | 0 | 0 | 5 | 0 | +| Field Notices | 2 | 0 | 2 | 0 | 0 | +| Flow Tracking | 1 | 0 | 1 | 0 | 0 | +| Greent | 2 | 0 | 0 | 2 | 0 | +| Hardware | 7 | 0 | 7 | 0 | 0 | +| Interfaces | 16 | 7 | 1 | 8 | 0 | +| ISIS | 7 | 0 | 7 | 0 | 0 | +| LANZ | 1 | 0 | 1 | 0 | 0 | +| Logging | 10 | 3 | 0 | 7 | 0 | +| MLAG | 6 | 4 | 0 | 2 | 0 | +| Multicast | 2 | 1 | 0 | 1 | 0 | +| OSPF | 3 | 0 | 3 | 0 | 0 | +| Path-Selection | 2 | 0 | 2 | 0 | 0 | +| Profiles | 2 | 0 | 2 | 0 | 0 | +| PTP | 5 | 0 | 5 | 0 | 0 | +| Routing | 6 | 2 | 0 | 4 | 0 | +| Security | 15 | 3 | 0 | 12 | 0 | +| Segment-Routing | 3 | 0 | 3 | 0 | 0 | +| Services | 4 | 1 | 0 | 3 | 0 | +| SNMP | 12 | 0 | 0 | 12 | 0 | +| Software | 3 | 1 | 0 | 2 | 0 | +| STP | 7 | 4 | 0 | 3 | 0 | +| STUN | 3 | 0 | 0 | 3 | 0 | +| System | 8 | 8 | 0 | 0 | 0 | +| VLAN | 3 | 0 | 0 | 3 | 0 | +| VXLAN | 5 | 3 | 0 | 2 | 0 | + +## Failed Test Results Summary + +| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages | +| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- | +| s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AAA | VerifyAcctDefaultMethods | Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA default accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AAA | VerifyAuthenMethods | Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x). | - | failure | AAA authentication methods are not configured for login console | +| s1-spine1 | AAA | VerifyAuthzMethods | Verifies the AAA authorization method lists for different authorization types (commands, exec). | - | failure | AAA authorization methods local, none, logging are not matching for commands, exec | +| s1-spine1 | AAA | VerifyTacacsServerGroups | Verifies if the provided TACACS server group(s) are configured. | - | failure | No TACACS server group(s) are configured | +| s1-spine1 | AAA | VerifyTacacsServers | Verifies TACACS servers are configured for a specified VRF. | - | failure | No TACACS servers are configured | +| s1-spine1 | AAA | VerifyTacacsSourceIntf | Verifies TACACS source-interface for a specified VRF. | - | failure | VRF: MGMT Source Interface: Management0 - Not configured | +| s1-spine1 | BFD | VerifyBFDPeersIntervals | Verifies the timers of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDPeersRegProtocols | Verifies the registered routing protocol of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDSpecificPeers | Verifies the state of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies the advertised communities of BGP peers. | - | failure | Peer: 172.30.11.17 VRF: default - Not found
Peer: 172.30.11.21 VRF: MGMT - Not found
Peer: fd00:dc:1::1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPExchangedRoutes | Verifies the advertised and received routes of BGP IPv4 peer(s). | - | failure | Peer: 172.30.255.5 VRF: default Advertised route: 192.0.254.5/32 - Not found
Peer: 172.30.255.5 VRF: default Received route: 192.0.255.4/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.255.1/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.254.5/32 - Not found | +| s1-spine1 | BGP | VerifyBGPNlriAcceptance | Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers. | - | failure | Peer: 10.100.0.128 VRF: default - Not found
Peer: 2001:db8:1::2 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPPeerASNCap | Verifies the four octet ASN capability of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerCount | Verifies the count of BGP peers for given address families. | - | failure | AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured
AFI: ipv4 SAFI: multicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPPeerDropStats | Verifies BGP NLRI drop statistics of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerGroup | Verifies BGP peer group of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerMD5Auth | Verifies the MD5 authentication and state of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - Session does not have MD5 authentication enabled | +| s1-spine1 | BGP | VerifyBGPPeerMPCaps | Verifies the multiprotocol capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - ipv4MplsLabels not found
Interface: Ethernet1 VRF: default - ipv4MplsVpn not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteLimit | Verifies maximum routes and warning limit for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteRefreshCap | Verifies the route refresh capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSession | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.0.2 VRF: default - Not found
Peer: 10.1.255.2 VRF: DEV - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Vlan3499 VRF: PROD - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSessionRibd | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerTtlMultiHops | Verifies BGP TTL and max-ttl-hops count for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.2 VRF: test - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerUpdateErrors | Verifies BGP update error counters of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeersHealth | Verifies the health of BGP peers for given address families. | - | failure | AFI: ipv6 SAFI: unicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPSpecificPeers | Verifies the health of specific BGP peer(s) for given address families. | - | failure | AFI: evpn Peer: 10.1.0.1 - Not configured
AFI: evpn Peer: 10.1.0.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.254.1 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.0 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.4 - Not configured | +| s1-spine1 | BGP | VerifyBGPTimers | Verifies the timers of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBgpRouteMaps | Verifies BGP inbound and outbound route-maps of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyEVPNType2Route | Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI. | - | failure | Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route
Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No EVPN Type-2 route | +| s1-spine1 | BGP | VerifyEVPNType5Routes | Verifies EVPN Type-5 routes for given IP prefixes and VNIs. | - | failure | Prefix: 192.168.10.0/24 VNI: 10 - No EVPN Type-5 routes found
Prefix: 192.168.20.0/24 VNI: 20 - No EVPN Type-5 routes found
Prefix: 192.168.30.0/24 VNI: 30 - No EVPN Type-5 routes found
Prefix: 192.168.40.0/24 VNI: 40 - No EVPN Type-5 routes found | +| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | failure | --- flash:/startup-config
+++ system:/running-config
@@ -18,6 +18,7 @@
hostname leaf1-dc1
ip name-server vrf MGMT 10.14.0.1
dns domain fun.aristanetworks.com
+ip host vitthal 192.168.66.220
!
platform tfa
personality arfa
| +| s1-spine1 | Configuration | VerifyRunningConfigLines | Search the Running-Config for the given RegEx patterns. | - | failure | Following patterns were not found: '^enable password.*$', 'bla bla' | +| s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3
Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 | +| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | failure | 'show cvx connections brief' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'connections') | +| s1-spine1 | Cvx | VerifyCVXClusterStatus | Verifies the CVX Server Cluster status. | - | failure | CVX Server status is not enabled
CVX Server is not a cluster | +| s1-spine1 | Cvx | VerifyManagementCVX | Verifies the management CVX global status. | - | failure | Management CVX status is not valid: Expected: enabled Actual: disabled | +| s1-spine1 | Cvx | VerifyMcsClientMounts | Verify if all MCS client mounts are in mountStateMountComplete. | - | failure | MCS Client mount states are not present | +| s1-spine1 | Cvx | VerifyMcsServerMounts | Verify if all MCS server mounts are in a MountComplete state. | - | failure | 'show cvx mounts' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'mounts') | +| s1-spine1 | Greent | VerifyGreenT | Verifies if a GreenT policy other than the default is created. | - | failure | No GreenT policy is created | +| s1-spine1 | Greent | VerifyGreenTCounters | Verifies if the GreenT counters are incremented. | - | failure | GreenT counters are not incremented | +| s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled
Interface: Ethernet2 - Proxy-ARP disabled | +| s1-spine1 | Interfaces | VerifyInterfaceIPv4 | Verifies the interface IPv4 addresses. | - | failure | Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.1/31 Actual: 10.100.0.11/31
Interface: Ethernet2 - Secondary IP address is not configured | +| s1-spine1 | Interfaces | VerifyInterfacesSpeed | Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. | - | failure | Interface: Ethernet2 - Bandwidth mismatch - Expected: 10.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Bandwidth mismatch - Expected: 100.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Auto-negotiation mismatch - Expected: success Actual: unknown
Interface: Ethernet3 - Data lanes count mismatch - Expected: 1 Actual: 0
Interface: Ethernet2 - Bandwidth mismatch - Expected: 2.5Gbps Actual: 1Gbps | +| s1-spine1 | Interfaces | VerifyInterfacesStatus | Verifies the operational states of specified interfaces to ensure they match expected configurations. | interface | failure | Port-Channel100 - Not configured
Ethernet49/1 - Not configured | +| s1-spine1 | Interfaces | VerifyL2MTU | Verifies the global L2 MTU of all L2 interfaces. | - | failure | Interface: Ethernet3 - Incorrect MTU configured - Expected: 1500 Actual: 9214
Interface: Port-Channel5 - Incorrect MTU configured - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyL3MTU | Verifies the global L3 MTU of all L3 interfaces. | - | failure | Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Ethernet1 - Incorrect MTU - Expected: 2500 Actual: 9214
Interface: Loopback0 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Loopback1 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Vlan1006 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan1199 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan4093 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan4094 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3019 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3009 - Incorrect MTU - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyLACPInterfacesStatus | Verifies the Link Aggregation Control Protocol (LACP) status of the interface. | - | failure | Interface: Ethernet1 Port-Channel: Port-Channel100 - Not configured | +| s1-spine1 | Interfaces | VerifyLoopbackCount | Verifies the number of loopback interfaces and their status. | - | failure | Loopback interface(s) count mismatch: Expected 3 Actual: 2 | +| s1-spine1 | Logging | VerifyLoggingEntries | Verifies that the expected log string is present in the last specified log messages. | - | failure | Pattern: `.*ACCOUNTING-5-EXEC: cvpadmin ssh.*` - Not found in last 30 alerts log entries
Pattern: `.*SPANTREE-6-INTERFACE_ADD:.*` - Not found in last 10 critical log entries | +| s1-spine1 | Logging | VerifyLoggingErrors | Verifies there are no syslog messages with a severity of ERRORS or higher. | - | failure | Device has reported syslog messages with a severity of ERRORS or higher:
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF data AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:29 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: received from neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes

| +| s1-spine1 | Logging | VerifyLoggingHostname | Verifies if logs are generated with the device FQDN. | - | failure | Logs are not generated with the device FQDN | +| s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default | +| s1-spine1 | Logging | VerifyLoggingPersistent | Verifies if logging persistent is enabled and logs are saved in flash. | - | failure | Persistent logging is disabled | +| s1-spine1 | Logging | VerifyLoggingSourceIntf | Verifies logging source-interface for a specified VRF. | - | failure | Source-interface: Management0 VRF: default - Not configured | +| s1-spine1 | Logging | VerifyLoggingTimestamp | Verifies if logs are generated with the appropriate timestamp. | - | failure | Logs are not generated with the appropriate timestamp format | +| s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled | +| s1-spine1 | MLAG | VerifyMlagPrimaryPriority | Verifies the configuration of the MLAG primary priority. | - | failure | MLAG primary priority mismatch - Expected: 3276 Actual: 32767 | +| s1-spine1 | Multicast | VerifyIGMPSnoopingVlans | Verifies the IGMP snooping status for the provided VLANs. | - | failure | VLAN10 - Incorrect IGMP state - Expected: disabled Actual: enabled
Supplied vlan 12 is not present on the device | +| s1-spine1 | Routing | VerifyIPv4RouteType | Verifies the route-type of the IPv4 prefixes. | - | failure | Prefix: 10.100.0.12/31 VRF: default - Route not found
Prefix: 10.100.1.5/32 VRF: default - Incorrect route type - Expected: iBGP Actual: connected | +| s1-spine1 | Routing | VerifyRoutingStatus | Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549). | - | failure | IPv6 unicast routing enabled status mismatch - Expected: True Actual: False | +| s1-spine1 | Routing | VerifyRoutingTableEntry | Verifies that the provided routes are present in the routing table of a specified VRF. | - | failure | The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.2 | +| s1-spine1 | Routing | VerifyRoutingTableSize | Verifies the size of the IP routing table of the default VRF. | - | failure | Routing table routes are outside the routes range - Expected: 2 <= to >= 20 Actual: 35 | +| s1-spine1 | Security | VerifyAPIHttpsSSL | Verifies if the eAPI has a valid SSL profile. | - | failure | eAPI HTTPS server SSL profile default is not configured | +| s1-spine1 | Security | VerifyAPIIPv4Acl | Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyAPIIPv6Acl | Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Login banner is not configured | +| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | MOTD banner is not configured | +| s1-spine1 | Security | VerifyHardwareEntropy | Verifies hardware entropy generation is enabled on device. | - | failure | Hardware entropy generation is disabled | +| s1-spine1 | Security | VerifyIPSecConnHealth | Verifies all IPv4 security connections. | - | failure | No IPv4 security connection configured | +| s1-spine1 | Security | VerifyIPv4ACL | Verifies the configuration of IPv4 ACLs. | - | failure | ACL name: LabTest - Not configured | +| s1-spine1 | Security | VerifySSHIPv4Acl | Verifies if the SSHD agent has IPv4 ACL(s) configured. | - | failure | VRF: default - SSH IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHIPv6Acl | Verifies if the SSHD agent has IPv6 ACL(s) configured. | - | failure | VRF: default - SSH IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHStatus | Verifies if the SSHD agent is disabled in the default VRF. | - | failure | SSHD status for Default VRF is enabled | +| s1-spine1 | Security | VerifySpecificIPSecConn | Verifies the IPv4 security connections. | - | failure | Peer: 10.255.0.1 VRF: default - Not configured
Peer: 10.255.0.2 VRF: default - Not configured | +| s1-spine1 | Services | VerifyDNSServers | Verifies if the DNS (Domain Name Service) servers are correctly configured. | - | failure | Server 10.14.0.1 VRF: default Priority: 1 - Not configured
Server 10.14.0.11 VRF: MGMT Priority: 0 - Not configured | +| s1-spine1 | Services | VerifyErrdisableRecovery | Verifies the error disable recovery functionality. | - | failure | Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300
Reason: bpduguard Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300 | +| s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 | +| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured | +| s1-spine1 | SNMP | VerifySnmpErrorCounters | Verifies the SNMP error counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpGroup | Verifies the SNMP group configurations for specified version(s). | - | failure | Group: Group1 Version: v1 - Not configured
Group: Group2 Version: v3 - Not configured | +| s1-spine1 | SNMP | VerifySnmpHostLogging | Verifies SNMP logging configurations. | - | failure | SNMP logging is disabled | +| s1-spine1 | SNMP | VerifySnmpIPv4Acl | Verifies if the SNMP agent has IPv4 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv4 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpIPv6Acl | Verifies if the SNMP agent has IPv6 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv6 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpLocation | Verifies the SNMP location of a device. | - | failure | SNMP location is not configured | +| s1-spine1 | SNMP | VerifySnmpNotificationHost | Verifies the SNMP notification host(s) (SNMP manager) configurations. | - | failure | No SNMP host is configured | +| s1-spine1 | SNMP | VerifySnmpPDUCounters | Verifies the SNMP PDU counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpSourceInterface | Verifies SNMP source interfaces. | - | failure | SNMP source interface(s) not configured | +| s1-spine1 | SNMP | VerifySnmpStatus | Verifies if the SNMP agent is enabled. | - | failure | VRF: default - SNMP agent disabled | +| s1-spine1 | SNMP | VerifySnmpUser | Verifies the SNMP user configurations. | - | failure | User: test Group: test_group Version: v3 - Not found | +| s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F | +| s1-spine1 | Software | VerifyTerminAttrVersion | Verifies the TerminAttr version of the device. | - | failure | TerminAttr version mismatch - Actual: v1.29.0 not in Expected: v1.13.6, v1.8.0 | +| s1-spine1 | STP | VerifySTPDisabledVlans | Verifies the STP disabled VLAN(s). | - | failure | VLAN: 6 - Not configured | +| s1-spine1 | STP | VerifySTPMode | Verifies the configured STP mode for a provided list of VLAN(s). | - | failure | VLAN 10 - Incorrect STP mode - Expected: rapidPvst Actual: mstp
VLAN 20 - Incorrect STP mode - Expected: rapidPvst Actual: mstp | +| s1-spine1 | STP | VerifySTPRootPriority | Verifies the STP root priority for a provided list of VLAN or MST instance ID(s). | - | failure | Instance: MST10 - Not configured
Instance: MST20 - Not configured | +| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found
Client 100.64.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunServer | Verifies the STUN server status is enabled and running. | - | failure | STUN server status is disabled and not running | +| s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync | +| s1-spine1 | VLAN | VerifyVlanInternalPolicy | Verifies the VLAN internal allocation policy and the range of VLANs. | - | failure | VLAN internal allocation policy: ascending - Incorrect end VLAN id configured - Expected: 4094 Actual: 1199 | +| s1-spine1 | VLAN | VerifyVlanStatus | Verifies the administrative status of specified VLANs. | - | failure | VLAN: Vlan10 - Incorrect administrative status - Expected: suspended Actual: active | +| s1-spine1 | VXLAN | VerifyVxlanVniBinding | Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface. | - | failure | Interface: Vxlan1 VNI: 500 - Binding not found | +| s1-spine1 | VXLAN | VerifyVxlanVtep | Verifies Vxlan1 VTEP peers. | - | failure | The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6
Unexpected VTEP peer(s) on Vxlan1 interface: 10.100.2.3 | + +## Test Results + +| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages | +| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- | +| s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AAA | VerifyAcctDefaultMethods | Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA default accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AAA | VerifyAuthenMethods | Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x). | - | failure | AAA authentication methods are not configured for login console | +| s1-spine1 | AAA | VerifyAuthzMethods | Verifies the AAA authorization method lists for different authorization types (commands, exec). | - | failure | AAA authorization methods local, none, logging are not matching for commands, exec | +| s1-spine1 | AAA | VerifyTacacsServerGroups | Verifies if the provided TACACS server group(s) are configured. | - | failure | No TACACS server group(s) are configured | +| s1-spine1 | AAA | VerifyTacacsServers | Verifies TACACS servers are configured for a specified VRF. | - | failure | No TACACS servers are configured | +| s1-spine1 | AAA | VerifyTacacsSourceIntf | Verifies TACACS source-interface for a specified VRF. | - | failure | VRF: MGMT Source Interface: Management0 - Not configured | +| s1-spine1 | AVT | VerifyAVTPathHealth | Verifies the status of all AVT paths for all VRFs. | - | skipped | VerifyAVTPathHealth test is not supported on cEOSLab | +| s1-spine1 | AVT | VerifyAVTRole | Verifies the AVT role of a device. | - | skipped | VerifyAVTRole test is not supported on cEOSLab | +| s1-spine1 | AVT | VerifyAVTSpecificPath | Verifies the Adaptive Virtual Topology (AVT) path. | - | skipped | VerifyAVTSpecificPath test is not supported on cEOSLab | +| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | success | - | +| s1-spine1 | BFD | VerifyBFDPeersIntervals | Verifies the timers of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDPeersRegProtocols | Verifies the registered routing protocol of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BFD | VerifyBFDSpecificPeers | Verifies the state of IPv4 BFD peer sessions. | - | failure | Peer: 192.0.255.8 VRF: default - Not found
Peer: 192.0.255.7 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies the advertised communities of BGP peers. | - | failure | Peer: 172.30.11.17 VRF: default - Not found
Peer: 172.30.11.21 VRF: MGMT - Not found
Peer: fd00:dc:1::1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPExchangedRoutes | Verifies the advertised and received routes of BGP IPv4 peer(s). | - | failure | Peer: 172.30.255.5 VRF: default Advertised route: 192.0.254.5/32 - Not found
Peer: 172.30.255.5 VRF: default Received route: 192.0.255.4/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.255.1/32 - Not found
Peer: 172.30.255.1 VRF: default Advertised route: 192.0.254.5/32 - Not found | +| s1-spine1 | BGP | VerifyBGPNlriAcceptance | Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers. | - | failure | Peer: 10.100.0.128 VRF: default - Not found
Peer: 2001:db8:1::2 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found
Peer: fe80::2%Et1 VRF: default - Not found | +| s1-spine1 | BGP | VerifyBGPPeerASNCap | Verifies the four octet ASN capability of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerCount | Verifies the count of BGP peers for given address families. | - | failure | AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured
AFI: ipv4 SAFI: multicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPPeerDropStats | Verifies BGP NLRI drop statistics of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerGroup | Verifies BGP peer group of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerMD5Auth | Verifies the MD5 authentication and state of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - Session does not have MD5 authentication enabled | +| s1-spine1 | BGP | VerifyBGPPeerMPCaps | Verifies the multiprotocol capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: default - ipv4MplsLabels not found
Interface: Ethernet1 VRF: default - ipv4MplsVpn not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteLimit | Verifies maximum routes and warning limit for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerRouteRefreshCap | Verifies the route refresh capabilities of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSession | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.0.2 VRF: default - Not found
Peer: 10.1.255.2 VRF: DEV - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Vlan3499 VRF: PROD - Not found | +| s1-spine1 | BGP | VerifyBGPPeerSessionRibd | Verifies the session state of BGP peers. | - | failure | Peer: 10.1.0.1 VRF: default - Not found
Peer: 10.1.255.4 VRF: DEV - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerTtlMultiHops | Verifies BGP TTL and max-ttl-hops count for BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.2 VRF: test - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeerUpdateErrors | Verifies BGP update error counters of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBGPPeersHealth | Verifies the health of BGP peers for given address families. | - | failure | AFI: ipv6 SAFI: unicast VRF: DEV - VRF not configured | +| s1-spine1 | BGP | VerifyBGPPeersHealthRibd | Verifies the health of all the BGP peers. | - | success | - | +| s1-spine1 | BGP | VerifyBGPRedistribution | Verifies BGP redistribution. | - | error | show bgp instance vrf all has failed: Invalid requested version for ModelMetaClass: no such revision 4, most current is 3 | +| s1-spine1 | BGP | VerifyBGPRouteECMP | Verifies BGP IPv4 route ECMP paths. | - | success | - | +| s1-spine1 | BGP | VerifyBGPRoutePaths | Verifies BGP IPv4 route paths. | - | success | - | +| s1-spine1 | BGP | VerifyBGPSpecificPeers | Verifies the health of specific BGP peer(s) for given address families. | - | failure | AFI: evpn Peer: 10.1.0.1 - Not configured
AFI: evpn Peer: 10.1.0.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.254.1 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.0 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.2 - Not configured
AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.4 - Not configured | +| s1-spine1 | BGP | VerifyBGPTimers | Verifies the timers of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: 172.30.11.5 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyBgpRouteMaps | Verifies BGP inbound and outbound route-maps of BGP peers. | - | failure | Peer: 172.30.11.1 VRF: default - Not found
Peer: fd00:dc:1::1 VRF: default - Not found
Interface: Ethernet1 VRF: MGMT - Not found | +| s1-spine1 | BGP | VerifyEVPNType2Route | Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI. | - | failure | Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route
Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No EVPN Type-2 route | +| s1-spine1 | BGP | VerifyEVPNType5Routes | Verifies EVPN Type-5 routes for given IP prefixes and VNIs. | - | failure | Prefix: 192.168.10.0/24 VNI: 10 - No EVPN Type-5 routes found
Prefix: 192.168.20.0/24 VNI: 20 - No EVPN Type-5 routes found
Prefix: 192.168.30.0/24 VNI: 30 - No EVPN Type-5 routes found
Prefix: 192.168.40.0/24 VNI: 40 - No EVPN Type-5 routes found | +| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | failure | --- flash:/startup-config
+++ system:/running-config
@@ -18,6 +18,7 @@
hostname leaf1-dc1
ip name-server vrf MGMT 10.14.0.1
dns domain fun.aristanetworks.com
+ip host vitthal 192.168.66.220
!
platform tfa
personality arfa
| +| s1-spine1 | Configuration | VerifyRunningConfigLines | Search the Running-Config for the given RegEx patterns. | - | failure | Following patterns were not found: '^enable password.*$', 'bla bla' | +| s1-spine1 | Configuration | VerifyZeroTouch | Verifies ZeroTouch is disabled. | - | success | - | +| s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3
Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 | +| s1-spine1 | Connectivity | VerifyReachability | Test network reachability to one or many destination IP(s). | - | success | - | +| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | failure | 'show cvx connections brief' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'connections') | +| s1-spine1 | Cvx | VerifyCVXClusterStatus | Verifies the CVX Server Cluster status. | - | failure | CVX Server status is not enabled
CVX Server is not a cluster | +| s1-spine1 | Cvx | VerifyManagementCVX | Verifies the management CVX global status. | - | failure | Management CVX status is not valid: Expected: enabled Actual: disabled | +| s1-spine1 | Cvx | VerifyMcsClientMounts | Verify if all MCS client mounts are in mountStateMountComplete. | - | failure | MCS Client mount states are not present | +| s1-spine1 | Cvx | VerifyMcsServerMounts | Verify if all MCS server mounts are in a MountComplete state. | - | failure | 'show cvx mounts' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'mounts') | +| s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab | +| s1-spine1 | Field Notices | VerifyFieldNotice72Resolution | Verifies if the device is exposed to FN0072, and if the issue has been mitigated. | - | skipped | VerifyFieldNotice72Resolution test is not supported on cEOSLab | +| s1-spine1 | Flow Tracking | VerifyHardwareFlowTrackerStatus | Verifies the hardware flow tracking state. | - | skipped | VerifyHardwareFlowTrackerStatus test is not supported on cEOSLab | +| s1-spine1 | Greent | VerifyGreenT | Verifies if a GreenT policy other than the default is created. | - | failure | No GreenT policy is created | +| s1-spine1 | Greent | VerifyGreenTCounters | Verifies if the GreenT counters are incremented. | - | failure | GreenT counters are not incremented | +| s1-spine1 | Hardware | VerifyAdverseDrops | Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches. | - | skipped | VerifyAdverseDrops test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentCooling | Verifies the status of power supply fans and all fan trays. | - | skipped | VerifyEnvironmentCooling test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentPower | Verifies the power supplies status. | - | skipped | VerifyEnvironmentPower test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyEnvironmentSystemCooling | Verifies the device's system cooling status. | - | skipped | VerifyEnvironmentSystemCooling test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTransceiversManufacturers | Verifies if all the transceivers come from approved manufacturers. | - | skipped | VerifyTransceiversManufacturers test is not supported on cEOSLab | +| s1-spine1 | Hardware | VerifyTransceiversTemperature | Verifies if all the transceivers are operating at an acceptable temperature. | - | skipped | VerifyTransceiversTemperature test is not supported on cEOSLab | +| s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled
Interface: Ethernet2 - Proxy-ARP disabled | +| s1-spine1 | Interfaces | VerifyIllegalLACP | Verifies there are no illegal LACP packets in all port channels. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceErrDisabled | Verifies there are no interfaces in the errdisabled state. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceErrors | Verifies that the interfaces error counters are equal to zero. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfaceIPv4 | Verifies the interface IPv4 addresses. | - | failure | Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.1/31 Actual: 10.100.0.11/31
Interface: Ethernet2 - Secondary IP address is not configured | +| s1-spine1 | Interfaces | VerifyInterfaceUtilization | Verifies that the utilization of interfaces is below a certain threshold. | - | success | - | +| s1-spine1 | Interfaces | VerifyInterfacesSpeed | Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. | - | failure | Interface: Ethernet2 - Bandwidth mismatch - Expected: 10.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Bandwidth mismatch - Expected: 100.0Gbps Actual: 1Gbps
Interface: Ethernet3 - Auto-negotiation mismatch - Expected: success Actual: unknown
Interface: Ethernet3 - Data lanes count mismatch - Expected: 1 Actual: 0
Interface: Ethernet2 - Bandwidth mismatch - Expected: 2.5Gbps Actual: 1Gbps | +| s1-spine1 | Interfaces | VerifyInterfacesStatus | Verifies the operational states of specified interfaces to ensure they match expected configurations. | interface | failure | Port-Channel100 - Not configured
Ethernet49/1 - Not configured | +| s1-spine1 | Interfaces | VerifyIpVirtualRouterMac | Verifies the IP virtual router MAC address. | - | success | - | +| s1-spine1 | Interfaces | VerifyL2MTU | Verifies the global L2 MTU of all L2 interfaces. | - | failure | Interface: Ethernet3 - Incorrect MTU configured - Expected: 1500 Actual: 9214
Interface: Port-Channel5 - Incorrect MTU configured - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyL3MTU | Verifies the global L3 MTU of all L3 interfaces. | - | failure | Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Ethernet1 - Incorrect MTU - Expected: 2500 Actual: 9214
Interface: Loopback0 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Loopback1 - Incorrect MTU - Expected: 1500 Actual: 65535
Interface: Vlan1006 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan1199 - Incorrect MTU - Expected: 1500 Actual: 9164
Interface: Vlan4093 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan4094 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3019 - Incorrect MTU - Expected: 1500 Actual: 9214
Interface: Vlan3009 - Incorrect MTU - Expected: 1500 Actual: 9214 | +| s1-spine1 | Interfaces | VerifyLACPInterfacesStatus | Verifies the Link Aggregation Control Protocol (LACP) status of the interface. | - | failure | Interface: Ethernet1 Port-Channel: Port-Channel100 - Not configured | +| s1-spine1 | Interfaces | VerifyLoopbackCount | Verifies the number of loopback interfaces and their status. | - | failure | Loopback interface(s) count mismatch: Expected 3 Actual: 2 | +| s1-spine1 | Interfaces | VerifyPortChannels | Verifies there are no inactive ports in all port channels. | - | success | - | +| s1-spine1 | Interfaces | VerifySVI | Verifies the status of all SVIs. | - | success | - | +| s1-spine1 | Interfaces | VerifyStormControlDrops | Verifies there are no interface storm-control drop counters. | - | skipped | VerifyStormControlDrops test is not supported on cEOSLab | +| s1-spine1 | ISIS | VerifyISISGracefulRestart | Verifies the IS-IS graceful restart feature. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS | VerifyISISInterfaceMode | Verifies IS-IS interfaces are running in the correct mode. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS | VerifyISISNeighborCount | Verifies the number of IS-IS neighbors per interface and level. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingAdjacencySegments | Verifies IS-IS segment routing adjacency segments. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingDataplane | Verifies IS-IS segment routing data-plane configuration. | - | skipped | IS-IS not configured | +| s1-spine1 | ISIS, Segment-Routing | VerifyISISSegmentRoutingTunnels | Verify ISIS-SR tunnels computed by device. | - | skipped | IS-IS-SR not configured | +| s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab | +| s1-spine1 | Logging | VerifyLoggingAccounting | Verifies if AAA accounting logs are generated. | - | success | - | +| s1-spine1 | Logging | VerifyLoggingEntries | Verifies that the expected log string is present in the last specified log messages. | - | failure | Pattern: `.*ACCOUNTING-5-EXEC: cvpadmin ssh.*` - Not found in last 30 alerts log entries
Pattern: `.*SPANTREE-6-INTERFACE_ADD:.*` - Not found in last 10 critical log entries | +| s1-spine1 | Logging | VerifyLoggingErrors | Verifies there are no syslog messages with a severity of ERRORS or higher. | - | failure | Device has reported syslog messages with a severity of ERRORS or higher:
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF data AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes
Apr 29 08:01:29 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: received from neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes

| +| s1-spine1 | Logging | VerifyLoggingHostname | Verifies if logs are generated with the device FQDN. | - | failure | Logs are not generated with the device FQDN | +| s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default | +| s1-spine1 | Logging | VerifyLoggingLogsGeneration | Verifies if logs are generated. | - | success | - | +| s1-spine1 | Logging | VerifyLoggingPersistent | Verifies if logging persistent is enabled and logs are saved in flash. | - | failure | Persistent logging is disabled | +| s1-spine1 | Logging | VerifyLoggingSourceIntf | Verifies logging source-interface for a specified VRF. | - | failure | Source-interface: Management0 VRF: default - Not configured | +| s1-spine1 | Logging | VerifyLoggingTimestamp | Verifies if logs are generated with the appropriate timestamp. | - | failure | Logs are not generated with the appropriate timestamp format | +| s1-spine1 | Logging | VerifySyslogLogging | Verifies if syslog logging is enabled. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagConfigSanity | Verifies there are no MLAG config-sanity inconsistencies. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled | +| s1-spine1 | MLAG | VerifyMlagInterfaces | Verifies there are no inactive or active-partial MLAG ports. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagPrimaryPriority | Verifies the configuration of the MLAG primary priority. | - | failure | MLAG primary priority mismatch - Expected: 3276 Actual: 32767 | +| s1-spine1 | MLAG | VerifyMlagReloadDelay | Verifies the reload-delay parameters of the MLAG configuration. | - | success | - | +| s1-spine1 | MLAG | VerifyMlagStatus | Verifies the health status of the MLAG configuration. | - | success | - | +| s1-spine1 | Multicast | VerifyIGMPSnoopingGlobal | Verifies the IGMP snooping global status. | - | success | - | +| s1-spine1 | Multicast | VerifyIGMPSnoopingVlans | Verifies the IGMP snooping status for the provided VLANs. | - | failure | VLAN10 - Incorrect IGMP state - Expected: disabled Actual: enabled
Supplied vlan 12 is not present on the device | +| s1-spine1 | OSPF | VerifyOSPFMaxLSA | Verifies all OSPF instances did not cross the maximum LSA threshold. | - | skipped | OSPF not configured | +| s1-spine1 | OSPF | VerifyOSPFNeighborCount | Verifies the number of OSPF neighbors in FULL state is the one we expect. | - | skipped | OSPF not configured | +| s1-spine1 | OSPF | VerifyOSPFNeighborState | Verifies all OSPF neighbors are in FULL state. | - | skipped | OSPF not configured | +| s1-spine1 | Path-Selection | VerifyPathsHealth | Verifies the path and telemetry state of all paths under router path-selection. | - | skipped | VerifyPathsHealth test is not supported on cEOSLab | +| s1-spine1 | Path-Selection | VerifySpecificPath | Verifies the DPS path and telemetry state of an IPv4 peer. | - | skipped | VerifySpecificPath test is not supported on cEOSLab | +| s1-spine1 | Profiles | VerifyTcamProfile | Verifies the device TCAM profile. | - | skipped | VerifyTcamProfile test is not supported on cEOSLab | +| s1-spine1 | Profiles | VerifyUnifiedForwardingTableMode | Verifies the device is using the expected UFT mode. | - | skipped | VerifyUnifiedForwardingTableMode test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpGMStatus | Verifies that the device is locked to a valid PTP Grandmaster. | - | skipped | VerifyPtpGMStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpLockStatus | Verifies that the device was locked to the upstream PTP GM in the last minute. | - | skipped | VerifyPtpLockStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpModeStatus | Verifies that the device is configured as a PTP Boundary Clock. | - | skipped | VerifyPtpModeStatus test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpOffset | Verifies that the PTP timing offset is within +/- 1000ns from the master clock. | - | skipped | VerifyPtpOffset test is not supported on cEOSLab | +| s1-spine1 | PTP | VerifyPtpPortModeStatus | Verifies the PTP interfaces state. | - | skipped | VerifyPtpPortModeStatus test is not supported on cEOSLab | +| s1-spine1 | Routing | VerifyIPv4RouteNextHops | Verifies the next-hops of the IPv4 prefixes. | - | success | - | +| s1-spine1 | Routing | VerifyIPv4RouteType | Verifies the route-type of the IPv4 prefixes. | - | failure | Prefix: 10.100.0.12/31 VRF: default - Route not found
Prefix: 10.100.1.5/32 VRF: default - Incorrect route type - Expected: iBGP Actual: connected | +| s1-spine1 | Routing | VerifyRoutingProtocolModel | Verifies the configured routing protocol model. | - | success | - | +| s1-spine1 | Routing | VerifyRoutingStatus | Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549). | - | failure | IPv6 unicast routing enabled status mismatch - Expected: True Actual: False | +| s1-spine1 | Routing | VerifyRoutingTableEntry | Verifies that the provided routes are present in the routing table of a specified VRF. | - | failure | The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.2 | +| s1-spine1 | Routing | VerifyRoutingTableSize | Verifies the size of the IP routing table of the default VRF. | - | failure | Routing table routes are outside the routes range - Expected: 2 <= to >= 20 Actual: 35 | +| s1-spine1 | Security | VerifyAPIHttpStatus | Verifies if eAPI HTTP server is disabled globally. | - | success | - | +| s1-spine1 | Security | VerifyAPIHttpsSSL | Verifies if the eAPI has a valid SSL profile. | - | failure | eAPI HTTPS server SSL profile default is not configured | +| s1-spine1 | Security | VerifyAPIIPv4Acl | Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyAPIIPv6Acl | Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF. | - | failure | VRF: default - eAPI IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifyAPISSLCertificate | Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size. | - | success | - | +| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Login banner is not configured | +| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | MOTD banner is not configured | +| s1-spine1 | Security | VerifyHardwareEntropy | Verifies hardware entropy generation is enabled on device. | - | failure | Hardware entropy generation is disabled | +| s1-spine1 | Security | VerifyIPSecConnHealth | Verifies all IPv4 security connections. | - | failure | No IPv4 security connection configured | +| s1-spine1 | Security | VerifyIPv4ACL | Verifies the configuration of IPv4 ACLs. | - | failure | ACL name: LabTest - Not configured | +| s1-spine1 | Security | VerifySSHIPv4Acl | Verifies if the SSHD agent has IPv4 ACL(s) configured. | - | failure | VRF: default - SSH IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHIPv6Acl | Verifies if the SSHD agent has IPv6 ACL(s) configured. | - | failure | VRF: default - SSH IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0 | +| s1-spine1 | Security | VerifySSHStatus | Verifies if the SSHD agent is disabled in the default VRF. | - | failure | SSHD status for Default VRF is enabled | +| s1-spine1 | Security | VerifySpecificIPSecConn | Verifies the IPv4 security connections. | - | failure | Peer: 10.255.0.1 VRF: default - Not configured
Peer: 10.255.0.2 VRF: default - Not configured | +| s1-spine1 | Security | VerifyTelnetStatus | Verifies if Telnet is disabled in the default VRF. | - | success | - | +| s1-spine1 | Services | VerifyDNSLookup | Verifies the DNS name to IP address resolution. | - | success | - | +| s1-spine1 | Services | VerifyDNSServers | Verifies if the DNS (Domain Name Service) servers are correctly configured. | - | failure | Server 10.14.0.1 VRF: default Priority: 1 - Not configured
Server 10.14.0.11 VRF: MGMT Priority: 0 - Not configured | +| s1-spine1 | Services | VerifyErrdisableRecovery | Verifies the error disable recovery functionality. | - | failure | Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300
Reason: bpduguard Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300 | +| s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 | +| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured | +| s1-spine1 | SNMP | VerifySnmpErrorCounters | Verifies the SNMP error counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpGroup | Verifies the SNMP group configurations for specified version(s). | - | failure | Group: Group1 Version: v1 - Not configured
Group: Group2 Version: v3 - Not configured | +| s1-spine1 | SNMP | VerifySnmpHostLogging | Verifies SNMP logging configurations. | - | failure | SNMP logging is disabled | +| s1-spine1 | SNMP | VerifySnmpIPv4Acl | Verifies if the SNMP agent has IPv4 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv4 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpIPv6Acl | Verifies if the SNMP agent has IPv6 ACL(s) configured. | - | failure | VRF: default - Incorrect SNMP IPv6 ACL(s) - Expected: 3 Actual: 0 | +| s1-spine1 | SNMP | VerifySnmpLocation | Verifies the SNMP location of a device. | - | failure | SNMP location is not configured | +| s1-spine1 | SNMP | VerifySnmpNotificationHost | Verifies the SNMP notification host(s) (SNMP manager) configurations. | - | failure | No SNMP host is configured | +| s1-spine1 | SNMP | VerifySnmpPDUCounters | Verifies the SNMP PDU counters. | - | failure | SNMP counters not found | +| s1-spine1 | SNMP | VerifySnmpSourceInterface | Verifies SNMP source interfaces. | - | failure | SNMP source interface(s) not configured | +| s1-spine1 | SNMP | VerifySnmpStatus | Verifies if the SNMP agent is enabled. | - | failure | VRF: default - SNMP agent disabled | +| s1-spine1 | SNMP | VerifySnmpUser | Verifies the SNMP user configurations. | - | failure | User: test Group: test_group Version: v3 - Not found | +| s1-spine1 | Software | VerifyEOSExtensions | Verifies that all EOS extensions installed on the device are enabled for boot persistence. | - | success | - | +| s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F | +| s1-spine1 | Software | VerifyTerminAttrVersion | Verifies the TerminAttr version of the device. | - | failure | TerminAttr version mismatch - Actual: v1.29.0 not in Expected: v1.13.6, v1.8.0 | +| s1-spine1 | STP | VerifySTPBlockedPorts | Verifies there is no STP blocked ports. | - | success | - | +| s1-spine1 | STP | VerifySTPCounters | Verifies there is no errors in STP BPDU packets. | - | success | - | +| s1-spine1 | STP | VerifySTPDisabledVlans | Verifies the STP disabled VLAN(s). | - | failure | VLAN: 6 - Not configured | +| s1-spine1 | STP | VerifySTPForwardingPorts | Verifies that all interfaces are forwarding for a provided list of VLAN(s). | - | success | - | +| s1-spine1 | STP | VerifySTPMode | Verifies the configured STP mode for a provided list of VLAN(s). | - | failure | VLAN 10 - Incorrect STP mode - Expected: rapidPvst Actual: mstp
VLAN 20 - Incorrect STP mode - Expected: rapidPvst Actual: mstp | +| s1-spine1 | STP | VerifySTPRootPriority | Verifies the STP root priority for a provided list of VLAN or MST instance ID(s). | - | failure | Instance: MST10 - Not configured
Instance: MST20 - Not configured | +| s1-spine1 | STP | VerifyStpTopologyChanges | Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. | - | success | - | +| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found
Client 100.64.3.2 Port: 4500 - STUN client translation not found | +| s1-spine1 | STUN | VerifyStunServer | Verifies the STUN server status is enabled and running. | - | failure | STUN server status is disabled and not running | +| s1-spine1 | System | VerifyAgentLogs | Verifies there are no agent crash reports. | - | success | - | +| s1-spine1 | System | VerifyCPUUtilization | Verifies whether the CPU utilization is below 75%. | - | success | - | +| s1-spine1 | System | VerifyCoredump | Verifies there are no core dump files. | - | success | - | +| s1-spine1 | System | VerifyFileSystemUtilization | Verifies that no partition is utilizing more than 75% of its disk space. | - | success | - | +| s1-spine1 | System | VerifyMaintenance | Verifies that the device is not currently under or entering maintenance. | - | success | - | +| s1-spine1 | System | VerifyNTP | Verifies if NTP is synchronised. | - | success | - | +| s1-spine1 | System | VerifyReloadCause | Verifies the last reload cause of the device. | reload-cause | success | - | +| s1-spine1 | System | VerifyUptime | Verifies the device uptime. | - | success | - | +| s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync | +| s1-spine1 | VLAN | VerifyVlanInternalPolicy | Verifies the VLAN internal allocation policy and the range of VLANs. | - | failure | VLAN internal allocation policy: ascending - Incorrect end VLAN id configured - Expected: 4094 Actual: 1199 | +| s1-spine1 | VLAN | VerifyVlanStatus | Verifies the administrative status of specified VLANs. | - | failure | VLAN: Vlan10 - Incorrect administrative status - Expected: suspended Actual: active | +| s1-spine1 | VXLAN | VerifyVxlan1ConnSettings | Verifies Vxlan1 source interface and UDP port. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlan1Interface | Verifies the Vxlan1 interface status. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlanConfigSanity | Verifies there are no VXLAN config-sanity inconsistencies. | - | success | - | +| s1-spine1 | VXLAN | VerifyVxlanVniBinding | Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface. | - | failure | Interface: Vxlan1 VNI: 500 - Binding not found | +| s1-spine1 | VXLAN | VerifyVxlanVtep | Verifies Vxlan1 VTEP peers. | - | failure | The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6
Unexpected VTEP peer(s) on Vxlan1 interface: 10.100.2.3 | diff --git a/tests/units/anta_tests/__init__.py b/tests/units/anta_tests/__init__.py index 9bfb5f8..a0eb228 100644 --- a/tests/units/anta_tests/__init__.py +++ b/tests/units/anta_tests/__init__.py @@ -3,30 +3,77 @@ # that can be found in the LICENSE file. """Tests for anta.tests module.""" +from __future__ import annotations + import asyncio -from typing import Any +import sys +from typing import TYPE_CHECKING, Any, Literal, TypedDict -from anta.device import AntaDevice +from anta.models import AntaTest + +if TYPE_CHECKING: + from anta.device import AntaDevice + from anta.result_manager.models import AntaTestStatus + + if sys.version_info >= (3, 11): + from typing import NotRequired + else: + from typing_extensions import NotRequired -def test(device: AntaDevice, data: dict[str, Any]) -> None: +class UnitTestResult(TypedDict): + """Expected result of a unit test of an AntaTest subclass. + + For our AntaTest unit tests we expect only success, failure or skipped. + Never unset nor error. + """ + + result: Literal[AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.SKIPPED] + messages: NotRequired[list[str]] + + +class AntaUnitTest(TypedDict): + """The parameters required for a unit test of an AntaTest subclass.""" + + inputs: NotRequired[dict[str, Any]] + eos_data: list[dict[str, Any] | str] + expected: UnitTestResult + + +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + TypeAlias = type + + +AntaUnitTestDataDict: TypeAlias = dict[tuple[type[AntaTest], str], AntaUnitTest] + + +def test(device: AntaDevice, data: tuple[tuple[type[AntaTest], str], AntaUnitTest]) -> None: """Generic test function for AntaTest subclass. Generate unit tests for each AntaTest subclass. See `tests/units/anta_tests/README.md` for more information on how to use it. """ + # Extract the test class, name and test data from a nested tuple structure: + # `val: Tuple[Tuple[Type[AntaTest], str], AntaUnitTest]` + (anta_test, name), test_data = data + # Instantiate the AntaTest subclass - test_instance = data["test"](device, inputs=data["inputs"], eos_data=data["eos_data"]) + test_instance = anta_test(device, inputs=test_data.get("inputs"), eos_data=test_data["eos_data"]) # Run the test() method asyncio.run(test_instance.test()) + # Assert expected result - assert test_instance.result.result == data["expected"]["result"], f"Expected '{data['expected']['result']}' result, got '{test_instance.result.result}'" - if "messages" in data["expected"]: + assert test_instance.result.result == test_data["expected"]["result"], ( + f"Expected '{test_data['expected']['result']}' result, got '{test_instance.result.result}'" + ) + if "messages" in test_data["expected"]: # We expect messages in test result - assert len(test_instance.result.messages) == len(data["expected"]["messages"]) + assert len(test_instance.result.messages) == len(test_data["expected"]["messages"]) # Test will pass if the expected message is included in the test result message - for message, expected in zip(test_instance.result.messages, data["expected"]["messages"]): # NOTE: zip(strict=True) has been added in Python 3.10 + for message, expected in zip(test_instance.result.messages, test_data["expected"]["messages"]): # NOTE: zip(strict=True) has been added in Python 3.10 assert expected in message else: # Test result should not have messages diff --git a/tests/units/anta_tests/conftest.py b/tests/units/anta_tests/conftest.py index 3296430..273f250 100644 --- a/tests/units/anta_tests/conftest.py +++ b/tests/units/anta_tests/conftest.py @@ -7,17 +7,27 @@ from typing import Any import pytest +from anta.models import AntaTest +from tests.units.anta_tests import AntaUnitTest -def build_test_id(val: dict[str, Any]) -> str: + +def build_test_id(val: tuple[tuple[type[AntaTest], str], AntaUnitTest]) -> str: """Build id for a unit test of an AntaTest subclass. { - "name": "meaniful test name", - "test": , + (, "meaniful test name"): + { + "eos_data": [{}], + .... + } ... } """ - return f"{val['test'].__module__}.{val['test'].__name__}-{val['name']}" + # Extract the test class and its name from a nested tuple structure: + # `val: Tuple[Tuple[Type[AntaTest], str], AntaUnitTest]` + (anta_test, test_name) = val[0] + + return f"{anta_test.__module__}.{anta_test.__name__}-{test_name}" def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: @@ -32,4 +42,4 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: """ if "tests.units.anta_tests" in metafunc.module.__package__ and metafunc.function.__name__ == "test": # This is a unit test for an AntaTest subclass - metafunc.parametrize("data", metafunc.module.DATA, ids=build_test_id) + metafunc.parametrize("data", metafunc.module.DATA.items(), ids=build_test_id) diff --git a/tests/units/anta_tests/routing/test_bgp.py b/tests/units/anta_tests/routing/test_bgp.py index 46115e2..71e6963 100644 --- a/tests/units/anta_tests/routing/test_bgp.py +++ b/tests/units/anta_tests/routing/test_bgp.py @@ -6,11 +6,14 @@ # pylint: disable=C0302 from __future__ import annotations +import sys from typing import TYPE_CHECKING, Any import pytest from anta.input_models.routing.bgp import BgpAddressFamily +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.routing.bgp import ( VerifyBGPAdvCommunities, VerifyBGPExchangedRoutes, @@ -40,6 +43,9 @@ from anta.tests.routing.bgp import ( ) from tests.units.anta_tests import test +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + @pytest.mark.parametrize( ("input_dict", "expected"), @@ -57,10 +63,8 @@ def test_check_bgp_neighbor_capability(input_dict: dict[str, bool], expected: bo assert _check_bgp_neighbor_capability(input_dict) == expected -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyBGPPeerCount, +DATA: AntaUnitTestDataDict = { + (VerifyBGPPeerCount, "success"): { "eos_data": [ { "vrfs": { @@ -96,7 +100,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -105,11 +109,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 1}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-peer-state-check-true", - "test": VerifyBGPPeerCount, + (VerifyBGPPeerCount, "success-peer-state-check-true"): { "eos_data": [ { "vrfs": { @@ -160,7 +162,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -169,11 +171,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 1, "check_peer_state": True}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-vrf-not-configured", - "test": VerifyBGPPeerCount, + (VerifyBGPPeerCount, "failure-vrf-not-configured"): { "eos_data": [ { "vrfs": { @@ -224,7 +224,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -233,16 +233,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "PROD", "num_peers": 2, "check_peer_state": True}, ] }, - "expected": { - "result": "failure", - "messages": [ - "AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured"]}, }, - { - "name": "failure-peer-state-check-true", - "test": VerifyBGPPeerCount, + (VerifyBGPPeerCount, "failure-peer-state-check-true"): { "eos_data": [ { "vrfs": { @@ -293,7 +286,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -303,16 +296,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "DEV", "num_peers": 1, "check_peer_state": True}, ] }, - "expected": { - "result": "failure", - "messages": [ - "AFI: vpn-ipv4 - Peer count mismatch - Expected: 2 Actual: 0", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AFI: vpn-ipv4 - Peer count mismatch - Expected: 2 Actual: 0"]}, }, - { - "name": "failure-wrong-count-peer-state-check-true", - "test": VerifyBGPPeerCount, + (VerifyBGPPeerCount, "failure-wrong-count-peer-state-check-true"): { "eos_data": [ { "vrfs": { @@ -363,7 +349,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -373,16 +359,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: evpn - Peer count mismatch - Expected: 3 Actual: 2", "AFI: ipv4 SAFI: unicast VRF: DEV - Peer count mismatch - Expected: 2 Actual: 1", ], }, }, - { - "name": "failure-wrong-count", - "test": VerifyBGPPeerCount, + (VerifyBGPPeerCount, "failure-wrong-count"): { "eos_data": [ { "vrfs": { @@ -396,7 +380,7 @@ DATA: list[dict[str, Any]] = [ "peerAsn": "65100", "ipv4Unicast": {"afiSafiState": "advertised", "nlrisReceived": 0, "nlrisAccepted": 0}, "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 42, "nlrisAccepted": 42}, - }, + } }, }, "DEV": { @@ -412,7 +396,7 @@ DATA: list[dict[str, Any]] = [ }, }, } - }, + } ], "inputs": { "address_families": [ @@ -422,7 +406,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: evpn - Peer count mismatch - Expected: 2 Actual: 1", "AFI: ipv4 SAFI: unicast VRF: default - Peer count mismatch - Expected: 2 Actual: 1", @@ -430,9 +414,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "success"): { "eos_data": [ { "vrfs": { @@ -459,24 +441,16 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } } ], - "inputs": { - "address_families": [ - {"afi": "evpn"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}, - ] - }, - "expected": {"result": "success"}, + "inputs": {"address_families": [{"afi": "evpn"}, {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-min-established-time", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "success-min-established-time"): { "eos_data": [ { "vrfs": { @@ -506,7 +480,7 @@ DATA: list[dict[str, Any]] = [ "establishedTime": 169883, "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -514,22 +488,12 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "minimum_established_time": 10000, - "address_families": [ - {"afi": "evpn"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}, - ], + "address_families": [{"afi": "evpn"}, {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-vrf-not-configured", - "test": VerifyBGPPeersHealth, - "eos_data": [ - { - "vrfs": {}, - } - ], + (VerifyBGPPeersHealth, "failure-vrf-not-configured"): { + "eos_data": [{"vrfs": {}}], "inputs": { "address_families": [ {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, @@ -539,7 +503,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default - VRF not configured", "AFI: ipv4 SAFI: sr-te VRF: MGMT - VRF not configured", @@ -548,9 +512,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-peer-not-found", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "failure-peer-not-found"): { "eos_data": [{"vrfs": {"default": {"peerList": []}, "MGMT": {"peerList": []}}}], "inputs": { "address_families": [ @@ -561,7 +523,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default - No peers found", "AFI: ipv4 SAFI: sr-te VRF: MGMT - No peers found", @@ -570,9 +532,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-session-not-established", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "failure-session-not-established"): { "eos_data": [ { "vrfs": { @@ -601,7 +561,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.12", "state": "Active", "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": True, "received": True, "enabled": True}}}, - }, + } ] }, } @@ -616,7 +576,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - Incorrect session state - Expected: Established Actual: Idle", "AFI: ipv4 SAFI: sr-te VRF: MGMT Peer: 10.100.0.12 - Incorrect session state - Expected: Established Actual: Active", @@ -625,9 +585,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-afi-not-negotiated", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "failure-afi-not-negotiated"): { "eos_data": [ { "vrfs": { @@ -660,7 +618,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": False, "received": False, "enabled": False}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -675,7 +633,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - AFI/SAFI state is not negotiated - Advertised: False, Received: False, Enabled: True", "AFI: ipv4 SAFI: sr-te VRF: MGMT Peer: 10.100.0.12 - AFI/SAFI state is not negotiated - Advertised: False, Received: False, Enabled: False", @@ -684,9 +642,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-tcp-queues", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "failure-tcp-queues"): { "eos_data": [ { "vrfs": { @@ -719,7 +675,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4SrTe": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 1, "inputQueueLength": 5}, - }, + } ] }, } @@ -734,7 +690,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - Session has non-empty message queues - InQ: 2 OutQ: 4", "AFI: ipv4 SAFI: sr-te VRF: MGMT Peer: 10.100.0.12 - Session has non-empty message queues - InQ: 5 OutQ: 1", @@ -743,9 +699,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-min-established-time", - "test": VerifyBGPPeersHealth, + (VerifyBGPPeersHealth, "failure-min-established-time"): { "eos_data": [ { "vrfs": { @@ -775,7 +729,7 @@ DATA: list[dict[str, Any]] = [ "establishedTime": 9883, "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -783,14 +737,10 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "minimum_established_time": 10000, - "address_families": [ - {"afi": "evpn"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, - {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}, - ], + "address_families": [{"afi": "evpn"}, {"afi": "ipv4", "safi": "unicast", "vrf": "default"}, {"afi": "ipv4", "safi": "unicast", "vrf": "DEV"}], }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: evpn Peer: 10.100.0.13 - BGP session not established for the minimum required duration - Expected: 10000s Actual: 9883s", "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - BGP session not established for the minimum required duration - " @@ -800,9 +750,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "success"): { "eos_data": [ { "vrfs": { @@ -829,7 +777,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -842,11 +790,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-min-established-time", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "success-min-established-time"): { "eos_data": [ { "vrfs": { @@ -876,7 +822,7 @@ DATA: list[dict[str, Any]] = [ "establishedTime": 169883, "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -890,11 +836,9 @@ DATA: list[dict[str, Any]] = [ {"afi": "ipv4", "safi": "unicast", "vrf": "MGMT", "peers": ["10.100.0.14"]}, ], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-peer-not-configured", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-peer-not-configured"): { "eos_data": [ { "vrfs": { @@ -915,7 +859,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -929,7 +873,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - Not configured", "AFI: evpn Peer: 10.100.0.13 - Not configured", @@ -937,14 +881,8 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-vrf-not-configured", - "test": VerifyBGPSpecificPeers, - "eos_data": [ - { - "vrfs": {}, - } - ], + (VerifyBGPSpecificPeers, "failure-vrf-not-configured"): { + "eos_data": [{"vrfs": {}}], "inputs": { "address_families": [ {"afi": "ipv4", "safi": "unicast", "peers": ["10.100.0.12"]}, @@ -953,7 +891,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default - VRF not configured", "AFI: evpn - VRF not configured", @@ -961,9 +899,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-session-not-established", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-session-not-established"): { "eos_data": [ { "vrfs": { @@ -982,7 +918,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.14", "state": "Idle", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, - }, + } ] }, } @@ -995,16 +931,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - Incorrect session state - Expected: Established Actual: Idle", "AFI: ipv4 SAFI: unicast VRF: MGMT Peer: 10.100.0.14 - Incorrect session state - Expected: Established Actual: Idle", ], }, }, - { - "name": "failure-afi-safi-not-negotiated", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-afi-safi-not-negotiated"): { "eos_data": [ { "vrfs": { @@ -1025,7 +959,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": False, "received": False, "enabled": False}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -1038,16 +972,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - AFI/SAFI state is not negotiated - Advertised: False, Received: False, Enabled: True", "AFI: ipv4 SAFI: unicast VRF: MGMT Peer: 10.100.0.14 - AFI/SAFI state is not negotiated - Advertised: False, Received: False, Enabled: False", ], }, }, - { - "name": "failure-afi-safi-not-correct", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-afi-safi-not-correct"): { "eos_data": [ { "vrfs": { @@ -1068,7 +1000,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"l2VpnEvpn": {"advertised": False, "received": False, "enabled": False}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -1081,16 +1013,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - AFI/SAFI state is not negotiated", "AFI: ipv4 SAFI: unicast VRF: MGMT Peer: 10.100.0.14 - AFI/SAFI state is not negotiated", ], }, }, - { - "name": "failure-tcp-queues", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-tcp-queues"): { "eos_data": [ { "vrfs": { @@ -1111,7 +1041,7 @@ DATA: list[dict[str, Any]] = [ "state": "Established", "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 2, "inputQueueLength": 2}, - }, + } ] }, } @@ -1124,16 +1054,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - Session has non-empty message queues - InQ: 3 OutQ: 3", "AFI: ipv4 SAFI: unicast VRF: MGMT Peer: 10.100.0.14 - Session has non-empty message queues - InQ: 2 OutQ: 2", ], }, }, - { - "name": "failure-min-established-time", - "test": VerifyBGPSpecificPeers, + (VerifyBGPSpecificPeers, "failure-min-established-time"): { "eos_data": [ { "vrfs": { @@ -1163,7 +1091,7 @@ DATA: list[dict[str, Any]] = [ "establishedTime": 9883, "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, "peerTcpInfo": {"state": "ESTABLISHED", "outputQueueLength": 0, "inputQueueLength": 0}, - }, + } ] }, } @@ -1178,7 +1106,7 @@ DATA: list[dict[str, Any]] = [ ], }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.100.0.12 - BGP session not established for the minimum required duration - " "Expected: 10000s Actual: 9883s", @@ -1188,35 +1116,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPExchangedRoutes, + (VerifyBGPExchangedRoutes, "success"): { "eos_data": [ { "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1224,27 +1132,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1252,27 +1142,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1280,27 +1152,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1321,37 +1175,17 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-advertised-route-validation-only", - "test": VerifyBGPExchangedRoutes, + (VerifyBGPExchangedRoutes, "success-check-active-false"): { "eos_data": [ { "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1359,166 +1193,92 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, ], "inputs": { + "check_active": False, "bgp_peers": [ { "peer_address": "172.30.11.1", "vrf": "default", "advertised_routes": ["192.0.254.5/32", "192.0.254.3/32"], - }, - { - "peer_address": "172.30.11.5", - "vrf": "default", - "advertised_routes": ["192.0.254.3/32", "192.0.254.5/32"], - }, - ] + "received_routes": ["192.0.254.3/32", "192.0.255.4/32"], + } + ], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-routes", - "test": VerifyBGPExchangedRoutes, + (VerifyBGPExchangedRoutes, "success-advertised-route-validation-only"): { "eos_data": [ { "vrfs": { "default": { - "vrf": "default", - "routerId": "192.0.255.1", - "asn": "65001", - "bgpRouteEntries": {}, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, { "vrfs": { "default": { - "vrf": "default", - "routerId": "192.0.255.1", - "asn": "65001", - "bgpRouteEntries": {}, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, { "vrfs": { "default": { - "vrf": "default", - "routerId": "192.0.255.1", - "asn": "65001", - "bgpRouteEntries": {}, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + } } } }, { "vrfs": { "default": { - "vrf": "default", - "routerId": "192.0.255.1", - "asn": "65001", - "bgpRouteEntries": {}, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + } } } }, ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.11", - "vrf": "default", - "advertised_routes": ["192.0.254.3/32"], - "received_routes": ["192.0.255.3/32"], - }, - { - "peer_address": "172.30.11.12", - "vrf": "default", - "advertised_routes": ["192.0.254.31/32"], - "received_routes": ["192.0.255.31/32"], - }, + {"peer_address": "172.30.11.1", "vrf": "default", "advertised_routes": ["192.0.254.5/32", "192.0.254.3/32"]}, + {"peer_address": "172.30.11.5", "vrf": "default", "advertised_routes": ["192.0.254.3/32", "192.0.254.5/32"]}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPExchangedRoutes, "failure-no-routes"): { + "eos_data": [ + {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}}, + {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}}, + {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}}, + {"vrfs": {"default": {"vrf": "default", "routerId": "192.0.255.1", "asn": "65001", "bgpRouteEntries": {}}}}, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.11", "vrf": "default", "advertised_routes": ["192.0.254.3/32"], "received_routes": ["192.0.255.3/32"]}, + {"peer_address": "172.30.11.12", "vrf": "default", "advertised_routes": ["192.0.254.31/32"], "received_routes": ["192.0.255.31/32"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.11 VRF: default Advertised route: 192.0.254.3/32 - Not found", "Peer: 172.30.11.11 VRF: default Received route: 192.0.255.3/32 - Not found", @@ -1527,35 +1287,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-invalid-or-inactive-routes", - "test": VerifyBGPExchangedRoutes, + (VerifyBGPExchangedRoutes, "failure-invalid-or-inactive-routes"): { "eos_data": [ { "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + } } } }, @@ -1563,27 +1303,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + } } } }, @@ -1591,27 +1313,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": False, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": False, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": False}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": False}}]}, + } } } }, @@ -1619,27 +1323,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + } } } }, @@ -1661,7 +1347,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.3/32 - Valid: False Active: True", "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.51/32 - Not found", @@ -1674,35 +1360,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-invalid-or-inactive-routes-as-per-given-input", - "test": VerifyBGPExchangedRoutes, + (VerifyBGPExchangedRoutes, "failure-invalid-or-inactive-routes-as-per-given-input"): { "eos_data": [ { "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": False, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + } } } }, @@ -1710,27 +1376,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - "192.0.254.5/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ] - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1738,27 +1386,9 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": True, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + } } } }, @@ -1766,47 +1396,21 @@ DATA: list[dict[str, Any]] = [ "vrfs": { "default": { "bgpRouteEntries": { - "192.0.254.3/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - "192.0.255.4/32": { - "bgpRoutePaths": [ - { - "routeType": { - "valid": True, - "active": False, - }, - } - ], - }, - }, + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": False}}]}, + } } } }, ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "advertised_routes": ["192.0.254.3/32", "192.0.254.51/32"], - }, - { - "peer_address": "172.30.11.5", - "vrf": "default", - "received_routes": ["192.0.254.3/32", "192.0.255.41/32"], - }, + {"peer_address": "172.30.11.1", "vrf": "default", "advertised_routes": ["192.0.254.3/32", "192.0.254.51/32"]}, + {"peer_address": "172.30.11.5", "vrf": "default", "received_routes": ["192.0.254.3/32", "192.0.255.41/32"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.3/32 - Valid: False Active: True", "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.51/32 - Not found", @@ -1815,139 +1419,50 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPPeerMPCaps, + (VerifyBGPExchangedRoutes, "failure-check-active-false"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsLabels": { - "advertised": True, - "received": True, - "enabled": True, - }, - } - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsVpn": { - "advertised": True, - "received": True, - "enabled": True, - }, - } - }, - } - ] - }, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": False}}]}, + "192.0.254.5/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + } + } } - } - ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"], - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - "capabilities": ["ipv4_Unicast", "ipv4 MplsVpn"], - }, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "failure-no-peer", - "test": VerifyBGPPeerMPCaps, - "eos_data": [ + }, { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, - } - ] - }, + "bgpRouteEntries": { + "192.0.254.3/32": {"bgpRoutePaths": [{"routeType": {"valid": True, "active": True}}]}, + "192.0.255.4/32": {"bgpRoutePaths": [{"routeType": {"valid": False, "active": True}}]}, + } + } } - } + }, ], "inputs": { + "check_active": False, "bgp_peers": [ - { - "peer_address": "172.30.11.10", - "vrf": "default", - "capabilities": ["ipv4Unicast", "l2-vpn-EVPN"], - }, { "peer_address": "172.30.11.1", - "vrf": "MGMT", - "capabilities": ["ipv4Unicast", "l2vpnevpn"], - }, - ] + "vrf": "default", + "advertised_routes": ["192.0.254.5/32", "192.0.254.3/32"], + "received_routes": ["192.0.254.3/32", "192.0.255.4/32"], + } + ], }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Peer: 172.30.11.10 VRF: default - Not found", - "Peer: 172.30.11.1 VRF: MGMT - Not found", + "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.5/32 - Valid: False", + "Peer: 172.30.11.1 VRF: default Advertised route: 192.0.254.3/32 - Valid: False", + "Peer: 172.30.11.1 VRF: default Received route: 192.0.255.4/32 - Valid: False", ], }, }, - { - "name": "failure-missing-capabilities", - "test": VerifyBGPPeerMPCaps, + (VerifyBGPPeerMPCaps, "success"): { "eos_data": [ { "vrfs": { @@ -1957,36 +1472,143 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.1", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, + } }, } ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "172.30.11.10", + "neighborCapabilities": { + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsVpn": {"advertised": True, "received": True, "enabled": True}, + } + }, + } + ] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "capabilities": ["ipv4_Unicast", "ipv4 MplsVpn"]}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerMPCaps, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "fd00:dc:1::1", + "neighborCapabilities": { + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, + } + }, + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": { + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, + } + }, + }, + ] + }, + "MGMT": { + "peerList": [ + { + "neighborCapabilities": { + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsVpn": {"advertised": True, "received": True, "enabled": True}, + } + }, + "ifName": "Ethernet1", + } + ] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"]}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"]}, + {"interface": "Ethernet1", "vrf": "MGMT", "capabilities": ["ipv4_Unicast", "ipv4 MplsVpn"]}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerMPCaps, "failure-no-peer"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "172.30.11.1", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + } + ] + }, + "MGMT": { + "peerList": [ + { + "peerAddress": "172.30.11.10", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + } + ] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.10", "vrf": "default", "capabilities": ["ipv4Unicast", "l2-vpn-EVPN"]}, + {"peer_address": "172.30.11.1", "vrf": "MGMT", "capabilities": ["ipv4Unicast", "l2vpnevpn"]}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.10 VRF: default - Not found", "Peer: 172.30.11.1 VRF: MGMT - Not found"]}, + }, + (VerifyBGPPeerMPCaps, "failure-capabilities-not-found"): { + "eos_data": [{"vrfs": {"default": {"peerList": [{"peerAddress": "172.30.11.1", "neighborCapabilities": {}}]}}}], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["ipv4Unicast", "l2-vpn-EVPN"]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.1 VRF: default - Multiprotocol capabilities not found"]}, + }, + (VerifyBGPPeerMPCaps, "failure-missing-capabilities"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "172.30.11.1", + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + } + ] } } } ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "capabilities": ["ipv4 Unicast", "L2VpnEVPN"], - } - ] - }, - "expected": { - "result": "failure", - "messages": ["Peer: 172.30.11.1 VRF: default - l2VpnEvpn not found"], - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["ipv4 Unicast", "L2VpnEVPN"]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.1 VRF: default - l2VpnEvpn not found"]}, }, - { - "name": "failure-incorrect-capabilities", - "test": VerifyBGPPeerMPCaps, + (VerifyBGPPeerMPCaps, "failure-incorrect-capabilities"): { "eos_data": [ { "vrfs": { @@ -1996,17 +1618,9 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.1", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": False, - "received": False, - "enabled": False, - }, - "ipv4MplsVpn": { - "advertised": False, - "received": True, - "enabled": False, - }, - }, + "ipv4Unicast": {"advertised": False, "received": False, "enabled": False}, + "ipv4MplsVpn": {"advertised": False, "received": True, "enabled": False}, + } }, } ] @@ -2017,34 +1631,18 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.10", "neighborCapabilities": { "multiprotocolCaps": { - "l2VpnEvpn": { - "advertised": True, - "received": False, - "enabled": False, - }, - "ipv4MplsVpn": { - "advertised": False, - "received": False, - "enabled": True, - }, - }, + "l2VpnEvpn": {"advertised": True, "received": False, "enabled": False}, + "ipv4MplsVpn": {"advertised": False, "received": False, "enabled": True}, + } }, }, { "peerAddress": "172.30.11.11", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": False, - "received": False, - "enabled": False, - }, - "ipv4MplsVpn": { - "advertised": False, - "received": False, - "enabled": False, - }, - }, + "ipv4Unicast": {"advertised": False, "received": False, "enabled": False}, + "ipv4MplsVpn": {"advertised": False, "received": False, "enabled": False}, + } }, }, ] @@ -2054,25 +1652,13 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "capabilities": ["ipv4 unicast", "ipv4 mpls vpn", "L2 vpn EVPN"], - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - "capabilities": ["ipv4_unicast", "ipv4 mplsvpn", "L2vpnEVPN"], - }, - { - "peer_address": "172.30.11.11", - "vrf": "MGMT", - "capabilities": ["Ipv4 Unicast", "ipv4 MPLSVPN", "L2 vpnEVPN"], - }, + {"peer_address": "172.30.11.1", "vrf": "default", "capabilities": ["ipv4 unicast", "ipv4 mpls vpn", "L2 vpn EVPN"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "capabilities": ["ipv4_unicast", "ipv4 mplsvpn", "L2vpnEVPN"]}, + {"peer_address": "172.30.11.11", "vrf": "MGMT", "capabilities": ["Ipv4 Unicast", "ipv4 MPLSVPN", "L2 vpnEVPN"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - ipv4Unicast not negotiated - Advertised: False, Received: False, Enabled: False", "Peer: 172.30.11.1 VRF: default - ipv4MplsVpn not negotiated - Advertised: False, Received: True, Enabled: False", @@ -2086,9 +1672,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success-strict", - "test": VerifyBGPPeerMPCaps, + (VerifyBGPPeerMPCaps, "success-strict"): { "eos_data": [ { "vrfs": { @@ -2098,16 +1682,8 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.1", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsLabels": { - "advertised": True, - "received": True, - "enabled": True, - }, + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, } }, } @@ -2119,16 +1695,8 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.10", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsVpn": { - "advertised": True, - "received": True, - "enabled": True, - }, + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsVpn": {"advertised": True, "received": True, "enabled": True}, } }, } @@ -2139,25 +1707,13 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "strict": True, - "capabilities": ["Ipv4 Unicast", "ipv4MplsLabels"], - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - "strict": True, - "capabilities": ["ipv4-Unicast", "ipv4MplsVpn"], - }, + {"peer_address": "172.30.11.1", "vrf": "default", "strict": True, "capabilities": ["Ipv4 Unicast", "ipv4MplsLabels"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "strict": True, "capabilities": ["ipv4-Unicast", "ipv4MplsVpn"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-srict", - "test": VerifyBGPPeerMPCaps, + (VerifyBGPPeerMPCaps, "failure-srict"): { "eos_data": [ { "vrfs": { @@ -2167,16 +1723,8 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.1", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsLabels": { - "advertised": True, - "received": True, - "enabled": True, - }, + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, } }, } @@ -2188,16 +1736,8 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "172.30.11.10", "neighborCapabilities": { "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - }, - "ipv4MplsVpn": { - "advertised": False, - "received": True, - "enabled": True, - }, + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsVpn": {"advertised": False, "received": True, "enabled": True}, } }, } @@ -2208,59 +1748,54 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "strict": True, - "capabilities": ["Ipv4 Unicast"], - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - "strict": True, - "capabilities": ["ipv4MplsVpn", "L2vpnEVPN"], - }, + {"peer_address": "172.30.11.1", "vrf": "default", "strict": True, "capabilities": ["Ipv4 Unicast"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "strict": True, "capabilities": ["ipv4MplsVpn", "L2vpnEVPN"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - Mismatch - Expected: ipv4Unicast Actual: ipv4Unicast, ipv4MplsLabels", "Peer: 172.30.11.10 VRF: MGMT - Mismatch - Expected: ipv4MplsVpn, l2VpnEvpn Actual: ipv4Unicast, ipv4MplsVpn", ], }, }, - { - "name": "success", - "test": VerifyBGPPeerASNCap, + (VerifyBGPPeerMPCaps, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ { - "peerAddress": "172.30.11.1", + "peerAddress": "fd00:dc:1::1", "neighborCapabilities": { - "fourOctetAsnCap": { - "advertised": True, - "received": True, - "enabled": True, - }, + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": False, "received": True, "enabled": True}, + } }, - } + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": { + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": False, "received": True, "enabled": True}, + "ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}, + } + }, + }, ] }, "MGMT": { "peerList": [ { - "peerAddress": "172.30.11.10", "neighborCapabilities": { - "fourOctetAsnCap": { - "advertised": True, - "received": True, - "enabled": True, - }, + "multiprotocolCaps": { + "ipv4Unicast": {"advertised": True, "received": True, "enabled": True}, + "ipv4MplsVpn": {"advertised": False, "received": True, "enabled": True}, + } }, + "ifName": "Ethernet1", } ] }, @@ -2269,21 +1804,69 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default", "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"]}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "capabilities": ["Ipv4Unicast", "ipv4 Mpls labels"]}, + {"interface": "Ethernet1", "vrf": "MGMT", "capabilities": ["ipv4_Unicast", "ipv4 MplsVpn"]}, ] }, - "expected": {"result": "success"}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - ipv4MplsLabels not negotiated - Advertised: False, Received: True, Enabled: True", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - ipv4Unicast not negotiated - Advertised: False, Received: True, Enabled: True", + "Interface: Ethernet1 VRF: MGMT - ipv4MplsVpn not negotiated - Advertised: False, Received: True, Enabled: True", + ], + }, }, - { - "name": "failure-no-peer", - "test": VerifyBGPPeerASNCap, + (VerifyBGPPeerASNCap, "success"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "172.30.11.1", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}} + ] + }, + "MGMT": { + "peerList": [ + {"peerAddress": "172.30.11.10", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}} + ] + }, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerASNCap, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "fd00:dc:1::1", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}}, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}, + }, + ] + }, + "MGMT": { + "peerList": [{"neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}, "ifName": "Ethernet1"}] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerASNCap, "failure-no-peer"): { "eos_data": [ { "vrfs": { @@ -2291,37 +1874,17 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, + } ] } } } ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.10", - "vrf": "default", - } - ] - }, - "expected": { - "result": "failure", - "messages": ["Peer: 172.30.11.10 VRF: default - Not found"], - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.10", "vrf": "default"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.10 VRF: default - Not found"]}, }, - { - "name": "failure-missing-capabilities", - "test": VerifyBGPPeerASNCap, + (VerifyBGPPeerASNCap, "failure-missing-capabilities"): { "eos_data": [ { "vrfs": { @@ -2329,15 +1892,7 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, } ] }, @@ -2345,140 +1900,128 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.10", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4MplsLabels": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ipv4MplsLabels": {"advertised": True, "received": True, "enabled": True}}}, } ] }, } } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "172.30.11.1", "vrf": "default"}, - {"peer_address": "172.30.11.10", "vrf": "MGMT"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]}, "expected": { - "result": "failure", - "messages": [ - "Peer: 172.30.11.1 VRF: default - 4-octet ASN capability not found", - "Peer: 172.30.11.10 VRF: MGMT - 4-octet ASN capability not found", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Peer: 172.30.11.1 VRF: default - 4-octet ASN capability not found", "Peer: 172.30.11.10 VRF: MGMT - 4-octet ASN capability not found"], }, }, - { - "name": "failure-incorrect-capabilities", - "test": VerifyBGPPeerASNCap, + (VerifyBGPPeerASNCap, "failure-incorrect-capabilities"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "fourOctetAsnCap": { - "advertised": False, - "received": False, - "enabled": False, - }, - }, - } + {"peerAddress": "172.30.11.1", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": False, "received": False, "enabled": False}}} ] }, "MGMT": { "peerList": [ - { - "peerAddress": "172.30.11.10", - "neighborCapabilities": { - "fourOctetAsnCap": { - "advertised": True, - "received": False, - "enabled": True, - }, - }, - } + {"peerAddress": "172.30.11.10", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": False, "enabled": True}}} ] }, } } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "172.30.11.1", "vrf": "default"}, - {"peer_address": "172.30.11.10", "vrf": "MGMT"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - 4-octet ASN capability not negotiated - Advertised: False, Received: False, Enabled: False", "Peer: 172.30.11.10 VRF: MGMT - 4-octet ASN capability not negotiated - Advertised: True, Received: False, Enabled: True", ], }, }, - { - "name": "success", - "test": VerifyBGPPeerRouteRefreshCap, + (VerifyBGPPeerASNCap, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ + {"peerAddress": "fd00:dc:1::1", "neighborCapabilities": {"fourOctetAsnCap": {"advertised": True, "received": True, "enabled": True}}}, { - "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "routeRefreshCap": { - "advertised": True, - "received": True, - "enabled": True, - }, - }, - } + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": {"fourOctetAsnCap": {"advertised": False, "received": True, "enabled": True}}, + }, ] }, - "CS": { - "peerList": [ - { - "peerAddress": "172.30.11.11", - "neighborCapabilities": { - "routeRefreshCap": { - "advertised": True, - "received": True, - "enabled": True, - }, - }, - } - ] + "MGMT": { + "peerList": [{"neighborCapabilities": {"fourOctetAsnCap": {"advertised": False, "received": True, "enabled": True}}, "ifName": "Ethernet1"}] }, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.11", - "vrf": "CS", - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, ] }, - "expected": {"result": "success"}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - 4-octet ASN capability not negotiated - Advertised: False, Received: True, Enabled: True", + "Interface: Ethernet1 VRF: MGMT - 4-octet ASN capability not negotiated - Advertised: False, Received: True, Enabled: True", + ], + }, }, - { - "name": "failure-no-peer", - "test": VerifyBGPPeerRouteRefreshCap, + (VerifyBGPPeerRouteRefreshCap, "success"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "172.30.11.1", "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}} + ] + }, + "CS": { + "peerList": [ + {"peerAddress": "172.30.11.11", "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}} + ] + }, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "CS"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerRouteRefreshCap, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "fd00:dc:1::1", "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}}, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}, + }, + ] + }, + "CS": { + "peerList": [{"neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}, "ifName": "Ethernet1"}] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "CS"}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerRouteRefreshCap, "failure-no-peer"): { "eos_data": [ { "vrfs": { @@ -2486,15 +2029,7 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ip4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ip4Unicast": {"advertised": True, "received": True, "enabled": True}}}, } ] }, @@ -2502,44 +2037,17 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.12", - "neighborCapabilities": { - "multiprotocolCaps": { - "ip4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ip4Unicast": {"advertised": True, "received": True, "enabled": True}}}, } ] }, } } ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.12", - "vrf": "default", - }, - { - "peer_address": "172.30.11.1", - "vrf": "CS", - }, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 172.30.11.12 VRF: default - Not found", - "Peer: 172.30.11.1 VRF: CS - Not found", - ], - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.12", "vrf": "default"}, {"peer_address": "172.30.11.1", "vrf": "CS"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.12 VRF: default - Not found", "Peer: 172.30.11.1 VRF: CS - Not found"]}, }, - { - "name": "failure-missing-capabilities", - "test": VerifyBGPPeerRouteRefreshCap, + (VerifyBGPPeerRouteRefreshCap, "failure-missing-capabilities"): { "eos_data": [ { "vrfs": { @@ -2547,15 +2055,7 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, } ] }, @@ -2563,872 +2063,531 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "172.30.11.11", - "neighborCapabilities": { - "multiprotocolCaps": { - "ipv4Unicast": { - "advertised": True, - "received": True, - "enabled": True, - } - }, - }, + "neighborCapabilities": {"multiprotocolCaps": {"ipv4Unicast": {"advertised": True, "received": True, "enabled": True}}}, } ] }, } } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "172.30.11.1", "vrf": "default"}, - {"peer_address": "172.30.11.11", "vrf": "CS"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "CS"}]}, "expected": { - "result": "failure", - "messages": [ - "Peer: 172.30.11.1 VRF: default - Route refresh capability not found", - "Peer: 172.30.11.11 VRF: CS - Route refresh capability not found", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Peer: 172.30.11.1 VRF: default - Route refresh capability not found", "Peer: 172.30.11.11 VRF: CS - Route refresh capability not found"], }, }, - { - "name": "failure-incorrect-capabilities", - "test": VerifyBGPPeerRouteRefreshCap, + (VerifyBGPPeerRouteRefreshCap, "failure-incorrect-capabilities"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "neighborCapabilities": { - "routeRefreshCap": { - "advertised": False, - "received": False, - "enabled": False, - }, - }, - } + {"peerAddress": "172.30.11.1", "neighborCapabilities": {"routeRefreshCap": {"advertised": False, "received": False, "enabled": False}}} ] }, "CS": { "peerList": [ - { - "peerAddress": "172.30.11.11", - "neighborCapabilities": { - "routeRefreshCap": { - "advertised": True, - "received": True, - "enabled": True, - }, - }, - } + {"peerAddress": "172.30.11.11", "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": True}}} ] }, } } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "172.30.11.1", "vrf": "default"}, - {"peer_address": "172.30.11.11", "vrf": "CS"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "CS"}]}, "expected": { - "result": "failure", - "messages": [ - "Peer: 172.30.11.1 VRF: default - Route refresh capability not negotiated - Advertised: False, Received: False, Enabled: False", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Peer: 172.30.11.1 VRF: default - Route refresh capability not negotiated - Advertised: False, Received: False, Enabled: False"], }, }, - { - "name": "success", - "test": VerifyBGPPeerMD5Auth, + (VerifyBGPPeerRouteRefreshCap, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ + {"peerAddress": "fd00:dc:1::1", "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": False, "enabled": True}}}, { - "peerAddress": "172.30.11.1", - "state": "Established", - "md5AuthEnabled": True, - } + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "neighborCapabilities": {"routeRefreshCap": {"advertised": True, "received": True, "enabled": False}}, + }, ] }, "CS": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "state": "Established", - "md5AuthEnabled": True, - } - ] + "peerList": [{"neighborCapabilities": {"routeRefreshCap": {"advertised": False, "received": True, "enabled": True}}, "ifName": "Ethernet1"}] }, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.10", - "vrf": "CS", - }, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "failure-no-peer", - "test": VerifyBGPPeerMD5Auth, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "state": "Established", - "md5AuthEnabled": True, - } - ] - }, - "CS": { - "peerList": [ - { - "peerAddress": "172.30.11.11", - "state": "Established", - "md5AuthEnabled": True, - } - ] - }, - } - } - ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.10", - "vrf": "default", - }, - { - "peer_address": "172.30.11.12", - "vrf": "CS", - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "CS"}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Peer: 172.30.11.10 VRF: default - Not found", - "Peer: 172.30.11.12 VRF: CS - Not found", + "Peer: fd00:dc:1::1 VRF: default - Route refresh capability not negotiated - Advertised: True, Received: False, Enabled: True", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Route refresh capability not negotiated - Advertised: True, Received: True, Enabled: False", + "Interface: Ethernet1 VRF: CS - Route refresh capability not negotiated - Advertised: False, Received: True, Enabled: True", ], }, }, - { - "name": "failure-not-established-peer", - "test": VerifyBGPPeerMD5Auth, + (VerifyBGPPeerMD5Auth, "success"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "state": "Established", "md5AuthEnabled": True}]}, + "CS": {"peerList": [{"peerAddress": "172.30.11.10", "state": "Established", "md5AuthEnabled": True}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "CS"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerMD5Auth, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "state": "Idle", - "md5AuthEnabled": True, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "state": "Idle", - "md5AuthEnabled": True, - } + {"peerAddress": "fd00:dc:1::1", "state": "Established", "md5AuthEnabled": True}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "state": "Established", "md5AuthEnabled": True}, ] }, + "CS": {"peerList": [{"state": "Established", "md5AuthEnabled": True, "ifName": "Ethernet1"}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "CS"}, ] }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerMD5Auth, "failure-no-peer"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "state": "Established", "md5AuthEnabled": True}]}, + "CS": {"peerList": [{"peerAddress": "172.30.11.11", "state": "Established", "md5AuthEnabled": True}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.10", "vrf": "default"}, {"peer_address": "172.30.11.12", "vrf": "CS"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.10 VRF: default - Not found", "Peer: 172.30.11.12 VRF: CS - Not found"]}, + }, + (VerifyBGPPeerMD5Auth, "failure-not-established-peer"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "state": "Idle", "md5AuthEnabled": True}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.10", "state": "Idle", "md5AuthEnabled": True}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - Incorrect session state - Expected: Established Actual: Idle", "Peer: 172.30.11.10 VRF: MGMT - Incorrect session state - Expected: Established Actual: Idle", ], }, }, - { - "name": "failure-not-md5-peer", - "test": VerifyBGPPeerMD5Auth, + (VerifyBGPPeerMD5Auth, "failure-not-md5-peer"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "state": "Established", - }, - { - "peerAddress": "172.30.11.10", - "state": "Established", - "md5AuthEnabled": False, - }, - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.11", - "state": "Established", - "md5AuthEnabled": False, - } + {"peerAddress": "172.30.11.1", "state": "Established"}, + {"peerAddress": "172.30.11.10", "state": "Established", "md5AuthEnabled": False}, ] }, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.11", "state": "Established", "md5AuthEnabled": False}]}, } } ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.11", - "vrf": "MGMT", - }, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.11", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - Session does not have MD5 authentication enabled", "Peer: 172.30.11.11 VRF: MGMT - Session does not have MD5 authentication enabled", ], }, }, - { - "name": "success", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - } - ], - "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, - "expected": {"result": "success"}, - }, - { - "name": "success-multiple-endpoints", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - }, - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10010 aac1.ab5d.b41e": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - }, - ], - "inputs": { - "vxlan_endpoints": [ - {"address": "192.168.20.102", "vni": 10020}, - {"address": "aac1.ab5d.b41e", "vni": 10010}, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "success-multiple-routes-ip", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": False, - "valid": False, - }, - }, - ] - }, - }, - }, - ], - "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, - "expected": {"result": "success"}, - }, - { - "name": "success-multiple-routes-mac", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - }, - ], - "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}]}, - "expected": {"result": "success"}, - }, - { - "name": "success-multiple-routes-multiple-paths-ip", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - "ecmp": True, - "ecmpContributor": True, - "ecmpHead": True, - }, - }, - { - "routeType": { - "active": False, - "valid": True, - "ecmp": True, - "ecmpContributor": True, - "ecmpHead": False, - }, - }, - ] - }, - "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - }, - ], - "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, - "expected": {"result": "success"}, - }, - { - "name": "success-multiple-routes-multiple-paths-mac", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - "ecmp": True, - "ecmpContributor": True, - "ecmpHead": True, - }, - }, - { - "routeType": { - "active": False, - "valid": True, - "ecmp": True, - "ecmpContributor": True, - "ecmpHead": False, - }, - }, - ] - }, - "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2": { - "evpnRoutePaths": [ - { - "routeType": { - "active": True, - "valid": True, - }, - }, - ] - }, - }, - }, - ], - "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}]}, - "expected": {"result": "success"}, - }, - { - "name": "failure-no-routes", - "test": VerifyEVPNType2Route, - "eos_data": [{"vrf": "default", "routerId": "10.1.0.3", "asn": 65120, "evpnRoutes": {}}], - "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, - "expected": { - "result": "failure", - "messages": ["Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route"], - }, - }, - { - "name": "failure-path-not-active", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": False, - "valid": True, - }, - }, - ] - }, - }, - }, - ], - "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, - "expected": { - "result": "failure", - "messages": ["Address: 192.168.20.102 VNI: 10020 - No valid and active path"], - }, - }, - { - "name": "failure-multiple-endpoints", - "test": VerifyEVPNType2Route, - "eos_data": [ - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { - "evpnRoutePaths": [ - { - "routeType": { - "active": False, - "valid": False, - }, - }, - ] - }, - }, - }, - { - "vrf": "default", - "routerId": "10.1.0.3", - "asn": 65120, - "evpnRoutes": { - "RD: 10.1.0.5:500 mac-ip 10010 aac1.ab5d.b41e": { - "evpnRoutePaths": [ - { - "routeType": { - "active": False, - "valid": False, - }, - }, - ] - }, - }, - }, - ], - "inputs": { - "vxlan_endpoints": [ - {"address": "192.168.20.102", "vni": 10020}, - {"address": "aac1.ab5d.b41e", "vni": 10010}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Address: 192.168.20.102 VNI: 10020 - No valid and active path", "Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No valid and active path"], - }, - }, - { - "name": "success", - "test": VerifyBGPAdvCommunities, + (VerifyBGPPeerMD5Auth, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "advertisedCommunities": { - "standard": True, - "extended": True, - "large": True, - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "advertisedCommunities": { - "standard": True, - "extended": True, - "large": True, - }, - } + {"peerAddress": "fd00:dc:1::1", "state": "Idle", "md5AuthEnabled": True}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "state": "Established"}, ] }, + "CS": {"peerList": [{"state": "Idle", "md5AuthEnabled": True, "ifName": "Ethernet1"}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - }, - { - "peer_address": "172.30.11.10", - "vrf": "MGMT", - }, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "failure-no-peer", - "test": VerifyBGPAdvCommunities, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "advertisedCommunities": { - "standard": True, - "extended": True, - "large": True, - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "advertisedCommunities": { - "standard": True, - "extended": True, - "large": True, - }, - } - ] - }, - } - } - ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.10", - "vrf": "default", - }, - { - "peer_address": "172.30.11.12", - "vrf": "MGMT", - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "CS"}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Peer: 172.30.11.10 VRF: default - Not found", - "Peer: 172.30.11.12 VRF: MGMT - Not found", + "Peer: fd00:dc:1::1 VRF: default - Incorrect session state - Expected: Established Actual: Idle", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Session does not have MD5 authentication enabled", + "Interface: Ethernet1 VRF: CS - Incorrect session state - Expected: Established Actual: Idle", ], }, }, - { - "name": "failure-not-correct-communities", - "test": VerifyBGPAdvCommunities, + (VerifyEVPNType2Route, "success"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": {"RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}}, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "success-multiple-endpoints"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": {"RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}}, + }, + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": {"RD: 10.1.0.5:500 mac-ip 10010 aac1.ab5d.b41e": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}}, + }, + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}, {"address": "aac1.ab5d.b41e", "vni": 10010}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "success-multiple-routes-ip"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}, + "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": False, "valid": False}}]}, + }, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "success-multiple-routes-mac"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}, + "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}, + }, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "success-multiple-routes-multiple-paths-ip"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": { + "evpnRoutePaths": [ + {"routeType": {"active": True, "valid": True, "ecmp": True, "ecmpContributor": True, "ecmpHead": True}}, + {"routeType": {"active": False, "valid": True, "ecmp": True, "ecmpContributor": True, "ecmpHead": False}}, + ] + }, + "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}, + }, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "success-multiple-routes-multiple-paths-mac"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2": { + "evpnRoutePaths": [ + {"routeType": {"active": True, "valid": True, "ecmp": True, "ecmpContributor": True, "ecmpHead": True}}, + {"routeType": {"active": False, "valid": True, "ecmp": True, "ecmpContributor": True, "ecmpHead": False}}, + ] + }, + "RD: 10.1.0.6:500 mac-ip 10020 aac1.ab4e.bec2": {"evpnRoutePaths": [{"routeType": {"active": True, "valid": True}}]}, + }, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "aac1.ab4e.bec2", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType2Route, "failure-no-routes"): { + "eos_data": [{"vrf": "default", "routerId": "10.1.0.3", "asn": 65120, "evpnRoutes": {}}], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route"]}, + }, + (VerifyEVPNType2Route, "failure-path-not-active"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": {"RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": False, "valid": True}}]}}, + } + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Address: 192.168.20.102 VNI: 10020 - No valid and active path"]}, + }, + (VerifyEVPNType2Route, "failure-multiple-endpoints"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.5:500 mac-ip 10020 aac1.ab4e.bec2 192.168.20.102": {"evpnRoutePaths": [{"routeType": {"active": False, "valid": False}}]} + }, + }, + { + "vrf": "default", + "routerId": "10.1.0.3", + "asn": 65120, + "evpnRoutes": {"RD: 10.1.0.5:500 mac-ip 10010 aac1.ab5d.b41e": {"evpnRoutePaths": [{"routeType": {"active": False, "valid": False}}]}}, + }, + ], + "inputs": {"vxlan_endpoints": [{"address": "192.168.20.102", "vni": 10020}, {"address": "aac1.ab5d.b41e", "vni": 10010}]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Address: 192.168.20.102 VNI: 10020 - No valid and active path", "Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No valid and active path"], + }, + }, + (VerifyBGPAdvCommunities, "success"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "advertisedCommunities": { - "standard": False, - "extended": False, - "large": False, - }, - } - ] - }, - "CS": { - "peerList": [ - { - "peerAddress": "172.30.11.10", - "advertisedCommunities": { - "standard": True, - "extended": True, - "large": False, - }, - } - ] - }, + "default": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.10", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1"}, {"peer_address": "172.30.11.10", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPAdvCommunities, "success-specified-communities"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": True, "extended": True, "large": False}}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.10", "advertisedCommunities": {"standard": False, "extended": True, "large": False}}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - }, - { - "peer_address": "172.30.11.10", - "vrf": "CS", - }, + {"peer_address": "172.30.11.1", "advertised_communities": ["standard", "extended"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "advertised_communities": ["extended"]}, ] }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPAdvCommunities, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "fd00:dc:1::1", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}, + ] + }, + "MGMT": {"peerList": [{"advertisedCommunities": {"standard": True, "extended": True, "large": True}, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPAdvCommunities, "failure-no-peer"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": True, "extended": True, "large": True}}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.10", "vrf": "default"}, {"peer_address": "172.30.11.12", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.10 VRF: default - Not found", "Peer: 172.30.11.12 VRF: MGMT - Not found"]}, + }, + (VerifyBGPAdvCommunities, "failure-not-correct-communities"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": False, "extended": False, "large": False}}]}, + "CS": {"peerList": [{"peerAddress": "172.30.11.10", "advertisedCommunities": {"standard": True, "extended": True, "large": False}}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "172.30.11.1", "vrf": "default"}, {"peer_address": "172.30.11.10", "vrf": "CS"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - Standard: False, Extended: False, Large: False", "Peer: 172.30.11.10 VRF: CS - Standard: True, Extended: True, Large: False", ], }, }, - { - "name": "success", - "test": VerifyBGPTimers, + (VerifyBGPAdvCommunities, "failure-specified-communities"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "advertisedCommunities": {"standard": False, "extended": False, "large": False}}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.10", "advertisedCommunities": {"standard": False, "extended": True, "large": False}}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.1", "advertised_communities": ["standard", "extended"]}, + {"peer_address": "172.30.11.10", "vrf": "MGMT", "advertised_communities": ["extended"]}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.1 VRF: default - Standard: False, Extended: False, Large: False"]}, + }, + (VerifyBGPAdvCommunities, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "172.30.11.1", - "holdTime": 180, - "keepaliveTime": 60, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.11", - "holdTime": 180, - "keepaliveTime": 60, - } + {"peerAddress": "fd00:dc:1::1", "advertisedCommunities": {"standard": False, "extended": True, "large": True}}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "advertisedCommunities": {"standard": True, "extended": False, "large": True}}, ] }, + "MGMT": {"peerList": [{"advertisedCommunities": {"standard": True, "extended": True, "large": False}, "ifName": "Ethernet1"}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "hold_time": 180, - "keep_alive_time": 60, - }, - { - "peer_address": "172.30.11.11", - "vrf": "MGMT", - "hold_time": 180, - "keep_alive_time": 60, - }, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "failure-no-peer", - "test": VerifyBGPTimers, - "eos_data": [ - { - "vrfs": { - "default": {"peerList": []}, - "MGMT": {"peerList": []}, - } - } - ], - "inputs": { - "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "MGMT", - "hold_time": 180, - "keep_alive_time": 60, - }, - { - "peer_address": "172.30.11.11", - "vrf": "default", - "hold_time": 180, - "keep_alive_time": 60, - }, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Peer: 172.30.11.1 VRF: MGMT - Not found", - "Peer: 172.30.11.11 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Standard: False, Extended: True, Large: True", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Standard: True, Extended: False, Large: True", + "Interface: Ethernet1 VRF: MGMT - Standard: True, Extended: True, Large: False", ], }, }, - { - "name": "failure-not-correct-timers", - "test": VerifyBGPTimers, + (VerifyBGPTimers, "success"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "172.30.11.1", - "holdTime": 160, - "keepaliveTime": 60, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "172.30.11.11", - "holdTime": 120, - "keepaliveTime": 40, - } - ] - }, + "default": {"peerList": [{"peerAddress": "172.30.11.1", "holdTime": 180, "keepaliveTime": 60}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.11", "holdTime": 180, "keepaliveTime": 60}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "172.30.11.1", - "vrf": "default", - "hold_time": 180, - "keep_alive_time": 60, - }, - { - "peer_address": "172.30.11.11", - "vrf": "MGMT", - "hold_time": 180, - "keep_alive_time": 60, - }, + {"peer_address": "172.30.11.1", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"peer_address": "172.30.11.11", "vrf": "MGMT", "hold_time": 180, "keep_alive_time": 60}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPTimers, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "fd00:dc:1::1", "holdTime": 180, "keepaliveTime": 60}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "holdTime": 180, "keepaliveTime": 60}, + ] + }, + "MGMT": {"peerList": [{"holdTime": 180, "keepaliveTime": 60, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"interface": "Ethernet1", "vrf": "MGMT", "hold_time": 180, "keep_alive_time": 60}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPTimers, "failure-no-peer"): { + "eos_data": [{"vrfs": {"default": {"peerList": []}, "MGMT": {"peerList": []}}}], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.1", "vrf": "MGMT", "hold_time": 180, "keep_alive_time": 60}, + {"peer_address": "172.30.11.11", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 172.30.11.1 VRF: MGMT - Not found", "Peer: 172.30.11.11 VRF: default - Not found"]}, + }, + (VerifyBGPTimers, "failure-not-correct-timers"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "172.30.11.1", "holdTime": 160, "keepaliveTime": 60}]}, + "MGMT": {"peerList": [{"peerAddress": "172.30.11.11", "holdTime": 120, "keepaliveTime": 40}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "172.30.11.1", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"peer_address": "172.30.11.11", "vrf": "MGMT", "hold_time": 180, "keep_alive_time": 60}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 172.30.11.1 VRF: default - Hold time mismatch - Expected: 180 Actual: 160", "Peer: 172.30.11.11 VRF: MGMT - Hold time mismatch - Expected: 180 Actual: 120", @@ -3436,9 +2595,38 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPPeerDropStats, + (VerifyBGPTimers, "failure-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "fd00:dc:1::1", "holdTime": 100, "keepaliveTime": 60}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "holdTime": 180, "keepaliveTime": 50}, + ] + }, + "MGMT": {"peerList": [{"holdTime": 150, "keepaliveTime": 50, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "hold_time": 180, "keep_alive_time": 60}, + {"interface": "Ethernet1", "vrf": "MGMT", "hold_time": 180, "keep_alive_time": 60}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Hold time mismatch - Expected: 180 Actual: 100", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Keepalive time mismatch - Expected: 60 Actual: 50", + "Interface: Ethernet1 VRF: MGMT - Hold time mismatch - Expected: 180 Actual: 150", + "Interface: Ethernet1 VRF: MGMT - Keepalive time mismatch - Expected: 60 Actual: 50", + ], + }, + }, + (VerifyBGPPeerDropStats, "success"): { "eos_data": [ { "vrfs": { @@ -3478,8 +2666,8 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -3491,11 +2679,83 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.9", "vrf": "MGMT", "drop_stats": ["inDropClusterIdLoop", "inDropOrigId", "inDropNhLocal"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-found", - "test": VerifyBGPPeerDropStats, + (VerifyBGPPeerDropStats, "success-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "fd00:dc:1::1", + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 0, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 0, + "prefixDroppedMaxRouteLimitViolatedV4": 0, + "prefixDroppedMartianV6": 0, + }, + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 0, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 0, + "prefixDroppedMaxRouteLimitViolatedV4": 0, + "prefixDroppedMartianV6": 0, + }, + }, + ] + }, + "MGMT": { + "peerList": [ + { + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 0, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 0, + "prefixDroppedMaxRouteLimitViolatedV4": 0, + "prefixDroppedMartianV6": 0, + }, + "ifName": "Ethernet1", + } + ] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + { + "peer_address": "fd00:dc:1::1", + "vrf": "default", + "drop_stats": ["prefixDroppedMartianV4", "prefixDroppedMaxRouteLimitViolatedV4", "prefixDroppedMartianV6"], + }, + { + "peer_address": "fe80::250:56ff:fe01:112%Vl4094", + "vrf": "default", + "drop_stats": ["prefixDroppedMartianV4", "prefixDroppedMaxRouteLimitViolatedV4", "prefixDroppedMartianV6"], + }, + {"interface": "Ethernet1", "vrf": "MGMT", "drop_stats": ["inDropClusterIdLoop", "inDropOrigId", "inDropNhLocal"]}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerDropStats, "failure-not-found"): { "eos_data": [{"vrfs": {}}], "inputs": { "bgp_peers": [ @@ -3507,17 +2767,9 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.9", "vrf": "MGMT", "drop_stats": ["inDropClusterIdLoop", "inDropOrigId", "inDropNhLocal"]}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Not found", - "Peer: 10.100.0.9 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.0.9 VRF: MGMT - Not found"]}, }, - { - "name": "failure", - "test": VerifyBGPPeerDropStats, + (VerifyBGPPeerDropStats, "failure"): { "eos_data": [ { "vrfs": { @@ -3557,8 +2809,8 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -3571,7 +2823,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMartianV4: 1", "Peer: 10.100.0.8 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMaxRouteLimitViolatedV4: 1", @@ -3580,9 +2832,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success-all-drop-stats", - "test": VerifyBGPPeerDropStats, + (VerifyBGPPeerDropStats, "success-all-drop-stats"): { "eos_data": [ { "vrfs": { @@ -3622,20 +2872,13 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": {"result": "success"}, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-all-drop-stats", - "test": VerifyBGPPeerDropStats, + (VerifyBGPPeerDropStats, "failure-all-drop-stats"): { "eos_data": [ { "vrfs": { @@ -3675,17 +2918,12 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Non-zero NLRI drop statistics counter - inDropAsloop: 3", "Peer: 10.100.0.8 VRF: default - Non-zero NLRI drop statistics counter - inDropOrigId: 1", @@ -3698,9 +2936,90 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerDropStats, "failure-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "fd00:dc:1::1", + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 0, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 4, + "prefixDroppedMaxRouteLimitViolatedV4": 0, + "prefixDroppedMartianV6": 2, + }, + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 0, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 0, + "prefixDroppedMaxRouteLimitViolatedV4": 3, + "prefixDroppedMartianV6": 2, + }, + }, + ] + }, + "MGMT": { + "peerList": [ + { + "dropStats": { + "inDropAsloop": 0, + "inDropClusterIdLoop": 0, + "inDropMalformedMpbgp": 0, + "inDropOrigId": 3, + "inDropNhLocal": 0, + "inDropNhAfV6": 0, + "prefixDroppedMartianV4": 0, + "prefixDroppedMaxRouteLimitViolatedV4": 0, + "prefixDroppedMartianV6": 0, + }, + "ifName": "Ethernet1", + } + ] + }, + } + } + ], + "inputs": { + "bgp_peers": [ + { + "peer_address": "fd00:dc:1::1", + "vrf": "default", + "drop_stats": ["prefixDroppedMartianV4", "prefixDroppedMaxRouteLimitViolatedV4", "prefixDroppedMartianV6"], + }, + { + "peer_address": "fe80::250:56ff:fe01:112%Vl4094", + "vrf": "default", + "drop_stats": ["prefixDroppedMartianV4", "prefixDroppedMaxRouteLimitViolatedV4", "prefixDroppedMartianV6"], + }, + {"interface": "Ethernet1", "vrf": "MGMT", "drop_stats": ["inDropClusterIdLoop", "inDropOrigId", "inDropNhLocal"]}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMartianV4: 4", + "Peer: fd00:dc:1::1 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMartianV6: 2", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMaxRouteLimitViolatedV4: 3", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Non-zero NLRI drop statistics counter - prefixDroppedMartianV6: 2", + "Interface: Ethernet1 VRF: MGMT - Non-zero NLRI drop statistics counter - inDropOrigId: 3", + ], + }, + }, + (VerifyBGPPeerUpdateErrors, "success"): { "eos_data": [ { "vrfs": { @@ -3732,8 +3051,8 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -3741,31 +3060,73 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.9", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-found", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerUpdateErrors, "success-ipv6-rfc5549"): { "eos_data": [ - {"vrfs": {}}, + { + "vrfs": { + "default": { + "peerList": [ + { + "peerAddress": "fd00:dc:1::1", + "peerInUpdateErrors": { + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "None", + "lastUpdErrTime": 0, + }, + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "peerInUpdateErrors": { + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "None", + "lastUpdErrTime": 0, + }, + }, + ] + }, + "MGMT": { + "peerList": [ + { + "peerInUpdateErrors": { + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "None", + "lastUpdErrTime": 0, + }, + "ifName": "Ethernet1", + } + ] + }, + } + } ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + {"interface": "Ethernet1", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerUpdateErrors, "failure-not-found"): { + "eos_data": [{"vrfs": {}}], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, {"peer_address": "10.100.0.9", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Not found", - "Peer: 10.100.0.9 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.0.9 VRF: MGMT - Not found"]}, }, - { - "name": "failure-errors", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerUpdateErrors, "failure-errors"): { "eos_data": [ { "vrfs": { @@ -3797,8 +3158,8 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -3807,16 +3168,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Non-zero update error counter - disabledAfiSafi: ipv4Unicast", "Peer: 10.100.0.9 VRF: MGMT - Non-zero update error counter - inUpdErrWithdraw: 1", ], }, }, - { - "name": "success-all-error-counters", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerUpdateErrors, "success-all-error-counters"): { "eos_data": [ { "vrfs": { @@ -3848,20 +3207,13 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": {"result": "success"}, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-all-error-counters", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerUpdateErrors, "failure-all-error-counters"): { "eos_data": [ { "vrfs": { @@ -3893,21 +3245,17 @@ DATA: list[dict[str, Any]] = [ } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, - { - "peer_address": "10.100.0.9", - "vrf": "MGMT", - "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi", "inUpdErrDisableAfiSafi"], - }, + {"peer_address": "10.100.0.9", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi", "inUpdErrDisableAfiSafi"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Non-zero update error counter - inUpdErrWithdraw: 1", "Peer: 10.100.0.8 VRF: default - Non-zero update error counter - disabledAfiSafi: ipv4Unicast", @@ -3916,9 +3264,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-all-not-found", - "test": VerifyBGPPeerUpdateErrors, + (VerifyBGPPeerUpdateErrors, "failure-all-not-found"): { "eos_data": [ { "vrfs": { @@ -3926,12 +3272,7 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "10.100.0.8", - "peerInUpdateErrors": { - "inUpdErrIgnore": 0, - "inUpdErrDisableAfiSafi": 0, - "disabledAfiSafi": "ipv4Unicast", - "lastUpdErrTime": 0, - }, + "peerInUpdateErrors": {"inUpdErrIgnore": 0, "inUpdErrDisableAfiSafi": 0, "disabledAfiSafi": "ipv4Unicast", "lastUpdErrTime": 0}, } ] }, @@ -3939,30 +3280,21 @@ DATA: list[dict[str, Any]] = [ "peerList": [ { "peerAddress": "10.100.0.9", - "peerInUpdateErrors": { - "inUpdErrWithdraw": 1, - "inUpdErrIgnore": 0, - "disabledAfiSafi": "None", - "lastUpdErrTime": 0, - }, + "peerInUpdateErrors": {"inUpdErrWithdraw": 1, "inUpdErrIgnore": 0, "disabledAfiSafi": "None", "lastUpdErrTime": 0}, } ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, - { - "peer_address": "10.100.0.9", - "vrf": "MGMT", - "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi", "inUpdErrDisableAfiSafi"], - }, + {"peer_address": "10.100.0.9", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi", "inUpdErrDisableAfiSafi"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Non-zero update error counter - inUpdErrWithdraw: Not Found", "Peer: 10.100.0.8 VRF: default - Non-zero update error counter - disabledAfiSafi: ipv4Unicast", @@ -3971,32 +3303,75 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBgpRouteMaps, + (VerifyBGPPeerUpdateErrors, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ { - "peerAddress": "10.100.0.8", - "routeMapInbound": "RM-MLAG-PEER-IN", - "routeMapOutbound": "RM-MLAG-PEER-OUT", - } + "peerAddress": "fd00:dc:1::1", + "peerInUpdateErrors": { + "inUpdErrWithdraw": 3, + "inUpdErrIgnore": 0, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "None", + "lastUpdErrTime": 0, + }, + }, + { + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "peerInUpdateErrors": { + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 3, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "None", + "lastUpdErrTime": 0, + }, + }, ] }, "MGMT": { "peerList": [ { - "peerAddress": "10.100.0.10", - "routeMapInbound": "RM-MLAG-PEER-IN", - "routeMapOutbound": "RM-MLAG-PEER-OUT", + "peerInUpdateErrors": { + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0, + "inUpdErrDisableAfiSafi": 0, + "disabledAfiSafi": "ipv4Unicast", + "lastUpdErrTime": 0, + }, + "ifName": "Ethernet1", } ] }, - }, - }, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + {"interface": "Ethernet1", "vrf": "MGMT", "update_errors": ["inUpdErrWithdraw", "inUpdErrIgnore", "disabledAfiSafi"]}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Non-zero update error counter - inUpdErrWithdraw: 3", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Non-zero update error counter - inUpdErrIgnore: 3", + "Interface: Ethernet1 VRF: MGMT - Non-zero update error counter - disabledAfiSafi: ipv4Unicast", + ], + }, + }, + (VerifyBgpRouteMaps, "success"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT"}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.10", "routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT"}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4004,34 +3379,44 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.10", "vrf": "MGMT", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-route-map", - "test": VerifyBgpRouteMaps, + (VerifyBgpRouteMaps, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "routeMapInbound": "RM-MLAG-PEER", - "routeMapOutbound": "RM-MLAG-PEER", - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.10", - "routeMapInbound": "RM-MLAG-PEER", - "routeMapOutbound": "RM-MLAG-PEER", - } + {"peerAddress": "fd00:dc:1::1", "routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT"}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT"}, ] }, + "MGMT": {"peerList": [{"routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT", "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, + { + "peer_address": "fe80::250:56ff:fe01:112%Vl4094", + "vrf": "default", + "inbound_route_map": "RM-MLAG-PEER-IN", + "outbound_route_map": "RM-MLAG-PEER-OUT", }, - }, + {"interface": "Ethernet1", "vrf": "MGMT", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBgpRouteMaps, "failure-incorrect-route-map"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "routeMapInbound": "RM-MLAG-PEER", "routeMapOutbound": "RM-MLAG-PEER"}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.10", "routeMapInbound": "RM-MLAG-PEER", "routeMapOutbound": "RM-MLAG-PEER"}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4040,7 +3425,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: RM-MLAG-PEER", "Peer: 10.100.0.8 VRF: default - Outbound route-map mismatch - Expected: RM-MLAG-PEER-OUT Actual: RM-MLAG-PEER", @@ -4049,32 +3434,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-inbound-map", - "test": VerifyBgpRouteMaps, + (VerifyBgpRouteMaps, "failure-incorrect-inbound-map"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "routeMapInbound": "RM-MLAG-PEER", - "routeMapOutbound": "RM-MLAG-PEER", - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.10", - "routeMapInbound": "RM-MLAG-PEER", - "routeMapOutbound": "RM-MLAG-PEER", - } - ] - }, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8", "routeMapInbound": "RM-MLAG-PEER", "routeMapOutbound": "RM-MLAG-PEER"}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.10", "routeMapInbound": "RM-MLAG-PEER", "routeMapOutbound": "RM-MLAG-PEER"}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4083,36 +3450,15 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: RM-MLAG-PEER", "Peer: 10.100.0.10 VRF: MGMT - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: RM-MLAG-PEER", ], }, }, - { - "name": "failure-route-maps-not-configured", - "test": VerifyBgpRouteMaps, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.10", - } - ] - }, - }, - }, - ], + (VerifyBgpRouteMaps, "failure-route-maps-not-configured"): { + "eos_data": [{"vrfs": {"default": {"peerList": [{"peerAddress": "10.100.0.8"}]}, "MGMT": {"peerList": [{"peerAddress": "10.100.0.10"}]}}}], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, @@ -4120,7 +3466,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: Not Configured", "Peer: 10.100.0.8 VRF: default - Outbound route-map mismatch - Expected: RM-MLAG-PEER-OUT Actual: Not Configured", @@ -4129,57 +3475,60 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-peer-not-found", - "test": VerifyBgpRouteMaps, - "eos_data": [ - { - "vrfs": { - "default": {"peerList": []}, - "MGMT": {"peerList": []}, - }, - }, - ], + (VerifyBgpRouteMaps, "failure-peer-not-found"): { + "eos_data": [{"vrfs": {"default": {"peerList": []}, "MGMT": {"peerList": []}}}], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "inbound_route_map": "RM-MLAG-PEER-IN"}, {"peer_address": "10.100.0.10", "vrf": "MGMT", "inbound_route_map": "RM-MLAG-PEER-IN"}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Not found", - "Peer: 10.100.0.10 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.0.10 VRF: MGMT - Not found"]}, }, - { - "name": "success", - "test": VerifyBGPPeerRouteLimit, + (VerifyBgpRouteMaps, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "maxTotalRoutes": 12000, - "totalRoutesWarnLimit": 10000, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "maxTotalRoutes": 10000, - "totalRoutesWarnLimit": 9000, - } + {"peerAddress": "fd00:dc:1::1", "routeMapInbound": "RM-MLAG-PEER-IN1", "routeMapOutbound": "RM-MLAG-PEER-OUT"}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "routeMapInbound": "RM-MLAG-PEER-IN", "routeMapOutbound": "RM-MLAG-PEER-OUT1"}, ] }, + "MGMT": {"peerList": [{"routeMapInbound": "RM-MLAG-PEER-IN1", "routeMapOutbound": "RM-MLAG-PEER-OUT1", "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, + { + "peer_address": "fe80::250:56ff:fe01:112%Vl4094", + "vrf": "default", + "inbound_route_map": "RM-MLAG-PEER-IN", + "outbound_route_map": "RM-MLAG-PEER-OUT", }, - }, + {"interface": "Ethernet1", "vrf": "MGMT", "inbound_route_map": "RM-MLAG-PEER-IN", "outbound_route_map": "RM-MLAG-PEER-OUT"}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: RM-MLAG-PEER-IN1", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Outbound route-map mismatch - Expected: RM-MLAG-PEER-OUT Actual: RM-MLAG-PEER-OUT1", + "Interface: Ethernet1 VRF: MGMT - Inbound route-map mismatch - Expected: RM-MLAG-PEER-IN Actual: RM-MLAG-PEER-IN1", + "Interface: Ethernet1 VRF: MGMT - Outbound route-map mismatch - Expected: RM-MLAG-PEER-OUT Actual: RM-MLAG-PEER-OUT1", + ], + }, + }, + (VerifyBGPPeerRouteLimit, "success"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "maxTotalRoutes": 12000, "totalRoutesWarnLimit": 10000}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "maxTotalRoutes": 10000, "totalRoutesWarnLimit": 9000}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4187,32 +3536,39 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.9", "vrf": "MGMT", "maximum_routes": 10000}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-no-warning-limit", - "test": VerifyBGPPeerRouteLimit, + (VerifyBGPPeerRouteLimit, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "maxTotalRoutes": 12000, - } + {"peerAddress": "fd00:dc:1::1", "maxTotalRoutes": 12000, "totalRoutesWarnLimit": 10000}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "maxTotalRoutes": 12000, "totalRoutesWarnLimit": 10000}, ] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "maxTotalRoutes": 10000, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"maxTotalRoutes": 10000, "totalRoutesWarnLimit": 9000, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, + {"interface": "Ethernet1", "vrf": "MGMT", "maximum_routes": 10000}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerRouteLimit, "success-no-warning-limit"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "maxTotalRoutes": 12000}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "maxTotalRoutes": 10000}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4220,18 +3576,26 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.0.9", "vrf": "MGMT", "maximum_routes": 10000}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-peer-not-found", - "test": VerifyBGPPeerRouteLimit, + (VerifyBGPPeerRouteLimit, "failure-peer-not-found"): { + "eos_data": [{"vrfs": {"default": {}, "MGMT": {}}}], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, + {"peer_address": "10.100.0.9", "vrf": "MGMT", "maximum_routes": 10000, "warning_limit": 9000}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.0.9 VRF: MGMT - Not found"]}, + }, + (VerifyBGPPeerRouteLimit, "failure-incorrect-max-routes"): { "eos_data": [ { "vrfs": { - "default": {}, - "MGMT": {}, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8", "maxTotalRoutes": 13000, "totalRoutesWarnLimit": 11000}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "maxTotalRoutes": 11000, "totalRoutesWarnLimit": 10000}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4240,48 +3604,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Not found", - "Peer: 10.100.0.9 VRF: MGMT - Not found", - ], - }, - }, - { - "name": "failure-incorrect-max-routes", - "test": VerifyBGPPeerRouteLimit, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "maxTotalRoutes": 13000, - "totalRoutesWarnLimit": 11000, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "maxTotalRoutes": 11000, - "totalRoutesWarnLimit": 10000, - } - ] - }, - }, - }, - ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, - {"peer_address": "10.100.0.9", "vrf": "MGMT", "maximum_routes": 10000, "warning_limit": 9000}, - ] - }, - "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Maximum routes mismatch - Expected: 12000 Actual: 13000", "Peer: 10.100.0.8 VRF: default - Maximum routes warning limit mismatch - Expected: 10000 Actual: 11000", @@ -4290,30 +3613,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-routes-not-found", - "test": VerifyBGPPeerRouteLimit, + (VerifyBGPPeerRouteLimit, "failure-routes-not-found"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "maxTotalRoutes": 12000, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "maxTotalRoutes": 10000, - } - ] - }, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8", "maxTotalRoutes": 12000}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "maxTotalRoutes": 10000}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4322,100 +3629,72 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Maximum routes warning limit mismatch - Expected: 10000 Actual: 0", "Peer: 10.100.0.9 VRF: MGMT - Maximum routes warning limit mismatch - Expected: 9000 Actual: 0", ], }, }, - { - "name": "success-no-check-tcp-queues", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerRouteLimit, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } + {"peerAddress": "fd00:dc:1::1", "maxTotalRoutes": 10000, "totalRoutesWarnLimit": 9000}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "maxTotalRoutes": 10000, "totalRoutesWarnLimit": 9000}, ] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"maxTotalRoutes": 11000, "totalRoutesWarnLimit": 9000, "ifName": "Ethernet1"}]}, + } + } ], "inputs": { - "check_tcp_queues": False, "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + {"peer_address": "fd00:dc:1::1", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "maximum_routes": 12000, "warning_limit": 10000}, + {"interface": "Ethernet1", "vrf": "MGMT", "maximum_routes": 10000}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Maximum routes mismatch - Expected: 12000 Actual: 10000", + "Peer: fd00:dc:1::1 VRF: default - Maximum routes warning limit mismatch - Expected: 10000 Actual: 9000", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Maximum routes mismatch - Expected: 12000 Actual: 10000", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Maximum routes warning limit mismatch - Expected: 10000 Actual: 9000", + "Interface: Ethernet1 VRF: MGMT - Maximum routes mismatch - Expected: 10000 Actual: 11000", ], }, - "expected": {"result": "success"}, }, - { - "name": "success-check-tcp-queues", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerSession, "success-no-check-tcp-queues"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}]}, + } + } ], - "inputs": { - "check_tcp_queues": True, - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ], - }, - "expected": {"result": "success"}, + "inputs": {"check_tcp_queues": False, "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-min-established-time", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerSession, "success-check-tcp-queues"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } + ], + "inputs": {"check_tcp_queues": True, "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerSession, "success-min-established-time"): { "eos_data": [ { "vrfs": { @@ -4425,10 +3704,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.8", "state": "Established", "establishedTime": 169883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, @@ -4438,156 +3714,108 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.9", "state": "Established", "establishedTime": 169883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, - }, - }, + } + } ], "inputs": { "minimum_established_time": 10000, "check_tcp_queues": True, - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ], + "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-peer-not-found", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerSession, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ { - "peerAddress": "10.100.0.8", + "peerAddress": "fd00:dc:1::1", "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, - ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.9 VRF: MGMT - Not found", - ], - }, - }, - { - "name": "failure-not-established", - "test": VerifyBGPPeerSession, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ + "establishedTime": 169883, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, + }, { - "peerAddress": "10.100.0.8", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", + "state": "Established", + "establishedTime": 169883, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, + }, ] }, "MGMT": { "peerList": [ { - "peerAddress": "10.100.0.9", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "state": "Established", + "establishedTime": 169883, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, + "ifName": "Ethernet1", } ] }, - }, - }, + } + } ], "inputs": { + "minimum_established_time": 11000, + "check_tcp_queues": True, "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, + ], }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerSession, "failure-peer-not-found"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]} + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.9 VRF: MGMT - Not found"]}, + }, + (VerifyBGPPeerSession, "failure-not-established"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Incorrect session state - Expected: Established Actual: Active", "Peer: 10.100.0.9 VRF: MGMT - Incorrect session state - Expected: Established Actual: Active", ], }, }, - { - "name": "failure-check-tcp-queues", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerSession, "failure-check-tcp-queues"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10", - ], - }, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10"]}, }, - { - "name": "failure-min-established-time", - "test": VerifyBGPPeerSession, + (VerifyBGPPeerSession, "failure-min-established-time"): { "eos_data": [ { "vrfs": { @@ -4597,10 +3825,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.8", "state": "Established", "establishedTime": 9883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, @@ -4610,68 +3835,45 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.9", "state": "Established", "establishedTime": 9883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, - }, - }, + } + } ], "inputs": { "minimum_established_time": 10000, "check_tcp_queues": True, - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ], + "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}], }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - BGP session not established for the minimum required duration - Expected: 10000s Actual: 9883s", "Peer: 10.100.0.9 VRF: MGMT - BGP session not established for the minimum required duration - Expected: 10000s Actual: 9883s", ], }, }, - { - "name": "success", - "test": VerifyBGPPeerGroup, + (VerifyBGPPeerGroup, "success"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "peerGroupName": "IPv4-UNDERLAY-PEERS", - }, - { - "peerAddress": "10.100.4.5", - "peerGroupName": "MLAG-IPv4-UNDERLAY-PEER", - }, - { - "peerAddress": "10.100.1.1", - "peerGroupName": "EVPN-OVERLAY-PEERS", - }, + {"peerAddress": "10.100.0.8", "peerGroupName": "IPv4-UNDERLAY-PEERS"}, + {"peerAddress": "10.100.4.5", "peerGroupName": "MLAG-IPv4-UNDERLAY-PEER"}, + {"peerAddress": "10.100.1.1", "peerGroupName": "EVPN-OVERLAY-PEERS"}, ] }, "MGMT": { "peerList": [ - { - "peerAddress": "10.100.0.10", - "peerGroupName": "IPv4-UNDERLAY-PEERS", - }, - { - "peerAddress": "10.100.1.2", - "peerGroupName": "EVPN-OVERLAY-PEERS", - }, + {"peerAddress": "10.100.0.10", "peerGroupName": "IPv4-UNDERLAY-PEERS"}, + {"peerAddress": "10.100.1.2", "peerGroupName": "EVPN-OVERLAY-PEERS"}, ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -4682,44 +3884,50 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "10.100.4.5", "vrf": "default", "peer_group": "MLAG-IPv4-UNDERLAY-PEER"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-peer-group", - "test": VerifyBGPPeerGroup, + (VerifyBGPPeerGroup, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "peerGroupName": "UNDERLAY-PEERS", - }, - { - "peerAddress": "10.100.1.1", - "peerGroupName": "OVERLAY-PEERS", - }, - { - "peerAddress": "10.100.4.5", - "peerGroupName": "UNDERLAY-PEER", - }, + {"peerAddress": "fd00:dc:1::1", "peerGroupName": "IPv4-UNDERLAY-PEERS"}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "peerGroupName": "EVPN-OVERLAY-PEERS"}, + ] + }, + "MGMT": {"peerList": [{"peerGroupName": "EVPN-OVERLAY-PEERS", "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"interface": "Ethernet1", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerGroup, "failure-incorrect-peer-group"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "10.100.0.8", "peerGroupName": "UNDERLAY-PEERS"}, + {"peerAddress": "10.100.1.1", "peerGroupName": "OVERLAY-PEERS"}, + {"peerAddress": "10.100.4.5", "peerGroupName": "UNDERLAY-PEER"}, ] }, "MGMT": { "peerList": [ - { - "peerAddress": "10.100.0.10", - "peerGroupName": "UNDERLAY-PEERS", - }, - { - "peerAddress": "10.100.1.2", - "peerGroupName": "OVERLAY-PEERS", - }, + {"peerAddress": "10.100.0.10", "peerGroupName": "UNDERLAY-PEERS"}, + {"peerAddress": "10.100.1.2", "peerGroupName": "OVERLAY-PEERS"}, ] }, - }, - }, + } + } ], "inputs": { "bgp_peers": [ @@ -4731,7 +3939,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: UNDERLAY-PEERS", "Peer: 10.100.0.10 VRF: MGMT - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: UNDERLAY-PEERS", @@ -4741,17 +3949,8 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-peers-not-found", - "test": VerifyBGPPeerGroup, - "eos_data": [ - { - "vrfs": { - "default": {"peerList": []}, - "MGMT": {"peerList": []}, - }, - }, - ], + (VerifyBGPPeerGroup, "failure-peers-not-found"): { + "eos_data": [{"vrfs": {"default": {"peerList": []}, "MGMT": {"peerList": []}}}], "inputs": { "bgp_peers": [ {"peer_address": "10.100.0.8", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, @@ -4762,7 +3961,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.0.10 VRF: MGMT - Not found", @@ -4772,37 +3971,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-peer-group-not-found", - "test": VerifyBGPPeerGroup, + (VerifyBGPPeerGroup, "failure-peer-group-not-found"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - }, - { - "peerAddress": "10.100.1.1", - }, - { - "peerAddress": "10.100.4.5", - }, - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.10", - }, - { - "peerAddress": "10.100.1.2", - }, - ] - }, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8"}, {"peerAddress": "10.100.1.1"}, {"peerAddress": "10.100.4.5"}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.10"}, {"peerAddress": "10.100.1.2"}]}, + } + } ], "inputs": { "bgp_peers": [ @@ -4814,7 +3990,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: Not Found", "Peer: 10.100.0.10 VRF: MGMT - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: Not Found", @@ -4824,51 +4000,51 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success-no-check-tcp-queues", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerGroup, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } + {"peerAddress": "fd00:dc:1::1", "peerGroupName": "IPv6-UNDERLAY-PEERS"}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "peerGroupName": "EVPN-UNDERLAY-PEERS"}, ] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerGroupName": "EVPN-UNDERLAY-PEERS", "ifName": "Ethernet1"}]}, + } + } ], "inputs": { - "check_tcp_queues": False, "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + {"peer_address": "fd00:dc:1::1", "vrf": "default", "peer_group": "IPv4-UNDERLAY-PEERS"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "peer_group": "EVPN-OVERLAY-PEERS"}, + {"interface": "Ethernet1", "vrf": "MGMT", "peer_group": "EVPN-OVERLAY-PEERS"}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Incorrect peer group configured - Expected: IPv4-UNDERLAY-PEERS Actual: IPv6-UNDERLAY-PEERS", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: EVPN-UNDERLAY-PEERS", + "Interface: Ethernet1 VRF: MGMT - Incorrect peer group configured - Expected: EVPN-OVERLAY-PEERS Actual: EVPN-UNDERLAY-PEERS", ], }, - "expected": {"result": "success"}, }, - { - "name": "success-check-tcp-queues", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerSessionRibd, "success-no-check-tcp-queues"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] + }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}]}, + } + } + ], + "inputs": {"check_tcp_queues": False, "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerSessionRibd, "success-check-tcp-queues"): { "eos_data": [ { "vrfs": { @@ -4878,10 +4054,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.8", "state": "Established", "establishedTime": 169883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, @@ -4891,198 +4064,104 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.9", "state": "Established", "establishedTime": 169883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, - }, - }, + } + } ], "inputs": { "minimum_established_time": 10000, "check_tcp_queues": True, - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ], + "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-min-established-time", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerSessionRibd, "success-min-established-time"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } + ], + "inputs": {"check_tcp_queues": True, "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerSessionRibd, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ + {"peerAddress": "fd00:dc:1::1", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}, { - "peerAddress": "10.100.0.8", + "peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } + "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}, + }, ] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}, "ifName": "Ethernet1"}]}, + } + } ], "inputs": { - "check_tcp_queues": True, + "check_tcp_queues": False, "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, ], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-peer-not-found", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerSessionRibd, "failure-peer-not-found"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]} + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.9 VRF: MGMT - Not found"]}, + }, + (VerifyBGPPeerSessionRibd, "failure-not-established"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } + ], + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.9 VRF: MGMT - Not found", - ], - }, - }, - { - "name": "failure-not-established", - "test": VerifyBGPPeerSessionRibd, - "eos_data": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, - ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Incorrect session state - Expected: Established Actual: Active", "Peer: 10.100.0.9 VRF: MGMT - Incorrect session state - Expected: Established Actual: Active", ], }, }, - { - "name": "failure-check-tcp-queues", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerSessionRibd, "failure-check-tcp-queues"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } ], - "inputs": { - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10", - ], - }, + "inputs": {"bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10"]}, }, - { - "name": "failure-min-established-time", - "test": VerifyBGPPeerSessionRibd, + (VerifyBGPPeerSessionRibd, "failure-min-established-time"): { "eos_data": [ { "vrfs": { @@ -5092,10 +4171,7 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.8", "state": "Established", "establishedTime": 9883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, @@ -5105,193 +4181,115 @@ DATA: list[dict[str, Any]] = [ "peerAddress": "10.100.0.9", "state": "Established", "establishedTime": 9883, - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, + "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}, } ] }, - }, - }, + } + } ], "inputs": { "minimum_established_time": 10000, - "bgp_peers": [ - {"peer_address": "10.100.0.8", "vrf": "default"}, - {"peer_address": "10.100.0.9", "vrf": "MGMT"}, - ], + "bgp_peers": [{"peer_address": "10.100.0.8", "vrf": "default"}, {"peer_address": "10.100.0.9", "vrf": "MGMT"}], }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - BGP session not established for the minimum required duration - Expected: 10000s Actual: 9883s", "Peer: 10.100.0.9 VRF: MGMT - BGP session not established for the minimum required duration - Expected: 10000s Actual: 9883s", ], }, }, - { - "name": "success-no-check-tcp-queues", - "test": VerifyBGPPeersHealthRibd, + (VerifyBGPPeerSessionRibd, "failure-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } + {"peerAddress": "fd00:dc:1::1", "state": "Active", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "state": "Active", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}, ] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"state": "Active", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}, "ifName": "Ethernet1"}]}, + } + } ], "inputs": { "check_tcp_queues": False, + "bgp_peers": [ + {"peer_address": "fd00:dc:1::1", "vrf": "default"}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default"}, + {"interface": "Ethernet1", "vrf": "MGMT"}, + ], + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: fd00:dc:1::1 VRF: default - Incorrect session state - Expected: Established Actual: Active", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - Incorrect session state - Expected: Established Actual: Active", + "Interface: Ethernet1 VRF: MGMT - Incorrect session state - Expected: Established Actual: Active", + ], }, - "expected": {"result": "success"}, }, - { - "name": "success-check-tcp-queues", - "test": VerifyBGPPeersHealthRibd, + (VerifyBGPPeersHealthRibd, "success-no-check-tcp-queues"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}]}, + } + } ], - "inputs": { - "check_tcp_queues": True, - }, - "expected": {"result": "success"}, + "inputs": {"check_tcp_queues": False}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-established", - "test": VerifyBGPPeersHealthRibd, + (VerifyBGPPeersHealthRibd, "success-check-tcp-queues"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Active", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } + ], + "inputs": {"check_tcp_queues": True}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeersHealthRibd, "failure-not-established"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.100.0.8", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Active", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - Incorrect session state - Expected: Established Actual: Active", "Peer: 10.100.0.9 VRF: MGMT - Incorrect session state - Expected: Established Actual: Active", ], }, }, - { - "name": "failure-check-tcp-queues", - "test": VerifyBGPPeersHealthRibd, + (VerifyBGPPeersHealthRibd, "failure-check-tcp-queues"): { "eos_data": [ { "vrfs": { "default": { - "peerList": [ - { - "peerAddress": "10.100.0.8", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 10, - "inputQueueLength": 5, - }, - } - ] + "peerList": [{"peerAddress": "10.100.0.8", "state": "Established", "peerTcpInfo": {"outputQueueLength": 10, "inputQueueLength": 5}}] }, - "MGMT": { - "peerList": [ - { - "peerAddress": "10.100.0.9", - "state": "Established", - "peerTcpInfo": { - "outputQueueLength": 0, - "inputQueueLength": 0, - }, - } - ] - }, - }, - }, + "MGMT": {"peerList": [{"peerAddress": "10.100.0.9", "state": "Established", "peerTcpInfo": {"outputQueueLength": 0, "inputQueueLength": 0}}]}, + } + } ], "inputs": {}, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Session has non-empty message queues - InQ: 5 OutQ: 10"]}, }, - { - "name": "success", - "test": VerifyBGPNlriAcceptance, + (VerifyBGPNlriAcceptance, "success"): { "eos_data": [ { "vrfs": { @@ -5305,7 +4303,7 @@ DATA: list[dict[str, Any]] = [ "peerAsn": "65100", "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 56, "nlrisAccepted": 56}, - }, + } }, }, "MGMT": { @@ -5322,70 +4320,75 @@ DATA: list[dict[str, Any]] = [ }, }, } - } + }, + {}, ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.100.0.8", - "vrf": "default", - "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"], - }, - { - "peer_address": "10.100.4.5", - "vrf": "MGMT", - "capabilities": ["ipv4 Unicast", "L2vpnEVPN"], - }, + {"peer_address": "10.100.0.8", "vrf": "default", "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"]}, + {"peer_address": "10.100.4.5", "vrf": "MGMT", "capabilities": ["ipv4 Unicast", "L2vpnEVPN"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-vrf-not-configured", - "test": VerifyBGPNlriAcceptance, + (VerifyBGPNlriAcceptance, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "vrf": "default", - "routerId": "10.100.1.5", - "asn": "65102", - "peers": {}, - }, - "MGMT": { - "vrf": "MGMT", - "routerId": "10.100.1.5", - "asn": "65102", - "peers": {}, - }, + "routerId": "1.1.1.1", + "asn": "65001", + "peers": { + "2001:db8:1::2": { + "peerState": "Established", + "peerAsn": "65003", + "ipv6Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 2, "nlrisAccepted": 2}, + }, + "fe80::2%Et1": { + "peerState": "Established", + "peerAsn": "65002", + "ipv6Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 1, "nlrisAccepted": 1}, + }, + "fe80::3%Et2": { + "peerState": "Established", + "peerAsn": "65002", + "ipv6Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 1, "nlrisAccepted": 1}, + }, + }, + } } - } + }, + {"vrfs": {"default": {"peerList": [{"peerAddress": "fe80::3%Et2", "ifName": "Ethernet2"}]}}}, ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.100.0.8", - "vrf": "default", - "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"], - }, - { - "peer_address": "10.100.4.5", - "vrf": "MGMT", - "capabilities": ["ipv4 Unicast", "L2vpnEVPN"], - }, + {"peer_address": "2001:db8:1::2", "vrf": "default", "capabilities": ["ipv6Unicast"]}, + {"peer_address": "fe80::2%Et1", "vrf": "default", "capabilities": ["ipv6Unicast"]}, + {"interface": "Ethernet2", "vrf": "default", "capabilities": ["ipv6Unicast"]}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - Not found", - "Peer: 10.100.4.5 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-capability-not-found", - "test": VerifyBGPNlriAcceptance, + (VerifyBGPNlriAcceptance, "failure-vrf-not-configured"): { + "eos_data": [ + { + "vrfs": { + "default": {"vrf": "default", "routerId": "10.100.1.5", "asn": "65102", "peers": {}}, + "MGMT": {"vrf": "MGMT", "routerId": "10.100.1.5", "asn": "65102", "peers": {}}, + } + }, + {}, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.100.0.8", "vrf": "default", "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"]}, + {"peer_address": "10.100.4.5", "vrf": "MGMT", "capabilities": ["ipv4 Unicast", "L2vpnEVPN"]}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.100.0.8 VRF: default - Not found", "Peer: 10.100.4.5 VRF: MGMT - Not found"]}, + }, + (VerifyBGPNlriAcceptance, "failure-capability-not-found"): { "eos_data": [ { "vrfs": { @@ -5398,7 +4401,7 @@ DATA: list[dict[str, Any]] = [ "peerState": "Established", "peerAsn": "65100", "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 17}, - }, + } }, }, "MGMT": { @@ -5414,33 +4417,21 @@ DATA: list[dict[str, Any]] = [ }, }, } - } + }, + {}, ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.100.0.8", - "vrf": "default", - "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"], - }, - { - "peer_address": "10.100.4.5", - "vrf": "MGMT", - "capabilities": ["ipv4 Unicast", "L2vpnEVPN"], - }, + {"peer_address": "10.100.0.8", "vrf": "default", "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"]}, + {"peer_address": "10.100.4.5", "vrf": "MGMT", "capabilities": ["ipv4 Unicast", "L2vpnEVPN"]}, ] }, "expected": { - "result": "failure", - "messages": [ - "Peer: 10.100.0.8 VRF: default - l2VpnEvpn not found", - "Peer: 10.100.4.5 VRF: MGMT - ipv4Unicast not found", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Peer: 10.100.0.8 VRF: default - l2VpnEvpn not found", "Peer: 10.100.4.5 VRF: MGMT - ipv4Unicast not found"], }, }, - { - "name": "failure-capability-not-negotiated", - "test": VerifyBGPNlriAcceptance, + (VerifyBGPNlriAcceptance, "failure-capability-not-negotiated"): { "eos_data": [ { "vrfs": { @@ -5453,7 +4444,7 @@ DATA: list[dict[str, Any]] = [ "peerState": "Established", "peerAsn": "65100", "ipv4Unicast": {"afiSafiState": "configured", "nlrisReceived": 17, "nlrisAccepted": 17}, - }, + } }, }, "MGMT": { @@ -5469,24 +4460,17 @@ DATA: list[dict[str, Any]] = [ }, }, } - } + }, + {}, ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.100.0.8", - "vrf": "default", - "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"], - }, - { - "peer_address": "10.100.4.5", - "vrf": "MGMT", - "capabilities": ["ipv4 Unicast", "L2vpnEVPN"], - }, + {"peer_address": "10.100.0.8", "vrf": "default", "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"]}, + {"peer_address": "10.100.4.5", "vrf": "MGMT", "capabilities": ["ipv4 Unicast", "L2vpnEVPN"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default - ipv4Unicast not negotiated", "Peer: 10.100.0.8 VRF: default - l2VpnEvpn not found", @@ -5495,9 +4479,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-nlris-not-accepted", - "test": VerifyBGPNlriAcceptance, + (VerifyBGPNlriAcceptance, "failure-nlris-not-accepted"): { "eos_data": [ { "vrfs": { @@ -5511,7 +4493,7 @@ DATA: list[dict[str, Any]] = [ "peerAsn": "65100", "ipv4Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 17, "nlrisAccepted": 16}, "l2VpnEvpn": {"afiSafiState": "negotiated", "nlrisReceived": 58, "nlrisAccepted": 56}, - }, + } }, }, "MGMT": { @@ -5528,24 +4510,17 @@ DATA: list[dict[str, Any]] = [ }, }, } - } + }, + {}, ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.100.0.8", - "vrf": "default", - "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"], - }, - { - "peer_address": "10.100.4.5", - "vrf": "MGMT", - "capabilities": ["ipv4 Unicast", "L2vpnEVPN"], - }, + {"peer_address": "10.100.0.8", "vrf": "default", "capabilities": ["Ipv4 Unicast", "L2vpnEVPN"]}, + {"peer_address": "10.100.4.5", "vrf": "MGMT", "capabilities": ["ipv4 Unicast", "L2vpnEVPN"]}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.100.0.8 VRF: default AFI/SAFI: ipv4Unicast - Some NLRI were filtered or rejected - Accepted: 16 Received: 17", "Peer: 10.100.0.8 VRF: default AFI/SAFI: l2VpnEvpn - Some NLRI were filtered or rejected - Accepted: 56 Received: 58", @@ -5554,9 +4529,59 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBGPRoutePaths, + (VerifyBGPNlriAcceptance, "failure-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "vrf": "default", + "routerId": "1.1.1.1", + "asn": "65001", + "peers": { + "2001:db8:1::2": { + "peerState": "Established", + "peerAsn": "65003", + "ipv6Unicast": {"afiSafiState": "configured", "nlrisReceived": 2, "nlrisAccepted": 3}, + }, + "fe80::2%Et1": { + "peerState": "Established", + "peerAsn": "65002", + "ipv6Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 2, "nlrisAccepted": 1}, + }, + "fe80::3%Et2": { + "peerState": "Established", + "peerAsn": "65002", + "ipv6Unicast": {"afiSafiState": "negotiated", "nlrisReceived": 3, "nlrisAccepted": 1}, + }, + }, + } + } + }, + {"vrfs": {"default": {"peerList": [{"peerAddress": "fe80::3%Et2", "ifName": "Ethernet2"}]}}}, + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "2001:db8:1::2", "vrf": "default", "capabilities": ["ipv6Unicast"]}, + {"peer_address": "fe80::2%Et1", "vrf": "default", "capabilities": ["ipv6Unicast"]}, + {"interface": "Ethernet2", "vrf": "default", "capabilities": ["ipv6Unicast"]}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: 2001:db8:1::2 VRF: default - ipv6Unicast not negotiated", + "Peer: 2001:db8:1::2 VRF: default AFI/SAFI: ipv6Unicast - Some NLRI were filtered or rejected - Accepted: 3 Received: 2", + "Peer: fe80::2%Et1 VRF: default AFI/SAFI: ipv6Unicast - Some NLRI were filtered or rejected - Accepted: 1 Received: 2", + "Interface: Ethernet2 VRF: default AFI/SAFI: ipv6Unicast - Some NLRI were filtered or rejected - Accepted: 1 Received: 3", + ], + }, + }, + (VerifyBGPNlriAcceptance, "failure-rfc5549-not-found"): { + "eos_data": [{"vrfs": {"default": {}}}, {"vrfs": {"default": {"peerList": []}}}], + "inputs": {"bgp_peers": [{"interface": "Ethernet2", "vrf": "default", "capabilities": ["ipv6Unicast"]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 VRF: default - Not found"]}, + }, + (VerifyBGPRoutePaths, "success"): { "eos_data": [ { "vrfs": { @@ -5564,19 +4589,9 @@ DATA: list[dict[str, Any]] = [ "bgpRouteEntries": { "10.100.0.128/31": { "bgpRoutePaths": [ - { - "nextHop": "10.100.0.10", - "routeType": { - "origin": "Igp", - }, - }, - { - "nextHop": "10.100.4.5", - "routeType": { - "origin": "Incomplete", - }, - }, - ], + {"nextHop": "10.100.0.10", "routeType": {"origin": "Igp"}}, + {"nextHop": "10.100.4.5", "routeType": {"origin": "Incomplete"}}, + ] } } }, @@ -5584,24 +4599,14 @@ DATA: list[dict[str, Any]] = [ "bgpRouteEntries": { "10.100.0.130/31": { "bgpRoutePaths": [ - { - "nextHop": "10.100.0.8", - "routeType": { - "origin": "Igp", - }, - }, - { - "nextHop": "10.100.0.10", - "routeType": { - "origin": "Igp", - }, - }, - ], + {"nextHop": "10.100.0.8", "routeType": {"origin": "Igp"}}, + {"nextHop": "10.100.0.10", "routeType": {"origin": "Igp"}}, + ] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -5610,18 +4615,12 @@ DATA: list[dict[str, Any]] = [ "vrf": "default", "paths": [{"nexthop": "10.100.0.10", "origin": "Igp"}, {"nexthop": "10.100.4.5", "origin": "Incomplete"}], }, - { - "prefix": "10.100.0.130/31", - "vrf": "MGMT", - "paths": [{"nexthop": "10.100.0.8", "origin": "Igp"}, {"nexthop": "10.100.0.10", "origin": "Igp"}], - }, + {"prefix": "10.100.0.130/31", "vrf": "MGMT", "paths": [{"nexthop": "10.100.0.8", "origin": "Igp"}, {"nexthop": "10.100.0.10", "origin": "Igp"}]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-origin-not-correct", - "test": VerifyBGPRoutePaths, + (VerifyBGPRoutePaths, "failure-origin-not-correct"): { "eos_data": [ { "vrfs": { @@ -5629,19 +4628,9 @@ DATA: list[dict[str, Any]] = [ "bgpRouteEntries": { "10.100.0.128/31": { "bgpRoutePaths": [ - { - "nextHop": "10.100.0.10", - "routeType": { - "origin": "Igp", - }, - }, - { - "nextHop": "10.100.4.5", - "routeType": { - "origin": "Incomplete", - }, - }, - ], + {"nextHop": "10.100.0.10", "routeType": {"origin": "Igp"}}, + {"nextHop": "10.100.4.5", "routeType": {"origin": "Incomplete"}}, + ] } } }, @@ -5649,24 +4638,14 @@ DATA: list[dict[str, Any]] = [ "bgpRouteEntries": { "10.100.0.130/31": { "bgpRoutePaths": [ - { - "nextHop": "10.100.0.8", - "routeType": { - "origin": "Igp", - }, - }, - { - "nextHop": "10.100.0.10", - "routeType": { - "origin": "Igp", - }, - }, - ], + {"nextHop": "10.100.0.8", "routeType": {"origin": "Igp"}}, + {"nextHop": "10.100.0.10", "routeType": {"origin": "Igp"}}, + ] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -5683,7 +4662,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.0.10 Origin: Incomplete - Origin mismatch - Actual: Igp", "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.4.5 Origin: Igp - Origin mismatch - Actual: Incomplete", @@ -5692,42 +4671,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-path-not-found", - "test": VerifyBGPRoutePaths, + (VerifyBGPRoutePaths, "failure-path-not-found"): { "eos_data": [ { "vrfs": { - "default": { - "bgpRouteEntries": { - "10.100.0.128/31": { - "bgpRoutePaths": [ - { - "nextHop": "10.100.0.15", - "routeType": { - "origin": "Igp", - }, - }, - ], - } - } - }, - "MGMT": { - "bgpRouteEntries": { - "10.100.0.130/31": { - "bgpRoutePaths": [ - { - "nextHop": "10.100.0.15", - "routeType": { - "origin": "Igp", - }, - }, - ], - } - } - }, + "default": {"bgpRouteEntries": {"10.100.0.128/31": {"bgpRoutePaths": [{"nextHop": "10.100.0.15", "routeType": {"origin": "Igp"}}]}}}, + "MGMT": {"bgpRouteEntries": {"10.100.0.130/31": {"bgpRoutePaths": [{"nextHop": "10.100.0.15", "routeType": {"origin": "Igp"}}]}}}, } - }, + } ], "inputs": { "route_entries": [ @@ -5744,7 +4695,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.0.10 Origin: Incomplete - Path not found", "Prefix: 10.100.0.128/31 VRF: default Next-hop: 10.100.4.5 Origin: Igp - Path not found", @@ -5753,12 +4704,8 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-prefix-not-found", - "test": VerifyBGPRoutePaths, - "eos_data": [ - {"vrfs": {"default": {"bgpRouteEntries": {}}, "MGMT": {"bgpRouteEntries": {}}}}, - ], + (VerifyBGPRoutePaths, "failure-prefix-not-found"): { + "eos_data": [{"vrfs": {"default": {"bgpRouteEntries": {}}, "MGMT": {"bgpRouteEntries": {}}}}], "inputs": { "route_entries": [ { @@ -5774,13 +4721,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.100.0.128/31 VRF: default - Prefix not found", "Prefix: 10.100.0.130/31 VRF: MGMT - Prefix not found"], }, }, - { - "name": "success", - "test": VerifyBGPRouteECMP, + (VerifyBGPRouteECMP, "success"): { "eos_data": [ { "vrfs": { @@ -5793,102 +4738,14 @@ DATA: list[dict[str, Any]] = [ "address": "10.111.134.0", "maskLength": 24, "bgpRoutePaths": [ - { - "nextHop": "10.111.2.0", - "routeType": { - "valid": True, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, - }, + {"nextHop": "10.111.2.0", "routeType": {"valid": True, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}}, { "nextHop": "10.111.1.0", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": True, "ecmpContributor": True}, }, ], "totalPaths": 2, - }, - }, - } - } - }, - { - "vrfs": { - "default": { - "routes": { - "10.111.112.0/24": {"routeType": "eBGP", "vias": [{"interface": "Vlan112"}]}, - "10.111.134.0/24": { - "routeType": "eBGP", - "vias": [ - {"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, - {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}, - ], - "directlyConnected": False, - }, - }, - } - } - }, - ], - "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "success"}, - }, - { - "name": "failure-prefix-not-found-bgp-table", - "test": VerifyBGPRouteECMP, - "eos_data": [ - { - "vrfs": { - "default": { - "vrf": "default", - "routerId": "10.111.254.1", - "asn": "65101", - "bgpRouteEntries": { - "10.111.134.0/24": { - "address": "10.111.134.0", - "maskLength": 24, - "bgpRoutePaths": [ - { - "nextHop": "10.111.1.0", - "routeType": { - "valid": True, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, - }, - { - "nextHop": "10.111.2.0", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": True, - "ecmpContributor": True, - }, - }, - { - "nextHop": "10.255.255.2", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": False, - "ecmpContributor": False, - }, - }, - ], - "totalPaths": 3, - }, + } }, } } @@ -5903,17 +4760,62 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}], "directlyConnected": False, }, + } + } + } + }, + ], + "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPRouteECMP, "failure-prefix-not-found-bgp-table"): { + "eos_data": [ + { + "vrfs": { + "default": { + "vrf": "default", + "routerId": "10.111.254.1", + "asn": "65101", + "bgpRouteEntries": { + "10.111.134.0/24": { + "address": "10.111.134.0", + "maskLength": 24, + "bgpRoutePaths": [ + {"nextHop": "10.111.1.0", "routeType": {"valid": True, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}}, + { + "nextHop": "10.111.2.0", + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": True, "ecmpContributor": True}, + }, + { + "nextHop": "10.255.255.2", + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": False, "ecmpContributor": False}, + }, + ], + "totalPaths": 3, + } }, } } }, + { + "vrfs": { + "default": { + "routes": { + "10.111.112.0/24": {"routeType": "eBGP", "vias": [{"interface": "Vlan112"}]}, + "10.111.134.0/24": { + "routeType": "eBGP", + "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}], + "directlyConnected": False, + }, + } + } + } + }, ], "inputs": {"route_entries": [{"prefix": "10.111.124.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.111.124.0/24 VRF: default - Prefix not found in BGP table"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.111.124.0/24 VRF: default - Prefix not found in BGP table"]}, }, - { - "name": "failure-valid-active-ecmp-head-not-found", - "test": VerifyBGPRouteECMP, + (VerifyBGPRouteECMP, "failure-valid-active-ecmp-head-not-found"): { "eos_data": [ { "vrfs": { @@ -5928,37 +4830,19 @@ DATA: list[dict[str, Any]] = [ "bgpRoutePaths": [ { "nextHop": "10.111.1.0", - "routeType": { - "valid": False, - "active": True, - "ecmpHead": False, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": False, "active": True, "ecmpHead": False, "ecmp": True, "ecmpContributor": True}, }, { "nextHop": "10.111.2.0", - "routeType": { - "valid": False, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": False, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}, }, { "nextHop": "10.255.255.2", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": False, - "ecmpContributor": False, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": False, "ecmpContributor": False}, }, ], "totalPaths": 3, - }, + } }, } } @@ -5973,17 +4857,15 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}], "directlyConnected": False, }, - }, + } } } }, ], "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.111.134.0/24 VRF: default - Valid and active ECMP head not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.111.134.0/24 VRF: default - Valid and active ECMP head not found"]}, }, - { - "name": "failure-ecmp-count-mismatch", - "test": VerifyBGPRouteECMP, + (VerifyBGPRouteECMP, "failure-ecmp-count-mismatch"): { "eos_data": [ { "vrfs": { @@ -5996,39 +4878,18 @@ DATA: list[dict[str, Any]] = [ "address": "10.111.134.0", "maskLength": 24, "bgpRoutePaths": [ - { - "nextHop": "10.111.1.0", - "routeType": { - "valid": True, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, - }, + {"nextHop": "10.111.1.0", "routeType": {"valid": True, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}}, { "nextHop": "10.111.2.0", - "routeType": { - "valid": False, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": False, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}, }, { "nextHop": "10.255.255.2", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": False, - "ecmpContributor": False, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": False, "ecmpContributor": False}, }, ], "totalPaths": 3, - }, + } }, } } @@ -6043,17 +4904,15 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}], "directlyConnected": False, }, - }, + } } } }, ], "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.111.134.0/24 VRF: default - ECMP count mismatch - Expected: 2 Actual: 1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.111.134.0/24 VRF: default - ECMP count mismatch - Expected: 2 Actual: 1"]}, }, - { - "name": "failure-prefix-not-found-routing-table", - "test": VerifyBGPRouteECMP, + (VerifyBGPRouteECMP, "failure-prefix-not-found-routing-table"): { "eos_data": [ { "vrfs": { @@ -6066,39 +4925,18 @@ DATA: list[dict[str, Any]] = [ "address": "10.111.134.0", "maskLength": 24, "bgpRoutePaths": [ - { - "nextHop": "10.111.1.0", - "routeType": { - "valid": True, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, - }, + {"nextHop": "10.111.1.0", "routeType": {"valid": True, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}}, { "nextHop": "10.111.2.0", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": True, "ecmpContributor": True}, }, { "nextHop": "10.255.255.2", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": False, - "ecmpContributor": False, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": False, "ecmpContributor": False}, }, ], "totalPaths": 3, - }, + } }, } } @@ -6113,17 +4951,15 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}, {"nexthopAddr": "10.111.2.0", "interface": "Ethernet3"}], "directlyConnected": False, }, - }, + } } } }, ], "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.111.134.0/24 VRF: default - Prefix not found in routing table"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.111.134.0/24 VRF: default - Prefix not found in routing table"]}, }, - { - "name": "failure-nexthops-mismatch", - "test": VerifyBGPRouteECMP, + (VerifyBGPRouteECMP, "failure-nexthops-mismatch"): { "eos_data": [ { "vrfs": { @@ -6136,39 +4972,18 @@ DATA: list[dict[str, Any]] = [ "address": "10.111.134.0", "maskLength": 24, "bgpRoutePaths": [ - { - "nextHop": "10.111.1.0", - "routeType": { - "valid": True, - "active": True, - "ecmpHead": True, - "ecmp": True, - "ecmpContributor": True, - }, - }, + {"nextHop": "10.111.1.0", "routeType": {"valid": True, "active": True, "ecmpHead": True, "ecmp": True, "ecmpContributor": True}}, { "nextHop": "10.111.2.0", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": True, - "ecmpContributor": True, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": True, "ecmpContributor": True}, }, { "nextHop": "10.255.255.2", - "routeType": { - "valid": True, - "active": False, - "ecmpHead": False, - "ecmp": False, - "ecmpContributor": False, - }, + "routeType": {"valid": True, "active": False, "ecmpHead": False, "ecmp": False, "ecmpContributor": False}, }, ], "totalPaths": 3, - }, + } }, } } @@ -6178,22 +4993,16 @@ DATA: list[dict[str, Any]] = [ "default": { "routes": { "10.111.112.0/24": {"routeType": "eBGP", "vias": [{"interface": "Vlan112"}]}, - "10.111.134.0/24": { - "routeType": "eBGP", - "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}], - "directlyConnected": False, - }, - }, + "10.111.134.0/24": {"routeType": "eBGP", "vias": [{"nexthopAddr": "10.111.1.0", "interface": "Ethernet2"}], "directlyConnected": False}, + } } } }, ], "inputs": {"route_entries": [{"prefix": "10.111.134.0/24", "vrf": "default", "ecmp_count": 2}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.111.134.0/24 VRF: default - Nexthops count mismatch - BGP: 2 RIB: 1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.111.134.0/24 VRF: default - Nexthops count mismatch - BGP: 2 RIB: 1"]}, }, - { - "name": "success", - "test": VerifyBGPRedistribution, + (VerifyBGPRedistribution, "success"): { "eos_data": [ { "vrfs": { @@ -6274,11 +5083,9 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-vrf-not-found", - "test": VerifyBGPRedistribution, + (VerifyBGPRedistribution, "failure-vrf-not-found"): { "eos_data": [ { "vrfs": { @@ -6292,30 +5099,20 @@ DATA: list[dict[str, Any]] = [ { "vrf": "default", "address_families": [ - { - "afi_safi": "ipv6 Multicast", - "redistributed_routes": [{"proto": "Connected", "include_leaked": False, "route_map": "RM-CONN-2-BGP"}], - }, + {"afi_safi": "ipv6 Multicast", "redistributed_routes": [{"proto": "Connected", "include_leaked": False, "route_map": "RM-CONN-2-BGP"}]} ], }, { "vrf": "test", "address_families": [ - { - "afi_safi": "ipv6 Multicast", - "redistributed_routes": [ - {"proto": "Connected", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}, - ], - }, + {"afi_safi": "ipv6 Multicast", "redistributed_routes": [{"proto": "Connected", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}]} ], }, ] }, - "expected": {"result": "failure", "messages": ["VRF: test - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: test - Not configured"]}, }, - { - "name": "failure-afi-safi-config-not-found", - "test": VerifyBGPRedistribution, + (VerifyBGPRedistribution, "failure-afi-safi-config-not-found"): { "eos_data": [ { "vrfs": { @@ -6335,16 +5132,14 @@ DATA: list[dict[str, Any]] = [ {"proto": "Connected", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}, {"proto": "Static", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}, ], - }, + } ], - }, + } ] }, - "expected": {"result": "failure", "messages": ["VRF: default, AFI-SAFI: IPv6 Multicast - Not redistributed"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: default, AFI-SAFI: IPv6 Multicast - Not redistributed"]}, }, - { - "name": "failure-expected-proto-not-found", - "test": VerifyBGPRedistribution, + (VerifyBGPRedistribution, "failure-expected-proto-not-found"): { "eos_data": [ { "vrfs": { @@ -6355,13 +5150,7 @@ DATA: list[dict[str, Any]] = [ } } }, - "test": { - "afiSafiConfig": { - "v6u": { - "redistributedRoutes": [{"proto": "Static", "routeMap": "RM-CONN-2-BGP"}], - } - } - }, + "test": {"afiSafiConfig": {"v6u": {"redistributedRoutes": [{"proto": "Static", "routeMap": "RM-CONN-2-BGP"}]}}}, } } ], @@ -6388,13 +5177,13 @@ DATA: list[dict[str, Any]] = [ {"proto": "DHCP", "route_map": "RM-CONN-2-BGP"}, {"proto": "Bgp", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}, ], - }, + } ], }, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VRF: default, AFI-SAFI: IPv4 Multicast, Proto: OSPFv3 External - Not configured", "VRF: default, AFI-SAFI: IPv4 Multicast, Proto: OSPFv3 Nssa-External - Not configured", @@ -6403,17 +5192,68 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-route-map-not-found", - "test": VerifyBGPRedistribution, + (VerifyBGPRedistribution, "failure-route-map-not-found"): { "eos_data": [ { "vrfs": { "default": {"afiSafiConfig": {"v4u": {"redistributedRoutes": [{"proto": "Connected", "routeMap": "RM-CONN-10-BGP"}, {"proto": "Static"}]}}}, + "test": {"afiSafiConfig": {"v6u": {"redistributedRoutes": [{"proto": "EOS SDK", "routeMap": "RM-MLAG-PEER-IN"}, {"proto": "DHCP"}]}}}, + } + } + ], + "inputs": { + "vrfs": [ + { + "vrf": "default", + "address_families": [ + { + "afi_safi": "ipv4 UNicast", + "redistributed_routes": [{"proto": "Connected", "route_map": "RM-CONN-2-BGP"}, {"proto": "Static", "route_map": "RM-CONN-2-BGP"}], + } + ], + }, + { + "vrf": "test", + "address_families": [ + { + "afi_safi": "ipv6-Unicast", + "redistributed_routes": [{"proto": "User", "route_map": "RM-CONN-2-BGP"}, {"proto": "DHCP", "route_map": "RM-CONN-2-BGP"}], + } + ], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "VRF: default, AFI-SAFI: IPv4 Unicast, Proto: Connected, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: RM-CONN-10-BGP", + "VRF: default, AFI-SAFI: IPv4 Unicast, Proto: Static, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: Not Found", + "VRF: test, AFI-SAFI: IPv6 Unicast, Proto: EOS SDK, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: RM-MLAG-PEER-IN", + "VRF: test, AFI-SAFI: IPv6 Unicast, Proto: DHCP, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: Not Found", + ], + }, + }, + (VerifyBGPRedistribution, "failure-incorrect-value-include-leaked"): { + "eos_data": [ + { + "vrfs": { + "default": { + "afiSafiConfig": { + "v4m": { + "redistributedRoutes": [ + {"proto": "Connected", "routeMap": "RM-CONN-2-BGP"}, + {"proto": "IS-IS", "includeLeaked": False, "routeMap": "RM-CONN-2-BGP"}, + ] + } + } + }, "test": { "afiSafiConfig": { "v6u": { - "redistributedRoutes": [{"proto": "EOS SDK", "routeMap": "RM-MLAG-PEER-IN"}, {"proto": "DHCP"}], + "redistributedRoutes": [ + {"proto": "Dynamic", "routeMap": "RM-CONN-2-BGP"}, + {"proto": "Bgp", "includeLeaked": True, "routeMap": "RM-CONN-2-BGP"}, + ] } } }, @@ -6425,79 +5265,7 @@ DATA: list[dict[str, Any]] = [ { "vrf": "default", "address_families": [ - { - "afi_safi": "ipv4 UNicast", - "redistributed_routes": [ - {"proto": "Connected", "route_map": "RM-CONN-2-BGP"}, - {"proto": "Static", "route_map": "RM-CONN-2-BGP"}, - ], - }, - ], - }, - { - "vrf": "test", - "address_families": [ - { - "afi_safi": "ipv6-Unicast", - "redistributed_routes": [ - {"proto": "User", "route_map": "RM-CONN-2-BGP"}, - {"proto": "DHCP", "route_map": "RM-CONN-2-BGP"}, - ], - }, - ], - }, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "VRF: default, AFI-SAFI: IPv4 Unicast, Proto: Connected, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: RM-CONN-10-BGP", - "VRF: default, AFI-SAFI: IPv4 Unicast, Proto: Static, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: Not Found", - "VRF: test, AFI-SAFI: IPv6 Unicast, Proto: EOS SDK, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: RM-MLAG-PEER-IN", - "VRF: test, AFI-SAFI: IPv6 Unicast, Proto: DHCP, Route Map: RM-CONN-2-BGP - Route map mismatch - Actual: Not Found", - ], - }, - }, - { - "name": "failure-incorrect-value-include-leaked", - "test": VerifyBGPRedistribution, - "eos_data": [ - { - "vrfs": { - "default": { - "afiSafiConfig": { - "v4m": { - "redistributedRoutes": [ - {"proto": "Connected", "routeMap": "RM-CONN-2-BGP"}, - {"proto": "IS-IS", "includeLeaked": False, "routeMap": "RM-CONN-2-BGP"}, - ] - }, - } - }, - "test": { - "afiSafiConfig": { - "v6u": { - "redistributedRoutes": [ - {"proto": "Dynamic", "routeMap": "RM-CONN-2-BGP"}, - {"proto": "Bgp", "includeLeaked": True, "routeMap": "RM-CONN-2-BGP"}, - ] - }, - } - }, - } - } - ], - "inputs": { - "vrfs": [ - { - "vrf": "default", - "address_families": [ - { - "afi_safi": "ipv4-multicast", - "redistributed_routes": [ - {"proto": "IS-IS", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}, - ], - }, + {"afi_safi": "ipv4-multicast", "redistributed_routes": [{"proto": "IS-IS", "include_leaked": True, "route_map": "RM-CONN-2-BGP"}]} ], }, { @@ -6509,157 +5277,95 @@ DATA: list[dict[str, Any]] = [ {"proto": "Dynamic", "route_map": "RM-CONN-2-BGP"}, {"proto": "Bgp", "include_leaked": False, "route_map": "RM-CONN-2-BGP"}, ], - }, + } ], }, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VRF: default, AFI-SAFI: IPv4 Multicast, Proto: IS-IS, Include Leaked: True, Route Map: RM-CONN-2-BGP - Include leaked mismatch - Actual: False", "VRF: test, AFI-SAFI: IPv6 Unicast, Proto: Bgp, Route Map: RM-CONN-2-BGP - Include leaked mismatch - Actual: True", ], }, }, - { - "name": "success", - "test": VerifyBGPPeerTtlMultiHops, + (VerifyBGPPeerTtlMultiHops, "success"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.111.0.1", - "ttl": 2, - "maxTtlHops": 2, - }, - { - "peerAddress": "10.111.0.2", - "ttl": 1, - "maxTtlHops": 1, - }, - ] - }, + "default": {"peerList": [{"peerAddress": "10.111.0.1", "ttl": 2, "maxTtlHops": 2}, {"peerAddress": "10.111.0.2", "ttl": 1, "maxTtlHops": 1}]}, "Test": {"peerList": [{"peerAddress": "10.111.0.3", "ttl": 255, "maxTtlHops": 255}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.111.0.1", - "vrf": "default", - "ttl": 2, - "max_ttl_hops": 2, - }, - { - "peer_address": "10.111.0.2", - "vrf": "default", - "ttl": 1, - "max_ttl_hops": 1, - }, - { - "peer_address": "10.111.0.3", - "vrf": "Test", - "ttl": 255, - "max_ttl_hops": 255, - }, + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "10.111.0.2", "vrf": "default", "ttl": 1, "max_ttl_hops": 1}, + {"peer_address": "10.111.0.3", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-peer-not-found", - "test": VerifyBGPPeerTtlMultiHops, + (VerifyBGPPeerTtlMultiHops, "success-ipv6-rfc5549"): { "eos_data": [ { "vrfs": { "default": { "peerList": [ - { - "peerAddress": "10.111.0.4", - "ttl": 2, - "maxTtlHops": 2, - }, - { - "peerAddress": "10.111.0.5", - "ttl": 1, - "maxTtlHops": 1, - }, + {"peerAddress": "10.111.0.1", "ttl": 2, "maxTtlHops": 2}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "ttl": 1, "maxTtlHops": 1}, ] }, + "Test": {"peerList": [{"ttl": 255, "maxTtlHops": 255, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "ttl": 1, "max_ttl_hops": 1}, + {"interface": "Ethernet1", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyBGPPeerTtlMultiHops, "failure-peer-not-found"): { + "eos_data": [ + { + "vrfs": { + "default": {"peerList": [{"peerAddress": "10.111.0.4", "ttl": 2, "maxTtlHops": 2}, {"peerAddress": "10.111.0.5", "ttl": 1, "maxTtlHops": 1}]}, "Test": {"peerList": [{"peerAddress": "10.111.0.6", "ttl": 255, "maxTtlHops": 255}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.111.0.1", - "vrf": "default", - "ttl": 2, - "max_ttl_hops": 2, - }, - { - "peer_address": "10.111.0.2", - "vrf": "Test", - "ttl": 255, - "max_ttl_hops": 255, - }, + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "10.111.0.2", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, ] }, - "expected": {"result": "failure", "messages": ["Peer: 10.111.0.1 VRF: default - Not found", "Peer: 10.111.0.2 VRF: Test - Not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.111.0.1 VRF: default - Not found", "Peer: 10.111.0.2 VRF: Test - Not found"]}, }, - { - "name": "failure-ttl-time-mismatch", - "test": VerifyBGPPeerTtlMultiHops, + (VerifyBGPPeerTtlMultiHops, "failure-ttl-time-mismatch"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.111.0.1", - "ttl": 12, - "maxTtlHops": 2, - }, - { - "peerAddress": "10.111.0.2", - "ttl": 120, - "maxTtlHops": 1, - }, - ] - }, + "default": {"peerList": [{"peerAddress": "10.111.0.1", "ttl": 12, "maxTtlHops": 2}, {"peerAddress": "10.111.0.2", "ttl": 120, "maxTtlHops": 1}]}, "Test": {"peerList": [{"peerAddress": "10.111.0.3", "ttl": 205, "maxTtlHops": 255}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.111.0.1", - "vrf": "default", - "ttl": 2, - "max_ttl_hops": 2, - }, - { - "peer_address": "10.111.0.2", - "vrf": "default", - "ttl": 1, - "max_ttl_hops": 1, - }, - { - "peer_address": "10.111.0.3", - "vrf": "Test", - "ttl": 255, - "max_ttl_hops": 255, - }, + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "10.111.0.2", "vrf": "default", "ttl": 1, "max_ttl_hops": 1}, + {"peer_address": "10.111.0.3", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.111.0.1 VRF: default - TTL mismatch - Expected: 2 Actual: 12", "Peer: 10.111.0.2 VRF: default - TTL mismatch - Expected: 1 Actual: 120", @@ -6667,54 +5373,24 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-max-ttl-hops-mismatch", - "test": VerifyBGPPeerTtlMultiHops, + (VerifyBGPPeerTtlMultiHops, "failure-max-ttl-hops-mismatch"): { "eos_data": [ { "vrfs": { - "default": { - "peerList": [ - { - "peerAddress": "10.111.0.1", - "ttl": 2, - "maxTtlHops": 12, - }, - { - "peerAddress": "10.111.0.2", - "ttl": 1, - "maxTtlHops": 100, - }, - ] - }, + "default": {"peerList": [{"peerAddress": "10.111.0.1", "ttl": 2, "maxTtlHops": 12}, {"peerAddress": "10.111.0.2", "ttl": 1, "maxTtlHops": 100}]}, "Test": {"peerList": [{"peerAddress": "10.111.0.3", "ttl": 255, "maxTtlHops": 205}]}, } } ], "inputs": { "bgp_peers": [ - { - "peer_address": "10.111.0.1", - "vrf": "default", - "ttl": 2, - "max_ttl_hops": 2, - }, - { - "peer_address": "10.111.0.2", - "vrf": "default", - "ttl": 1, - "max_ttl_hops": 1, - }, - { - "peer_address": "10.111.0.3", - "vrf": "Test", - "ttl": 255, - "max_ttl_hops": 255, - }, + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "10.111.0.2", "vrf": "default", "ttl": 1, "max_ttl_hops": 1}, + {"peer_address": "10.111.0.3", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.111.0.1 VRF: default - Max TTL Hops mismatch - Expected: 2 Actual: 12", "Peer: 10.111.0.2 VRF: default - Max TTL Hops mismatch - Expected: 1 Actual: 100", @@ -6722,4 +5398,36 @@ DATA: list[dict[str, Any]] = [ ], }, }, -] + (VerifyBGPPeerTtlMultiHops, "failure-ipv6-rfc5549"): { + "eos_data": [ + { + "vrfs": { + "default": { + "peerList": [ + {"peerAddress": "10.111.0.1", "ttl": 3, "maxTtlHops": 3}, + {"peerAddress": "fe80::250:56ff:fe01:112%Vl4094", "ttl": 2, "maxTtlHops": 1}, + ] + }, + "Test": {"peerList": [{"ttl": 250, "maxTtlHops": 250, "ifName": "Ethernet1"}]}, + } + } + ], + "inputs": { + "bgp_peers": [ + {"peer_address": "10.111.0.1", "vrf": "default", "ttl": 2, "max_ttl_hops": 2}, + {"peer_address": "fe80::250:56ff:fe01:112%Vl4094", "vrf": "default", "ttl": 1, "max_ttl_hops": 1}, + {"interface": "Ethernet1", "vrf": "Test", "ttl": 255, "max_ttl_hops": 255}, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Peer: 10.111.0.1 VRF: default - TTL mismatch - Expected: 2 Actual: 3", + "Peer: 10.111.0.1 VRF: default - Max TTL Hops mismatch - Expected: 2 Actual: 3", + "Peer: fe80::250:56ff:fe01:112%Vl4094 VRF: default - TTL mismatch - Expected: 1 Actual: 2", + "Interface: Ethernet1 VRF: Test - TTL mismatch - Expected: 255 Actual: 250", + "Interface: Ethernet1 VRF: Test - Max TTL Hops mismatch - Expected: 255 Actual: 250", + ], + }, + }, +} diff --git a/tests/units/anta_tests/routing/test_generic.py b/tests/units/anta_tests/routing/test_generic.py index 2e67f91..ed95743 100644 --- a/tests/units/anta_tests/routing/test_generic.py +++ b/tests/units/anta_tests/routing/test_generic.py @@ -6,73 +6,53 @@ from __future__ import annotations import sys -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from pydantic import ValidationError -from anta.tests.routing.generic import VerifyIPv4RouteNextHops, VerifyIPv4RouteType, VerifyRoutingProtocolModel, VerifyRoutingTableEntry, VerifyRoutingTableSize +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus +from anta.tests.routing.generic import ( + VerifyIPv4RouteNextHops, + VerifyIPv4RouteType, + VerifyRoutingProtocolModel, + VerifyRoutingStatus, + VerifyRoutingTableEntry, + VerifyRoutingTableSize, +) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyRoutingProtocolModel, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyRoutingProtocolModel, "success"): { "eos_data": [{"vrfs": {"default": {}}, "protoModelStatus": {"configuredProtoModel": "multi-agent", "operatingProtoModel": "multi-agent"}}], "inputs": {"model": "multi-agent"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-configured-model", - "test": VerifyRoutingProtocolModel, + (VerifyRoutingProtocolModel, "failure-wrong-configured-model"): { "eos_data": [{"vrfs": {"default": {}}, "protoModelStatus": {"configuredProtoModel": "ribd", "operatingProtoModel": "ribd"}}], "inputs": {"model": "multi-agent"}, - "expected": {"result": "failure", "messages": ["Routing model is misconfigured - Expected: multi-agent Actual: ribd"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Routing model is misconfigured - Expected: multi-agent Actual: ribd"]}, }, - { - "name": "failure-mismatch-operating-model", - "test": VerifyRoutingProtocolModel, + (VerifyRoutingProtocolModel, "failure-mismatch-operating-model"): { "eos_data": [{"vrfs": {"default": {}}, "protoModelStatus": {"configuredProtoModel": "multi-agent", "operatingProtoModel": "ribd"}}], "inputs": {"model": "multi-agent"}, - "expected": {"result": "failure", "messages": ["Routing model is misconfigured - Expected: multi-agent Actual: ribd"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Routing model is misconfigured - Expected: multi-agent Actual: ribd"]}, }, - { - "name": "success", - "test": VerifyRoutingTableSize, - "eos_data": [ - { - "vrfs": { - "default": { - # Output truncated - "maskLen": {"8": 2}, - "totalRoutes": 123, - }, - }, - }, - ], + (VerifyRoutingTableSize, "success"): { + "eos_data": [{"vrfs": {"default": {"maskLen": {"8": 2}, "totalRoutes": 123}}}], "inputs": {"minimum": 42, "maximum": 666}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyRoutingTableSize, - "eos_data": [ - { - "vrfs": { - "default": { - # Output truncated - "maskLen": {"8": 2}, - "totalRoutes": 1000, - }, - }, - }, - ], + (VerifyRoutingTableSize, "failure"): { + "eos_data": [{"vrfs": {"default": {"maskLen": {"8": 2}, "totalRoutes": 1000}}}], "inputs": {"minimum": 42, "maximum": 666}, - "expected": {"result": "failure", "messages": ["Routing table routes are outside the routes range - Expected: 42 <= to >= 666 Actual: 1000"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Routing table routes are outside the routes range - Expected: 42 <= to >= 666 Actual: 1000"]}, }, - { - "name": "success", - "test": VerifyRoutingTableEntry, + (VerifyRoutingTableEntry, "success"): { "eos_data": [ { "vrfs": { @@ -92,10 +72,10 @@ DATA: list[dict[str, Any]] = [ "preference": 20, "metric": 0, "vias": [{"nexthopAddr": "10.1.255.4", "interface": "Ethernet1"}], - }, + } }, - }, - }, + } + } }, { "vrfs": { @@ -115,18 +95,16 @@ DATA: list[dict[str, Any]] = [ "preference": 20, "metric": 0, "vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}], - }, + } }, - }, - }, + } + } }, ], "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-collect-all", - "test": VerifyRoutingTableEntry, + (VerifyRoutingTableEntry, "success-collect-all"): { "eos_data": [ { "vrfs": { @@ -159,16 +137,14 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}], }, }, - }, - }, - }, + } + } + } ], "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"], "collect": "all"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-missing-route", - "test": VerifyRoutingTableEntry, + (VerifyRoutingTableEntry, "failure-missing-route"): { "eos_data": [ { "vrfs": { @@ -178,8 +154,8 @@ DATA: list[dict[str, Any]] = [ "allRoutesProgrammedKernel": True, "defaultRouteState": "notSet", "routes": {}, - }, - }, + } + } }, { "vrfs": { @@ -199,10 +175,10 @@ DATA: list[dict[str, Any]] = [ "preference": 20, "metric": 0, "vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}], - }, + } }, - }, - }, + } + } }, { "vrfs": { @@ -212,16 +188,14 @@ DATA: list[dict[str, Any]] = [ "allRoutesProgrammedKernel": True, "defaultRouteState": "notSet", "routes": {}, - }, - }, + } + } }, ], "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2", "10.1.0.3"]}, - "expected": {"result": "failure", "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.3"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.3"]}, }, - { - "name": "failure-wrong-route", - "test": VerifyRoutingTableEntry, + (VerifyRoutingTableEntry, "failure-wrong-route"): { "eos_data": [ { "vrfs": { @@ -241,10 +215,10 @@ DATA: list[dict[str, Any]] = [ "preference": 20, "metric": 0, "vias": [{"nexthopAddr": "10.1.255.4", "interface": "Ethernet1"}], - }, + } }, - }, - }, + } + } }, { "vrfs": { @@ -264,18 +238,16 @@ DATA: list[dict[str, Any]] = [ "preference": 20, "metric": 0, "vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}], - }, + } }, - }, - }, + } + } }, ], "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"]}, - "expected": {"result": "failure", "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.2"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.2"]}, }, - { - "name": "failure-wrong-route-collect-all", - "test": VerifyRoutingTableEntry, + (VerifyRoutingTableEntry, "failure-wrong-route-collect-all"): { "eos_data": [ { "vrfs": { @@ -308,16 +280,14 @@ DATA: list[dict[str, Any]] = [ "vias": [{"nexthopAddr": "10.1.255.6", "interface": "Ethernet2"}], }, }, - }, - }, - }, + } + } + } ], "inputs": {"vrf": "default", "routes": ["10.1.0.1", "10.1.0.2"], "collect": "all"}, - "expected": {"result": "failure", "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.2"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following route(s) are missing from the routing table of VRF default: 10.1.0.2"]}, }, - { - "name": "success-valid-route-type", - "test": VerifyIPv4RouteType, + (VerifyIPv4RouteType, "success-valid-route-type"): { "eos_data": [ { "vrfs": { @@ -333,42 +303,31 @@ DATA: list[dict[str, Any]] = [ {"vrf": "MGMT", "prefix": "10.100.1.5/32", "route_type": "iBGP"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-route-not-found", - "test": VerifyIPv4RouteType, + (VerifyIPv4RouteType, "failure-route-not-found"): { "eos_data": [{"vrfs": {"default": {"routes": {}}}}], "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "eBGP"}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.10.0.1/32 VRF: default - Route not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.10.0.1/32 VRF: default - Route not found"]}, }, - { - "name": "failure-invalid-route-type", - "test": VerifyIPv4RouteType, + (VerifyIPv4RouteType, "failure-invalid-route-type"): { "eos_data": [{"vrfs": {"default": {"routes": {"10.10.0.1/32": {"routeType": "eBGP"}}}}}], "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "iBGP"}]}, - "expected": { - "result": "failure", - "messages": ["Prefix: 10.10.0.1/32 VRF: default - Incorrect route type - Expected: iBGP Actual: eBGP"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.10.0.1/32 VRF: default - Incorrect route type - Expected: iBGP Actual: eBGP"]}, }, - { - "name": "failure-vrf-not-configured", - "test": VerifyIPv4RouteType, + (VerifyIPv4RouteType, "failure-vrf-not-configured"): { "eos_data": [{"vrfs": {}}], "inputs": {"routes_entries": [{"vrf": "default", "prefix": "10.10.0.1/32", "route_type": "eBGP"}]}, - "expected": {"result": "failure", "messages": ["Prefix: 10.10.0.1/32 VRF: default - VRF not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.10.0.1/32 VRF: default - VRF not configured"]}, }, - { - "name": "success", - "test": VerifyIPv4RouteNextHops, + (VerifyIPv4RouteNextHops, "success"): { "eos_data": [ { "vrfs": { "default": { "routes": { "10.10.0.1/32": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, @@ -379,12 +338,12 @@ DATA: list[dict[str, Any]] = [ {"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}, {"nexthopAddr": "10.100.0.101", "interface": "Ethernet4"}, - ], + ] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -392,30 +351,28 @@ DATA: list[dict[str, Any]] = [ {"prefix": "10.100.0.128/31", "vrf": "MGMT", "nexthops": ["10.100.0.8", "10.100.0.10"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-strict-true", - "test": VerifyIPv4RouteNextHops, + (VerifyIPv4RouteNextHops, "success-strict-true"): { "eos_data": [ { "vrfs": { "default": { "routes": { "10.10.0.1/32": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, "MGMT": { "routes": { "10.100.0.128/31": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -423,14 +380,10 @@ DATA: list[dict[str, Any]] = [ {"prefix": "10.100.0.128/31", "vrf": "MGMT", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyIPv4RouteNextHops, - "eos_data": [ - {"vrfs": {"default": {"routes": {}}, "MGMT": {"routes": {}}}}, - ], + (VerifyIPv4RouteNextHops, "failure-not-configured"): { + "eos_data": [{"vrfs": {"default": {"routes": {}}, "MGMT": {"routes": {}}}}], "inputs": { "route_entries": [ {"prefix": "10.10.0.1/32", "vrf": "default", "strict": True, "nexthops": ["10.100.0.8", "10.100.0.10"]}, @@ -438,32 +391,30 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.10.0.1/32 VRF: default - prefix not found", "Prefix: 10.100.0.128/31 VRF: MGMT - prefix not found"], }, }, - { - "name": "failure-strict-failed", - "test": VerifyIPv4RouteNextHops, + (VerifyIPv4RouteNextHops, "failure-strict-failed"): { "eos_data": [ { "vrfs": { "default": { "routes": { "10.10.0.1/32": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, "MGMT": { "routes": { "10.100.0.128/31": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.11", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.11", "interface": "Ethernet2"}] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -472,36 +423,34 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Prefix: 10.10.0.1/32 VRF: default - List of next-hops not matching - Expected: 10.100.0.10, 10.100.0.11, 10.100.0.8 " - "Actual: 10.100.0.10, 10.100.0.8", + "Prefix: 10.10.0.1/32 VRF: default - List of next-hops not matching - " + "Expected: 10.100.0.10, 10.100.0.11, 10.100.0.8 Actual: 10.100.0.10, 10.100.0.8", "Prefix: 10.100.0.128/31 VRF: MGMT - List of next-hops not matching - Expected: 10.100.0.10, 10.100.0.8 Actual: 10.100.0.11, 10.100.0.8", ], }, }, - { - "name": "failure", - "test": VerifyIPv4RouteNextHops, + (VerifyIPv4RouteNextHops, "failure"): { "eos_data": [ { "vrfs": { "default": { "routes": { "10.10.0.1/32": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, "MGMT": { "routes": { "10.100.0.128/31": { - "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}], + "vias": [{"nexthopAddr": "10.100.0.8", "interface": "Ethernet1"}, {"nexthopAddr": "10.100.0.10", "interface": "Ethernet2"}] } } }, } - }, + } ], "inputs": { "route_entries": [ @@ -510,14 +459,93 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Prefix: 10.10.0.1/32 VRF: default Nexthop: 10.100.0.11 - Route not found", "Prefix: 10.100.0.128/31 VRF: MGMT Nexthop: 10.100.0.11 - Route not found", ], }, }, -] + (VerifyRoutingStatus, "success-routing-enablement"): { + "eos_data": [ + { + "v4RoutingEnabled": True, + "v6RoutingEnabled": True, + "vrrpIntfs": 0, + "v6IntfForwarding": True, + "multicastRouting": {"ipMulticastEnabled": False, "ip6MulticastEnabled": False}, + "v6EcmpInfo": {"v6EcmpRouteSupport": True}, + } + ], + "inputs": {"ipv4_unicast": True, "ipv6_unicast": True, "ipv6_interfaces": True}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyRoutingStatus, "success-routing-disable-all"): { + "eos_data": [ + { + "v4RoutingEnabled": False, + "v6RoutingEnabled": False, + "vrrpIntfs": 0, + "multicastRouting": {"ipMulticastEnabled": False, "ip6MulticastEnabled": False}, + "v6EcmpInfo": {"v6EcmpRouteSupport": False}, + } + ], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyRoutingStatus, "failure-ip-multicastrouting-enablement"): { + "eos_data": [ + { + "v4RoutingEnabled": False, + "v6RoutingEnabled": False, + "vrrpIntfs": 0, + "multicastRouting": {"ipMulticastEnabled": False, "ip6MulticastEnabled": False}, + "v6EcmpInfo": {"v6EcmpRouteSupport": True}, + } + ], + "inputs": {"ipv4_multicast": True, "ipv6_multicast": True}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "IPv4 multicast routing enabled status mismatch - Expected: True Actual: False", + "IPv6 multicast routing enabled status mismatch - Expected: True Actual: False", + ], + }, + }, + (VerifyRoutingStatus, "failure-ip-routing-enablement"): { + "eos_data": [ + { + "v4RoutingEnabled": False, + "v6RoutingEnabled": False, + "vrrpIntfs": 0, + "multicastRouting": {"ipMulticastEnabled": True, "ip6MulticastEnabled": True}, + "v6EcmpInfo": {"v6EcmpRouteSupport": True}, + } + ], + "inputs": {"ipv4_unicast": True, "ipv6_unicast": True}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "IPv4 unicast routing enabled status mismatch - Expected: True Actual: False", + "IPv6 unicast routing enabled status mismatch - Expected: True Actual: False", + "IPv4 multicast routing enabled status mismatch - Expected: False Actual: True", + "IPv6 multicast routing enabled status mismatch - Expected: False Actual: True", + ], + }, + }, + (VerifyRoutingStatus, "failure-ipv6-interface-routing-enablement"): { + "eos_data": [ + { + "v4RoutingEnabled": True, + "v6RoutingEnabled": True, + "vrrpIntfs": 0, + "multicastRouting": {"ipMulticastEnabled": False, "ip6MulticastEnabled": False}, + "v6EcmpInfo": {"v6EcmpRouteSupport": True}, + } + ], + "inputs": {"ipv4_unicast": True, "ipv6_unicast": True, "ipv6_interfaces": True}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["IPv6 interfaces routing enabled status mismatch - Expected: True Actual: False"]}, + }, +} class TestVerifyRoutingTableSizeInputs: diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 8a36d82..b586649 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -7,11 +7,15 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any import pytest +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.routing.isis import ( + VerifyISISGracefulRestart, VerifyISISInterfaceMode, VerifyISISNeighborCount, VerifyISISNeighborState, @@ -21,10 +25,11 @@ from anta.tests.routing.isis import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success-default-vrf", - "test": VerifyISISNeighborState, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyISISNeighborState, "success-default-vrf"): { "eos_data": [ { "vrfs": { @@ -81,14 +86,11 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiple-vrfs", - "test": VerifyISISNeighborState, + (VerifyISISNeighborState, "success-multiple-vrfs"): { "eos_data": [ { "vrfs": { @@ -107,10 +109,10 @@ DATA: list[dict[str, Any]] = [ "routerIdV4": "1.0.0.111", } ] - }, - }, - }, - }, + } + } + } + } }, "customer": { "isisInstances": { @@ -133,14 +135,12 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], "inputs": {"check_all_vrfs": True}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyISISNeighborState, + (VerifyISISNeighborState, "failure"): { "eos_data": [ { "vrfs": { @@ -177,29 +177,18 @@ DATA: list[dict[str, Any]] = [ } } } - }, + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet1 - Incorrect adjacency state - Expected: up Actual: down"], }, }, - { - "name": "skipped-not-configured", - "test": VerifyISISNeighborState, - "eos_data": [ - {"vrfs": {}}, - ], - "inputs": None, - "expected": { - "result": "skipped", - "messages": ["IS-IS not configured"], - }, + (VerifyISISNeighborState, "skipped-not-configured"): { + "eos_data": [{"vrfs": {}}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, }, - { - "name": "failure-multiple-vrfs", - "test": VerifyISISNeighborState, + (VerifyISISNeighborState, "failure-multiple-vrfs"): { "eos_data": [ { "vrfs": { @@ -218,10 +207,10 @@ DATA: list[dict[str, Any]] = [ "routerIdV4": "1.0.0.111", } ] - }, - }, - }, - }, + } + } + } + } }, "customer": { "isisInstances": { @@ -244,40 +233,20 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], "inputs": {"check_all_vrfs": True}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS VRF: customer Interface: Ethernet2 - Incorrect adjacency state - Expected: up Actual: down"], }, }, - { - "name": "skipped-no-neighbor-detected", - "test": VerifyISISNeighborState, - "eos_data": [ - { - "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "neighbors": {}, - }, - }, - }, - "customer": {"isisInstances": {"CORE-ISIS": {"neighbors": {}}}}, - } - }, - ], + (VerifyISISNeighborState, "skipped-no-neighbor-detected"): { + "eos_data": [{"vrfs": {"default": {"isisInstances": {"CORE-ISIS": {"neighbors": {}}}}, "customer": {"isisInstances": {"CORE-ISIS": {"neighbors": {}}}}}}], "inputs": {"check_all_vrfs": True}, - "expected": { - "result": "skipped", - "messages": ["No IS-IS neighbor detected"], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["No IS-IS neighbor detected"]}, }, - { - "name": "success-default-vrf", - "test": VerifyISISNeighborCount, + (VerifyISISNeighborCount, "success-default-vrf"): { "eos_data": [ { "vrfs": { @@ -337,19 +306,12 @@ DATA: list[dict[str, Any]] = [ } } } - }, + } ], - "inputs": { - "interfaces": [ - {"name": "Ethernet1", "level": 2, "count": 1}, - {"name": "Ethernet2", "level": 2, "count": 1}, - ] - }, - "expected": {"result": "success"}, + "inputs": {"interfaces": [{"name": "Ethernet1", "level": 2, "count": 1}, {"name": "Ethernet2", "level": 2, "count": 1}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiple-VRFs", - "test": VerifyISISNeighborCount, + (VerifyISISNeighborCount, "success-multiple-VRFs"): { "eos_data": [ { "vrfs": { @@ -428,13 +390,13 @@ DATA: list[dict[str, Any]] = [ }, "interfaceSpeed": 1000, "areaProxyBoundary": False, - }, + } } } } }, } - }, + } ], "inputs": { "interfaces": [ @@ -443,27 +405,14 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet3", "vrf": "PROD", "level": 1, "count": 1}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped-not-configured", - "test": VerifyISISNeighborCount, - "eos_data": [ - {"vrfs": {}}, - ], - "inputs": { - "interfaces": [ - {"name": "Ethernet1", "level": 2, "count": 1}, - ] - }, - "expected": { - "result": "skipped", - "messages": ["IS-IS not configured"], - }, + (VerifyISISNeighborCount, "skipped-not-configured"): { + "eos_data": [{"vrfs": {}}], + "inputs": {"interfaces": [{"name": "Ethernet1", "level": 2, "count": 1}]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, }, - { - "name": "failure-interface-not-configured", - "test": VerifyISISNeighborCount, + (VerifyISISNeighborCount, "failure-interface-not-configured"): { "eos_data": [ { "vrfs": { @@ -486,27 +435,18 @@ DATA: list[dict[str, Any]] = [ }, "interfaceSpeed": 1000, "areaProxyBoundary": False, - }, + } } } } } } - }, + } ], - "inputs": { - "interfaces": [ - {"name": "Ethernet2", "level": 2, "count": 1}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured"], - }, + "inputs": {"interfaces": [{"name": "Ethernet2", "level": 2, "count": 1}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured"]}, }, - { - "name": "success-interface-is-in-wrong-vrf", - "test": VerifyISISNeighborCount, + (VerifyISISNeighborCount, "success-interface-is-in-wrong-vrf"): { "eos_data": [ { "vrfs": { @@ -529,7 +469,7 @@ DATA: list[dict[str, Any]] = [ }, "interfaceSpeed": 1000, "areaProxyBoundary": False, - }, + } } } } @@ -554,28 +494,21 @@ DATA: list[dict[str, Any]] = [ }, "interfaceSpeed": 1000, "areaProxyBoundary": False, - }, + } } } } }, } - }, + } ], - "inputs": { - "interfaces": [ - {"name": "Ethernet2", "level": 2, "count": 1}, - {"name": "Ethernet1", "vrf": "PROD", "level": 1, "count": 1}, - ] - }, + "inputs": {"interfaces": [{"name": "Ethernet2", "level": 2, "count": 1}, {"name": "Ethernet1", "vrf": "PROD", "level": 1, "count": 1}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured", "Interface: Ethernet1 VRF: PROD Level: 1 - Not configured"], }, }, - { - "name": "failure-wrong-count", - "test": VerifyISISNeighborCount, + (VerifyISISNeighborCount, "failure-wrong-count"): { "eos_data": [ { "vrfs": { @@ -598,27 +531,18 @@ DATA: list[dict[str, Any]] = [ }, "interfaceSpeed": 1000, "areaProxyBoundary": False, - }, + } } } } } } - }, + } ], - "inputs": { - "interfaces": [ - {"name": "Ethernet1", "level": 2, "count": 1}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Neighbor count mismatch - Expected: 1 Actual: 3"], - }, + "inputs": {"interfaces": [{"name": "Ethernet1", "level": 2, "count": 1}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Neighbor count mismatch - Expected: 1 Actual: 3"]}, }, - { - "name": "success-default-vrf", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "success-default-vrf"): { "eos_data": [ { "vrfs": { @@ -696,11 +620,9 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiple-VRFs", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "success-multiple-VRFs"): { "eos_data": [ { "vrfs": { @@ -807,11 +729,9 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet5", "mode": "passive", "vrf": "PROD"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-interface-not-passive", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "failure-interface-not-passive"): { "eos_data": [ { "vrfs": { @@ -889,14 +809,9 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, ] }, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not running in passive mode"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not running in passive mode"]}, }, - { - "name": "failure-interface-not-point-to-point", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "failure-interface-not-point-to-point"): { "eos_data": [ { "vrfs": { @@ -975,13 +890,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast"], }, }, - { - "name": "failure-interface-wrong-vrf", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "failure-interface-wrong-vrf"): { "eos_data": [ { "vrfs": { @@ -1060,7 +973,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Loopback0 VRF: default Level: 2 - Not configured", "Interface: Ethernet2 VRF: default Level: 2 - Not configured", @@ -1068,9 +981,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "skipped-not-configured", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "skipped-not-configured"): { "eos_data": [{"vrfs": {}}], "inputs": { "interfaces": [ @@ -1079,11 +990,9 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, ] }, - "expected": {"result": "skipped", "messages": ["IS-IS not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, }, - { - "name": "failure-multiple-VRFs", - "test": VerifyISISInterfaceMode, + (VerifyISISInterfaceMode, "failure-multiple-VRFs"): { "eos_data": [ { "vrfs": { @@ -1191,7 +1100,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 VRF: default Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast", "Interface: Ethernet4 VRF: PROD Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast", @@ -1199,30 +1108,12 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "skipped-not-configured", - "test": VerifyISISSegmentRoutingAdjacencySegments, + (VerifyISISSegmentRoutingAdjacencySegments, "skipped-not-configured"): { "eos_data": [{"vrfs": {}}], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - } - ], - } - ] - }, - "expected": {"result": "skipped", "messages": ["IS-IS not configured"]}, + "inputs": {"instances": [{"name": "CORE-ISIS", "vrf": "default", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}]}]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "success", + (VerifyISISSegmentRoutingAdjacencySegments, "success"): { "eos_data": [ { "vrfs": { @@ -1244,13 +1135,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, { @@ -1260,13 +1145,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, ], @@ -1278,29 +1157,10 @@ DATA: list[dict[str, Any]] = [ } } ], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - } - ], - } - ] - }, - "expected": { - "result": "success", - "messages": [], - }, + "inputs": {"instances": [{"name": "CORE-ISIS", "vrf": "default", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}]}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "failure-segment-not-found", + (VerifyISISSegmentRoutingAdjacencySegments, "failure-segment-not-found"): { "eos_data": [ { "vrfs": { @@ -1322,13 +1182,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, { @@ -1338,13 +1192,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, ], @@ -1362,28 +1210,18 @@ DATA: list[dict[str, Any]] = [ "name": "CORE-ISIS", "vrf": "default", "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - }, - { - "interface": "Ethernet3", - "address": "10.0.1.2", - "sid_origin": "dynamic", - }, + {"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}, + {"interface": "Ethernet3", "address": "10.0.1.2", "sid_origin": "dynamic"}, ], } ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS VRF: default Local Intf: Ethernet3 Adj IP Address: 10.0.1.2 - Adjacency segment not found"], }, }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "failure-no-segments-incorrect-instance", + (VerifyISISSegmentRoutingAdjacencySegments, "failure-no-segments-incorrect-instance"): { "eos_data": [ { "vrfs": { @@ -1405,13 +1243,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, { @@ -1421,13 +1253,7 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, }, ], @@ -1445,28 +1271,15 @@ DATA: list[dict[str, Any]] = [ "name": "CORE-ISIS2", "vrf": "default", "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - }, - { - "interface": "Ethernet3", - "address": "10.0.1.2", - "sid_origin": "dynamic", - }, + {"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}, + {"interface": "Ethernet3", "address": "10.0.1.2", "sid_origin": "dynamic"}, ], } ] }, - "expected": { - "result": "failure", - "messages": ["Instance: CORE-ISIS2 VRF: default - No adjacency segments found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS2 VRF: default - No adjacency segments found"]}, }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "failure-incorrect-segment-level", + (VerifyISISSegmentRoutingAdjacencySegments, "failure-incorrect-segment-level"): { "eos_data": [ { "vrfs": { @@ -1488,15 +1301,9 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "dynamic", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, - }, + } ], "receivedGlobalAdjacencySegments": [], "misconfiguredAdjacencySegments": [], @@ -1508,28 +1315,15 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - "level": 1, # Wrong level - }, - ], - } + {"name": "CORE-ISIS", "vrf": "default", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic", "level": 1}]} ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS VRF: default Local Intf: Ethernet2 Adj IP Address: 10.0.1.3 - Incorrect IS-IS level - Expected: 1 Actual: 2"], }, }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "failure-incorrect-sid-origin", + (VerifyISISSegmentRoutingAdjacencySegments, "failure-incorrect-sid-origin"): { "eos_data": [ { "vrfs": { @@ -1551,15 +1345,9 @@ DATA: list[dict[str, Any]] = [ "lan": False, "sidOrigin": "configured", "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, + "flags": {"b": False, "v": True, "l": True, "f": False, "s": False}, "level": 2, - }, + } ], "receivedGlobalAdjacencySegments": [], "misconfiguredAdjacencySegments": [], @@ -1571,730 +1359,412 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - "level": 2, # Wrong level - }, - ], - } + {"name": "CORE-ISIS", "vrf": "default", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic", "level": 2}]} ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Instance: CORE-ISIS VRF: default Local Intf: Ethernet2 Adj IP Address: 10.0.1.3 - Incorrect SID origin - Expected: dynamic Actual: configured" ], }, }, - { - "test": VerifyISISSegmentRoutingDataplane, - "name": "success", + (VerifyISISSegmentRoutingDataplane, "success"): { "eos_data": [ { "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "dataPlane": "MPLS", - "routerId": "1.0.0.11", - "systemId": "0168.0000.0011", - "hostname": "s1-pe01", - } - } - } + "default": {"isisInstances": {"CORE-ISIS": {"dataPlane": "MPLS", "routerId": "1.0.0.11", "systemId": "0168.0000.0011", "hostname": "s1-pe01"}}} } } ], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "dataplane": "MPLS", - }, - ] - }, - "expected": { - "result": "success", - "messages": [], - }, + "inputs": {"instances": [{"name": "CORE-ISIS", "vrf": "default", "dataplane": "MPLS"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "test": VerifyISISSegmentRoutingDataplane, - "name": "failure-incorrect-dataplane", + (VerifyISISSegmentRoutingDataplane, "failure-incorrect-dataplane"): { "eos_data": [ { "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "dataPlane": "MPLS", - "routerId": "1.0.0.11", - "systemId": "0168.0000.0011", - "hostname": "s1-pe01", - } - } - } + "default": {"isisInstances": {"CORE-ISIS": {"dataPlane": "MPLS", "routerId": "1.0.0.11", "systemId": "0168.0000.0011", "hostname": "s1-pe01"}}} } } ], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "dataplane": "unset", - }, - ] - }, + "inputs": {"instances": [{"name": "CORE-ISIS", "vrf": "default", "dataplane": "unset"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS VRF: default - Data-plane not correctly configured - Expected: UNSET Actual: MPLS"], }, }, - { - "test": VerifyISISSegmentRoutingDataplane, - "name": "failure-instance-not-configured", + (VerifyISISSegmentRoutingDataplane, "failure-instance-not-configured"): { + "eos_data": [ + { + "vrfs": { + "default": {"isisInstances": {"CORE-ISIS": {"dataPlane": "MPLS", "routerId": "1.0.0.11", "systemId": "0168.0000.0011", "hostname": "s1-pe01"}}} + } + } + ], + "inputs": {"instances": [{"name": "CORE-ISIS2", "vrf": "default", "dataplane": "unset"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Instance: CORE-ISIS2 VRF: default - Not configured"]}, + }, + (VerifyISISSegmentRoutingDataplane, "skipped-not-configured"): { + "eos_data": [{"vrfs": {}}], + "inputs": {"instances": [{"name": "CORE-ISIS", "vrf": "default", "dataplane": "unset"}]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, + }, + (VerifyISISSegmentRoutingTunnels, "runs successfully"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnel_id": "ti-lfa"}]}, + { + "endpoint": "1.0.0.122/32", + "vias": [{"interface": "Ethernet1", "nexthop": "10.0.1.1"}, {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}], + }, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyISISSegmentRoutingTunnels, "is skipped if not entry founf in EOS"): { + "eos_data": [{"entries": {}}], + "inputs": {"entries": [{"endpoint": "1.0.0.122/32"}]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS-SR not configured"]}, + }, + (VerifyISISSegmentRoutingTunnels, "runs successfully only endpoint"): { + "eos_data": [{"entries": {"2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}}}], + "inputs": {"entries": [{"endpoint": "1.0.0.122/32"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Endpoint: 1.0.0.122/32 - Tunnel not found"]}, + }, + (VerifyISISSegmentRoutingTunnels, "fails with incorrect tunnel type"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": {"entries": [{"endpoint": "1.0.0.122/32"}, {"endpoint": "1.0.0.13/32", "vias": [{"type": "tunnel"}]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Endpoint: 1.0.0.13/32 Type: tunnel - Tunnel is incorrect"]}, + }, + (VerifyISISSegmentRoutingTunnels, "fails with incorrect nexthop interface"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + { + "endpoint": "1.0.0.122/32", + "vias": [{"type": "ip", "interface": "Ethernet1", "nexthop": "10.0.1.2"}, {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - Tunnel is incorrect"], + }, + }, + (VerifyISISSegmentRoutingTunnels, "fails with incorrect nexthop"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + { + "endpoint": "1.0.0.122/32", + "vias": [{"type": "ip", "interface": "Ethernet4", "nexthop": "10.0.1.1"}, {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.1 Type: ip Interface: Ethernet4 - Tunnel is incorrect"], + }, + }, + (VerifyISISSegmentRoutingTunnels, "fails with incorrect interface"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "TI-LFA", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + { + "endpoint": "1.0.0.122/32", + "vias": [{"type": "ip", "interface": "Ethernet1", "nexthop": "10.0.1.2"}, {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - Tunnel is incorrect"], + }, + }, + (VerifyISISSegmentRoutingTunnels, "fails with incorrect tunnel ID type"): { + "eos_data": [ + { + "entries": { + "3": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "31": { + "endpoint": "1.0.0.13/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "32": { + "endpoint": "1.0.0.122/32", + "vias": [ + {"type": "ip", "nexthop": "10.0.1.1", "interface": "Ethernet1", "labels": ["900021"]}, + {"type": "ip", "nexthop": "10.0.1.3", "interface": "Ethernet2", "labels": ["900021"]}, + ], + }, + "2": {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnelId": {"type": "unset", "index": 4}, "labels": ["3"]}]}, + } + } + ], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnel_id": "ti-lfa"}]}, + ] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Endpoint: 1.0.0.111/32 Type: tunnel Tunnel ID: ti-lfa - Tunnel is incorrect"]}, + }, + (VerifyISISSegmentRoutingTunnels, "skipped with ISIS-SR not running"): { + "eos_data": [{"entries": {}}], + "inputs": { + "entries": [ + {"endpoint": "1.0.0.122/32"}, + {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, + {"endpoint": "1.0.0.111/32", "vias": [{"type": "tunnel", "tunnel_id": "unset"}]}, + ] + }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS-SR not configured"]}, + }, + (VerifyISISGracefulRestart, "success"): { "eos_data": [ { "vrfs": { "default": { "isisInstances": { - "CORE-ISIS": { - "dataPlane": "MPLS", - "routerId": "1.0.0.11", - "systemId": "0168.0000.0011", - "hostname": "s1-pe01", - } + "1": {"gracefulRestart": "enabled", "gracefulRestartHelper": "enabled"}, + "2": {"gracefulRestart": "enabled", "gracefulRestartHelper": "disabled"}, } - } + }, + "test": { + "isisInstances": { + "11": {"gracefulRestart": "disabled", "gracefulRestartHelper": "enabled"}, + "12": {"gracefulRestart": "enabled", "gracefulRestartHelper": "disabled"}, + } + }, } } ], "inputs": { "instances": [ - { - "name": "CORE-ISIS2", - "vrf": "default", - "dataplane": "unset", - }, + {"vrf": "default", "name": "1", "graceful_restart": True}, + {"vrf": "default", "name": "2", "graceful_restart": True, "graceful_restart_helper": False}, + {"vrf": "test", "name": "11"}, + {"vrf": "test", "name": "12", "graceful_restart": True, "graceful_restart_helper": False}, ] }, - "expected": { - "result": "failure", - "messages": ["Instance: CORE-ISIS2 VRF: default - Not configured"], - }, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "test": VerifyISISSegmentRoutingDataplane, - "name": "skipped-not-configured", + (VerifyISISGracefulRestart, "failure-isis-not-configured"): { "eos_data": [{"vrfs": {}}], + "inputs": {"instances": [{"vrf": "default", "name": "1", "graceful_restart": True}]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["IS-IS not configured"]}, + }, + (VerifyISISGracefulRestart, "failure-isis-instance-not-found"): { + "eos_data": [{"vrfs": {"default": {"isisInstances": {"2": {"gracefulRestart": "enabled", "gracefulRestartHelper": "enabled"}}}}}], + "inputs": {"instances": [{"vrf": "default", "name": "1", "graceful_restart": True}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Instance: 1 VRF: default - Not configured"]}, + }, + (VerifyISISGracefulRestart, "failure-graceful-restart-disabled"): { + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "1": {"gracefulRestart": "disabled", "gracefulRestartHelper": "enabled"}, + "2": {"gracefulRestart": "enabled", "gracefulRestartHelper": "enabled"}, + } + }, + "test": { + "isisInstances": { + "11": {"gracefulRestart": "enabled", "gracefulRestartHelper": "enabled"}, + "12": {"gracefulRestart": "enabled", "gracefulRestartHelper": "disabled"}, + } + }, + } + } + ], "inputs": { "instances": [ - { - "name": "CORE-ISIS", - "vrf": "default", - "dataplane": "unset", - }, + {"vrf": "default", "name": "1", "graceful_restart": True}, + {"vrf": "default", "name": "2", "graceful_restart": True}, + {"vrf": "test", "name": "11"}, + {"vrf": "test", "name": "12", "graceful_restart": True, "graceful_restart_helper": False}, ] }, "expected": { - "result": "skipped", - "messages": ["IS-IS not configured"], + "result": AntaTestStatus.FAILURE, + "messages": [ + "Instance: 1 VRF: default - Incorrect graceful restart state - Expected: enabled Actual: disabled", + "Instance: 11 VRF: test - Incorrect graceful restart state - Expected: disabled Actual: enabled", + ], }, }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "runs successfully", + (VerifyISISGracefulRestart, "failure-graceful-restart-helper-disabled"): { "eos_data": [ { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, + "vrfs": { + "default": {"isisInstances": {"1": {"gracefulRestart": "disabled", "gracefulRestartHelper": "disabled"}}}, + "test": {"isisInstances": {"11": {"gracefulRestart": "disabled", "gracefulRestartHelper": "enabled"}}}, } } ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.111/32", - "vias": [{"type": "tunnel", "tunnel_id": "ti-lfa"}], - }, - { - "endpoint": "1.0.0.122/32", - "vias": [ - {"interface": "Ethernet1", "nexthop": "10.0.1.1"}, # Testing empty type - {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}, - ], - }, - ] - }, + "inputs": {"instances": [{"vrf": "default", "name": "1"}, {"vrf": "test", "name": "11", "graceful_restart_helper": False}]}, "expected": { - "result": "success", - "messages": [], + "result": AntaTestStatus.FAILURE, + "messages": [ + "Instance: 1 VRF: default - Incorrect graceful restart helper state - Expected: enabled Actual: disabled", + "Instance: 11 VRF: test - Incorrect graceful restart helper state - Expected: disabled Actual: enabled", + ], }, }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "is skipped if not entry founf in EOS", - "eos_data": [{"entries": {}}], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - ] - }, - "expected": { - "result": "skipped", - "messages": ["IS-IS-SR not configured"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "runs successfully", - "eos_data": [ - { - "entries": { - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 - Tunnel not found"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect tunnel type", - "eos_data": [ - { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "tunnel"}]}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.13/32 Type: tunnel - Tunnel is incorrect"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect nexthop", - "eos_data": [ - { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.122/32", - "vias": [ - {"type": "ip", "interface": "Ethernet1", "nexthop": "10.0.1.2"}, - {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}, - ], - }, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - Tunnel is incorrect"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect nexthop", - "eos_data": [ - { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.122/32", - "vias": [ - {"type": "ip", "interface": "Ethernet4", "nexthop": "10.0.1.1"}, - {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}, - ], - }, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.1 Type: ip Interface: Ethernet4 - Tunnel is incorrect"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect interface", - "eos_data": [ - { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "TI-LFA", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.122/32", - "vias": [ - {"type": "ip", "interface": "Ethernet1", "nexthop": "10.0.1.2"}, - {"type": "ip", "interface": "Ethernet2", "nexthop": "10.0.1.3"}, - ], - }, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - Tunnel is incorrect"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect tunnel ID type", - "eos_data": [ - { - "entries": { - "3": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "31": { - "endpoint": "1.0.0.13/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "32": { - "endpoint": "1.0.0.122/32", - "vias": [ - { - "type": "ip", - "nexthop": "10.0.1.1", - "interface": "Ethernet1", - "labels": ["900021"], - }, - { - "type": "ip", - "nexthop": "10.0.1.3", - "interface": "Ethernet2", - "labels": ["900021"], - }, - ], - }, - "2": { - "endpoint": "1.0.0.111/32", - "vias": [ - { - "type": "tunnel", - "tunnelId": {"type": "unset", "index": 4}, - "labels": ["3"], - } - ], - }, - } - } - ], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.111/32", - "vias": [ - {"type": "tunnel", "tunnel_id": "ti-lfa"}, - ], - }, - ] - }, - "expected": { - "result": "failure", - "messages": ["Endpoint: 1.0.0.111/32 Type: tunnel Tunnel ID: ti-lfa - Tunnel is incorrect"], - }, - }, - { - "test": VerifyISISSegmentRoutingTunnels, - "name": "skipped with ISIS-SR not running", - "eos_data": [{"entries": {}}], - "inputs": { - "entries": [ - {"endpoint": "1.0.0.122/32"}, - {"endpoint": "1.0.0.13/32", "vias": [{"type": "ip"}]}, - { - "endpoint": "1.0.0.111/32", - "vias": [ - {"type": "tunnel", "tunnel_id": "unset"}, - ], - }, - ] - }, - "expected": { - "result": "skipped", - "messages": ["IS-IS-SR not configured"], - }, - }, -] +} diff --git a/tests/units/anta_tests/routing/test_ospf.py b/tests/units/anta_tests/routing/test_ospf.py index 0c736dc..4dd5c63 100644 --- a/tests/units/anta_tests/routing/test_ospf.py +++ b/tests/units/anta_tests/routing/test_ospf.py @@ -5,15 +5,24 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.routing.ospf import VerifyOSPFMaxLSA, VerifyOSPFNeighborCount, VerifyOSPFNeighborState -from tests.units.anta_tests import test +from tests.units.anta_tests import AntaUnitTest, test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyOSPFNeighborState, +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + TypeAlias = type + + +AntaUnitTestDataDict: TypeAlias = dict[tuple[type[AntaTest], str], AntaUnitTest] + +DATA: AntaUnitTestDataDict = { + (VerifyOSPFNeighborState, "success"): { "eos_data": [ { "vrfs": { @@ -39,9 +48,9 @@ DATA: list[dict[str, Any]] = [ "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", }, - ], - }, - }, + ] + } + } }, "BLAH": { "instList": { @@ -55,20 +64,17 @@ DATA: list[dict[str, Any]] = [ "adjacencyState": "full", "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", - }, - ], - }, - }, + } + ] + } + } }, - }, - }, + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyOSPFNeighborState, + (VerifyOSPFNeighborState, "failure"): { "eos_data": [ { "vrfs": { @@ -94,9 +100,9 @@ DATA: list[dict[str, Any]] = [ "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", }, - ], - }, - }, + ] + } + } }, "BLAH": { "instList": { @@ -110,63 +116,31 @@ DATA: list[dict[str, Any]] = [ "adjacencyState": "down", "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", - }, - ], - }, - }, + } + ] + } + } }, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Instance: 666 VRF: default Interface: 7.7.7.7 - Incorrect adjacency state - Expected: Full Actual: 2-way", "Instance: 777 VRF: BLAH Interface: 8.8.8.8 - Incorrect adjacency state - Expected: Full Actual: down", ], }, }, - { - "name": "skipped-ospf-not-configured", - "test": VerifyOSPFNeighborState, - "eos_data": [ - { - "vrfs": {}, - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, + (VerifyOSPFNeighborState, "skipped-ospf-not-configured"): { + "eos_data": [{"vrfs": {}}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["OSPF not configured"]}, }, - { - "name": "skipped-neighbor-not-found", - "test": VerifyOSPFNeighborState, - "eos_data": [ - { - "vrfs": { - "default": { - "instList": { - "666": { - "ospfNeighborEntries": [], - }, - }, - }, - "BLAH": { - "instList": { - "777": { - "ospfNeighborEntries": [], - }, - }, - }, - }, - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["No OSPF neighbor detected"]}, + (VerifyOSPFNeighborState, "skipped-neighbor-not-found"): { + "eos_data": [{"vrfs": {"default": {"instList": {"666": {"ospfNeighborEntries": []}}}, "BLAH": {"instList": {"777": {"ospfNeighborEntries": []}}}}}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["No OSPF neighbor detected"]}, }, - { - "name": "success", - "test": VerifyOSPFNeighborCount, + (VerifyOSPFNeighborCount, "success"): { "eos_data": [ { "vrfs": { @@ -192,9 +166,9 @@ DATA: list[dict[str, Any]] = [ "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", }, - ], - }, - }, + ] + } + } }, "BLAH": { "instList": { @@ -208,20 +182,18 @@ DATA: list[dict[str, Any]] = [ "adjacencyState": "full", "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", - }, - ], - }, - }, + } + ] + } + } }, - }, - }, + } + } ], "inputs": {"number": 3}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-good-number-wrong-state", - "test": VerifyOSPFNeighborCount, + (VerifyOSPFNeighborCount, "failure-good-number-wrong-state"): { "eos_data": [ { "vrfs": { @@ -247,9 +219,9 @@ DATA: list[dict[str, Any]] = [ "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", }, - ], - }, - }, + ] + } + } }, "BLAH": { "instList": { @@ -263,65 +235,28 @@ DATA: list[dict[str, Any]] = [ "adjacencyState": "down", "inactivity": 1683298014.844345, "interfaceAddress": "10.3.0.1", - }, - ], - }, - }, + } + ] + } + } }, - }, - }, + } + } ], "inputs": {"number": 3}, - "expected": { - "result": "failure", - "messages": ["Neighbor count mismatch - Expected: 3 Actual: 1"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Neighbor count mismatch - Expected: 3 Actual: 1"]}, }, - { - "name": "skipped-ospf-not-configured", - "test": VerifyOSPFNeighborCount, - "eos_data": [ - { - "vrfs": {}, - }, - ], + (VerifyOSPFNeighborCount, "skipped-ospf-not-configured"): { + "eos_data": [{"vrfs": {}}], "inputs": {"number": 3}, - "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["OSPF not configured"]}, }, - { - "name": "skipped-no-neighbor-detected", - "test": VerifyOSPFNeighborCount, - "eos_data": [ - { - "vrfs": { - "default": { - "instList": { - "666": { - "ospfNeighborEntries": [], - }, - }, - }, - "BLAH": { - "instList": { - "777": { - "ospfNeighborEntries": [], - }, - }, - }, - }, - }, - ], + (VerifyOSPFNeighborCount, "skipped-no-neighbor-detected"): { + "eos_data": [{"vrfs": {"default": {"instList": {"666": {"ospfNeighborEntries": []}}}, "BLAH": {"instList": {"777": {"ospfNeighborEntries": []}}}}}], "inputs": {"number": 3}, - "expected": { - "result": "skipped", - "messages": [ - "No OSPF neighbor detected", - ], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["No OSPF neighbor detected"]}, }, - { - "name": "success", - "test": VerifyOSPFMaxLSA, + (VerifyOSPFMaxLSA, "success"): { "eos_data": [ { "vrfs": { @@ -329,10 +264,7 @@ DATA: list[dict[str, Any]] = [ "instList": { "1": { "instanceId": 1, - "maxLsaInformation": { - "maxLsa": 12000, - "maxLsaThreshold": 75, - }, + "maxLsaInformation": {"maxLsa": 12000, "maxLsaThreshold": 75}, "routerId": "1.1.1.1", "lsaInformation": { "lsaArrivalInterval": 1000, @@ -341,17 +273,14 @@ DATA: list[dict[str, Any]] = [ "lsaMaxWaitInterval": 5000, "numLsa": 9, }, - }, - }, + } + } }, "TEST": { "instList": { "10": { "instanceId": 10, - "maxLsaInformation": { - "maxLsa": 1000, - "maxLsaThreshold": 75, - }, + "maxLsaInformation": {"maxLsa": 1000, "maxLsaThreshold": 75}, "routerId": "20.20.20.20", "lsaInformation": { "lsaArrivalInterval": 1000, @@ -360,18 +289,15 @@ DATA: list[dict[str, Any]] = [ "lsaMaxWaitInterval": 5000, "numLsa": 5, }, - }, - }, + } + } }, - }, - }, + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyOSPFMaxLSA, + (VerifyOSPFMaxLSA, "failure"): { "eos_data": [ { "vrfs": { @@ -379,10 +305,7 @@ DATA: list[dict[str, Any]] = [ "instList": { "1": { "instanceId": 1, - "maxLsaInformation": { - "maxLsa": 12000, - "maxLsaThreshold": 75, - }, + "maxLsaInformation": {"maxLsa": 12000, "maxLsaThreshold": 75}, "routerId": "1.1.1.1", "lsaInformation": { "lsaArrivalInterval": 1000, @@ -391,17 +314,14 @@ DATA: list[dict[str, Any]] = [ "lsaMaxWaitInterval": 5000, "numLsa": 11500, }, - }, - }, + } + } }, "TEST": { "instList": { "10": { "instanceId": 10, - "maxLsaInformation": { - "maxLsa": 1000, - "maxLsaThreshold": 75, - }, + "maxLsaInformation": {"maxLsa": 1000, "maxLsaThreshold": 75}, "routerId": "20.20.20.20", "lsaInformation": { "lsaArrivalInterval": 1000, @@ -410,30 +330,19 @@ DATA: list[dict[str, Any]] = [ "lsaMaxWaitInterval": 5000, "numLsa": 1500, }, - }, - }, + } + } }, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Instance: 1 - Crossed the maximum LSA threshold - Expected: < 9000 Actual: 11500", "Instance: 10 - Crossed the maximum LSA threshold - Expected: < 750 Actual: 1500", ], }, }, - { - "name": "skipped", - "test": VerifyOSPFMaxLSA, - "eos_data": [ - { - "vrfs": {}, - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, - }, -] + (VerifyOSPFMaxLSA, "skipped"): {"eos_data": [{"vrfs": {}}], "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["OSPF not configured"]}}, +} diff --git a/tests/units/anta_tests/test_aaa.py b/tests/units/anta_tests/test_aaa.py index c91f051..c2b8c16 100644 --- a/tests/units/anta_tests/test_aaa.py +++ b/tests/units/anta_tests/test_aaa.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.aaa import ( VerifyAcctConsoleMethods, VerifyAcctDefaultMethods, @@ -18,510 +21,326 @@ from anta.tests.aaa import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyTacacsSourceIntf, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyTacacsSourceIntf, "success"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"intf": "Management0", "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyTacacsSourceIntf, - "eos_data": [ - { - "tacacsServers": [], - "groups": {}, - "srcIntf": {}, - }, - ], + (VerifyTacacsSourceIntf, "failure-not-configured"): { + "eos_data": [{"tacacsServers": [], "groups": {}, "srcIntf": {}}], "inputs": {"intf": "Management0", "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT Source Interface: Management0 - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT Source Interface: Management0 - Not configured"]}, }, - { - "name": "failure-wrong-intf", - "test": VerifyTacacsSourceIntf, + (VerifyTacacsSourceIntf, "failure-wrong-intf"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management1"}, - }, + } ], "inputs": {"intf": "Management0", "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Source interface mismatch - Expected: Management0 Actual: Management1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Source interface mismatch - Expected: Management0 Actual: Management1"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifyTacacsSourceIntf, + (VerifyTacacsSourceIntf, "failure-wrong-vrf"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"PROD": "Management0"}, - }, + } ], "inputs": {"intf": "Management0", "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT Source Interface: Management0 - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT Source Interface: Management0 - Not configured"]}, }, - { - "name": "success", - "test": VerifyTacacsServers, + (VerifyTacacsServers, "success"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-servers", - "test": VerifyTacacsServers, - "eos_data": [ - { - "tacacsServers": [], - "groups": {}, - "srcIntf": {}, - }, - ], + (VerifyTacacsServers, "failure-no-servers"): { + "eos_data": [{"tacacsServers": [], "groups": {}, "srcIntf": {}}], "inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["No TACACS servers are configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No TACACS servers are configured"]}, }, - { - "name": "failure-not-configured", - "test": VerifyTacacsServers, + (VerifyTacacsServers, "failure-not-configured"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"servers": ["10.22.10.91", "10.22.10.92"], "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["TACACS servers 10.22.10.92 are not configured in VRF MGMT"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["TACACS servers 10.22.10.92 are not configured in VRF MGMT"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifyTacacsServers, + (VerifyTacacsServers, "failure-wrong-vrf"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "PROD"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "PROD"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"servers": ["10.22.10.91"], "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["TACACS servers 10.22.10.91 are not configured in VRF MGMT"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["TACACS servers 10.22.10.91 are not configured in VRF MGMT"]}, }, - { - "name": "success", - "test": VerifyTacacsServerGroups, + (VerifyTacacsServerGroups, "success"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP1": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"groups": ["GROUP1"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-server-groups", - "test": VerifyTacacsServerGroups, - "eos_data": [ - { - "tacacsServers": [], - "groups": {}, - "srcIntf": {}, - }, - ], + (VerifyTacacsServerGroups, "failure-no-server-groups"): { + "eos_data": [{"tacacsServers": [], "groups": {}, "srcIntf": {}}], "inputs": {"groups": ["GROUP1"]}, - "expected": {"result": "failure", "messages": ["No TACACS server group(s) are configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No TACACS server group(s) are configured"]}, }, - { - "name": "failure-not-configured", - "test": VerifyTacacsServerGroups, + (VerifyTacacsServerGroups, "failure-not-configured"): { "eos_data": [ { - "tacacsServers": [ - { - "serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}, - }, - ], + "tacacsServers": [{"serverInfo": {"hostname": "10.22.10.91", "authport": 49, "vrf": "MGMT"}}], "groups": {"GROUP2": {"serverGroup": "TACACS+", "members": [{"hostname": "SERVER1", "authport": 49, "vrf": "MGMT"}]}}, "srcIntf": {"MGMT": "Management0"}, - }, + } ], "inputs": {"groups": ["GROUP1"]}, - "expected": {"result": "failure", "messages": ["TACACS server group(s) GROUP1 are not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["TACACS server group(s) GROUP1 are not configured"]}, }, - { - "name": "success-login-enable", - "test": VerifyAuthenMethods, + (VerifyAuthenMethods, "success-login-enable"): { "eos_data": [ { "loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group tacacs+", "local"]}}, "enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "dot1xAuthenMethods": {"default": {"methods": ["group radius"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-dot1x", - "test": VerifyAuthenMethods, + (VerifyAuthenMethods, "success-dot1x"): { "eos_data": [ { "loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group tacacs+", "local"]}}, "enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "dot1xAuthenMethods": {"default": {"methods": ["group radius"]}}, - }, + } ], "inputs": {"methods": ["radius"], "types": ["dot1x"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-login-console", - "test": VerifyAuthenMethods, + (VerifyAuthenMethods, "failure-no-login-console"): { "eos_data": [ { "loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "dot1xAuthenMethods": {"default": {"methods": ["group radius"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]}, - "expected": {"result": "failure", "messages": ["AAA authentication methods are not configured for login console"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA authentication methods are not configured for login console"]}, }, - { - "name": "failure-login-console", - "test": VerifyAuthenMethods, + (VerifyAuthenMethods, "failure-login-console"): { "eos_data": [ { "loginAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}, "login": {"methods": ["group radius", "local"]}}, "enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "dot1xAuthenMethods": {"default": {"methods": ["group radius"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]}, - "expected": {"result": "failure", "messages": ["AAA authentication methods group tacacs+, local are not matching for login console"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA authentication methods group tacacs+, local are not matching for login console"]}, }, - { - "name": "failure-login-default", - "test": VerifyAuthenMethods, + (VerifyAuthenMethods, "failure-login-default"): { "eos_data": [ { "loginAuthenMethods": {"default": {"methods": ["group radius", "local"]}, "login": {"methods": ["group tacacs+", "local"]}}, "enableAuthenMethods": {"default": {"methods": ["group tacacs+", "local"]}}, "dot1xAuthenMethods": {"default": {"methods": ["group radius"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["login", "enable"]}, - "expected": {"result": "failure", "messages": ["AAA authentication methods group tacacs+, local are not matching for login"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA authentication methods group tacacs+, local are not matching for login"]}, }, - { - "name": "success", - "test": VerifyAuthzMethods, + (VerifyAuthzMethods, "success"): { "eos_data": [ { "commandsAuthzMethods": {"privilege0-15": {"methods": ["group tacacs+", "local"]}}, "execAuthzMethods": {"exec": {"methods": ["group tacacs+", "local"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-skipping-exec", - "test": VerifyAuthzMethods, + (VerifyAuthzMethods, "success-skipping-exec"): { "eos_data": [ { "commandsAuthzMethods": {"privilege0-15": {"methods": ["group tacacs+", "local"]}}, "execAuthzMethods": {"exec": {"methods": ["group tacacs+", "local"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["commands"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-commands", - "test": VerifyAuthzMethods, + (VerifyAuthzMethods, "failure-commands"): { "eos_data": [ { "commandsAuthzMethods": {"privilege0-15": {"methods": ["group radius", "local"]}}, "execAuthzMethods": {"exec": {"methods": ["group tacacs+", "local"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]}, - "expected": {"result": "failure", "messages": ["AAA authorization methods group tacacs+, local are not matching for commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA authorization methods group tacacs+, local are not matching for commands"]}, }, - { - "name": "failure-exec", - "test": VerifyAuthzMethods, + (VerifyAuthzMethods, "failure-exec"): { "eos_data": [ { "commandsAuthzMethods": {"privilege0-15": {"methods": ["group tacacs+", "local"]}}, "execAuthzMethods": {"exec": {"methods": ["group radius", "local"]}}, - }, + } ], "inputs": {"methods": ["tacacs+", "local"], "types": ["commands", "exec"]}, - "expected": {"result": "failure", "messages": ["AAA authorization methods group tacacs+, local are not matching for exec"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA authorization methods group tacacs+, local are not matching for exec"]}, }, - { - "name": "success-commands-exec-system", - "test": VerifyAcctDefaultMethods, + (VerifyAcctDefaultMethods, "success-commands-exec-system"): { "eos_data": [ { "commandsAcctMethods": {"privilege0-15": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-dot1x", - "test": VerifyAcctDefaultMethods, + (VerifyAcctDefaultMethods, "success-dot1x"): { "eos_data": [ { "commandsAcctMethods": {"privilege0-15": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultAction": "startStop", "defaultMethods": ["group radius", "logging"], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["radius", "logging"], "types": ["dot1x"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyAcctDefaultMethods, + (VerifyAcctDefaultMethods, "failure-not-configured"): { "eos_data": [ { "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA default accounting is not configured for commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA default accounting is not configured for commands"]}, }, - { - "name": "failure-not-configured-empty", - "test": VerifyAcctDefaultMethods, + (VerifyAcctDefaultMethods, "failure-not-configured-empty"): { "eos_data": [ { "systemAcctMethods": {"system": {"defaultMethods": [], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}}, "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA default accounting is not configured for system, exec, commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA default accounting is not configured for system, exec, commands"]}, }, - { - "name": "failure-not-matching", - "test": VerifyAcctDefaultMethods, + (VerifyAcctDefaultMethods, "failure-not-matching"): { "eos_data": [ { "commandsAcctMethods": {"privilege0-15": {"defaultAction": "startStop", "defaultMethods": ["group radius", "logging"], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "systemAcctMethods": {"system": {"defaultAction": "startStop", "defaultMethods": ["group tacacs+", "logging"], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA accounting default methods group tacacs+, logging are not matching for commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA accounting default methods group tacacs+, logging are not matching for commands"]}, }, - { - "name": "success-commands-exec-system", - "test": VerifyAcctConsoleMethods, + (VerifyAcctConsoleMethods, "success-commands-exec-system"): { "eos_data": [ { - "commandsAcctMethods": { - "privilege0-15": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "execAcctMethods": { - "exec": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "systemAcctMethods": { - "system": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, + "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "execAcctMethods": {"exec": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "systemAcctMethods": {"system": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-dot1x", - "test": VerifyAcctConsoleMethods, + (VerifyAcctConsoleMethods, "success-dot1x"): { "eos_data": [ { - "commandsAcctMethods": { - "privilege0-15": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "execAcctMethods": { - "exec": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "systemAcctMethods": { - "system": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "dot1xAcctMethods": { - "dot1x": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - }, + "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "execAcctMethods": {"exec": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "systemAcctMethods": {"system": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["dot1x"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyAcctConsoleMethods, + (VerifyAcctConsoleMethods, "failure-not-configured"): { "eos_data": [ { - "commandsAcctMethods": { - "privilege0-15": { - "defaultMethods": [], - "consoleMethods": [], - }, - }, - "execAcctMethods": { - "exec": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "systemAcctMethods": { - "system": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, + "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}}, + "execAcctMethods": {"exec": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "systemAcctMethods": {"system": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA console accounting is not configured for commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA console accounting is not configured for commands"]}, }, - { - "name": "failure-not-configured-empty", - "test": VerifyAcctConsoleMethods, + (VerifyAcctConsoleMethods, "failure-not-configured-empty"): { "eos_data": [ { "systemAcctMethods": {"system": {"defaultMethods": [], "consoleMethods": []}}, "execAcctMethods": {"exec": {"defaultMethods": [], "consoleMethods": []}}, "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleMethods": []}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA console accounting is not configured for system, exec, commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA console accounting is not configured for system, exec, commands"]}, }, - { - "name": "failure-not-matching", - "test": VerifyAcctConsoleMethods, + (VerifyAcctConsoleMethods, "failure-not-matching"): { "eos_data": [ { - "commandsAcctMethods": { - "privilege0-15": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group radius", "logging"], - }, - }, - "execAcctMethods": { - "exec": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, - "systemAcctMethods": { - "system": { - "defaultMethods": [], - "consoleAction": "startStop", - "consoleMethods": ["group tacacs+", "logging"], - }, - }, + "commandsAcctMethods": {"privilege0-15": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group radius", "logging"]}}, + "execAcctMethods": {"exec": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, + "systemAcctMethods": {"system": {"defaultMethods": [], "consoleAction": "startStop", "consoleMethods": ["group tacacs+", "logging"]}}, "dot1xAcctMethods": {"dot1x": {"defaultMethods": [], "consoleMethods": []}}, - }, + } ], "inputs": {"methods": ["tacacs+", "logging"], "types": ["commands", "exec", "system"]}, - "expected": {"result": "failure", "messages": ["AAA accounting console methods group tacacs+, logging are not matching for commands"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA accounting console methods group tacacs+, logging are not matching for commands"]}, }, -] +} diff --git a/tests/units/anta_tests/test_avt.py b/tests/units/anta_tests/test_avt.py index 39c5e1e..aeeefaa 100644 --- a/tests/units/anta_tests/test_avt.py +++ b/tests/units/anta_tests/test_avt.py @@ -5,15 +5,19 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.avt import VerifyAVTPathHealth, VerifyAVTRole, VerifyAVTSpecificPath from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyAVTPathHealth, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyAVTPathHealth, "success"): { "eos_data": [ { "vrfs": { @@ -21,18 +25,10 @@ DATA: list[dict[str, Any]] = [ "avts": { "DATA-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -41,12 +37,8 @@ DATA: list[dict[str, Any]] = [ "avts": { "GUEST-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -55,28 +47,16 @@ DATA: list[dict[str, Any]] = [ "avts": { "CONTROL-PLANE-PROFILE": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, "DEFAULT-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, } @@ -85,21 +65,14 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-avt-not-configured", - "test": VerifyAVTPathHealth, + (VerifyAVTPathHealth, "failure-avt-not-configured"): { "eos_data": [{"vrfs": {}}], "inputs": {}, - "expected": { - "result": "failure", - "messages": ["Adaptive virtual topology paths are not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Adaptive virtual topology paths are not configured"]}, }, - { - "name": "failure-not-active-path", - "test": VerifyAVTPathHealth, + (VerifyAVTPathHealth, "failure-not-active-path"): { "eos_data": [ { "vrfs": { @@ -107,18 +80,10 @@ DATA: list[dict[str, Any]] = [ "avts": { "DATA-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -127,12 +92,8 @@ DATA: list[dict[str, Any]] = [ "avts": { "GUEST-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": False}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": False}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -141,28 +102,16 @@ DATA: list[dict[str, Any]] = [ "avts": { "CONTROL-PLANE-PROFILE": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": False}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": False}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, "DEFAULT-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": False}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": False}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, } @@ -172,7 +121,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VRF: guest Profile: GUEST-AVT-POLICY-DEFAULT AVT path: direct:10 - Not active", "VRF: default Profile: CONTROL-PLANE-PROFILE AVT path: direct:1 - Not active", @@ -180,9 +129,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-invalid-path", - "test": VerifyAVTPathHealth, + (VerifyAVTPathHealth, "failure-invalid-path"): { "eos_data": [ { "vrfs": { @@ -190,18 +137,10 @@ DATA: list[dict[str, Any]] = [ "avts": { "DATA-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": False, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": False, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -210,12 +149,8 @@ DATA: list[dict[str, Any]] = [ "avts": { "GUEST-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": False, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": False, "active": True}}, } } } @@ -224,28 +159,16 @@ DATA: list[dict[str, Any]] = [ "avts": { "CONTROL-PLANE-PROFILE": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": False, "active": True}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": False, "active": True}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, "DEFAULT-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": False, "active": True}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": False, "active": True}}, } }, } @@ -255,7 +178,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VRF: data Profile: DATA-AVT-POLICY-DEFAULT AVT path: direct:10 - Invalid", "VRF: guest Profile: GUEST-AVT-POLICY-DEFAULT AVT path: direct:8 - Invalid", @@ -264,9 +187,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-not-active-and-invalid", - "test": VerifyAVTPathHealth, + (VerifyAVTPathHealth, "failure-not-active-and-invalid"): { "eos_data": [ { "vrfs": { @@ -274,18 +195,10 @@ DATA: list[dict[str, Any]] = [ "avts": { "DATA-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": False, "active": False}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": False}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": False, "active": False}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": False}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } } } @@ -294,12 +207,8 @@ DATA: list[dict[str, Any]] = [ "avts": { "GUEST-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": False, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": False, "active": False}, - }, + "direct:10": {"flags": {"directPath": True, "valid": False, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": False, "active": False}}, } } } @@ -308,28 +217,16 @@ DATA: list[dict[str, Any]] = [ "avts": { "CONTROL-PLANE-PROFILE": { "avtPaths": { - "direct:9": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:10": { - "flags": {"directPath": True, "valid": False, "active": False}, - }, - "direct:1": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": True, "active": True}, - }, + "direct:9": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:10": {"flags": {"directPath": True, "valid": False, "active": False}}, + "direct:1": {"flags": {"directPath": True, "valid": True, "active": True}}, + "direct:8": {"flags": {"directPath": True, "valid": True, "active": True}}, } }, "DEFAULT-AVT-POLICY-DEFAULT": { "avtPaths": { - "direct:10": { - "flags": {"directPath": True, "valid": True, "active": False}, - }, - "direct:8": { - "flags": {"directPath": True, "valid": False, "active": False}, - }, + "direct:10": {"flags": {"directPath": True, "valid": True, "active": False}}, + "direct:8": {"flags": {"directPath": True, "valid": False, "active": False}}, } }, } @@ -339,7 +236,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VRF: data Profile: DATA-AVT-POLICY-DEFAULT AVT path: direct:10 - Invalid and not active", "VRF: data Profile: DATA-AVT-POLICY-DEFAULT AVT path: direct:1 - Not active", @@ -351,9 +248,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyAVTSpecificPath, + (VerifyAVTSpecificPath, "success"): { "eos_data": [ { "vrfs": { @@ -383,7 +278,7 @@ DATA: list[dict[str, Any]] = [ }, } } - }, + } }, "data": { "avts": { @@ -415,11 +310,11 @@ DATA: list[dict[str, Any]] = [ "destination": "10.101.255.1", }, } - }, + } } }, } - }, + } ], "inputs": { "avt_paths": [ @@ -428,14 +323,10 @@ DATA: list[dict[str, Any]] = [ {"avt_name": "DATA-AVT-POLICY-CONTROL-PLANE", "vrf": "data", "destination": "10.101.255.1", "next_hop": "10.101.255.2"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-peer", - "test": VerifyAVTSpecificPath, - "eos_data": [ - {"vrfs": {}}, - ], + (VerifyAVTSpecificPath, "failure-no-peer"): { + "eos_data": [{"vrfs": {}}], "inputs": { "avt_paths": [ {"avt_name": "MGMT-AVT-POLICY-DEFAULT", "vrf": "default", "destination": "10.101.255.2", "next_hop": "10.101.255.1", "path_type": "multihop"}, @@ -443,13 +334,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["AVT: MGMT-AVT-POLICY-DEFAULT VRF: default Destination: 10.101.255.2 Next-hop: 10.101.255.1 - No AVT path configured"], }, }, - { - "name": "failure-path_type_check_true", - "test": VerifyAVTSpecificPath, + (VerifyAVTSpecificPath, "failure-path_type_check_true"): { "eos_data": [ { "vrfs": { @@ -469,7 +358,7 @@ DATA: list[dict[str, Any]] = [ }, } } - }, + } }, "data": { "avts": { @@ -486,11 +375,11 @@ DATA: list[dict[str, Any]] = [ "destination": "10.101.255.3", }, } - }, + } } }, } - }, + } ], "inputs": { "avt_paths": [ @@ -505,16 +394,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AVT: DEFAULT-AVT-POLICY-CONTROL-PLANE VRF: default Destination: 10.101.255.2 Next-hop: 10.101.255.11 Path Type: multihop - Path not found", "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.21 Path Type: direct - Path not found", ], }, }, - { - "name": "failure-path_type_check_false", - "test": VerifyAVTSpecificPath, + (VerifyAVTSpecificPath, "failure-path_type_check_false"): { "eos_data": [ { "vrfs": { @@ -534,7 +421,7 @@ DATA: list[dict[str, Any]] = [ }, } } - }, + } }, "data": { "avts": { @@ -551,34 +438,27 @@ DATA: list[dict[str, Any]] = [ "destination": "10.101.255.3", }, } - }, + } } }, } - }, + } ], "inputs": { "avt_paths": [ - { - "avt_name": "DEFAULT-AVT-POLICY-CONTROL-PLANE", - "vrf": "default", - "destination": "10.101.255.2", - "next_hop": "10.101.255.11", - }, + {"avt_name": "DEFAULT-AVT-POLICY-CONTROL-PLANE", "vrf": "default", "destination": "10.101.255.2", "next_hop": "10.101.255.11"}, {"avt_name": "DATA-AVT-POLICY-CONTROL-PLANE", "vrf": "data", "destination": "10.101.255.1", "next_hop": "10.101.255.21"}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "AVT: DEFAULT-AVT-POLICY-CONTROL-PLANE VRF: default Destination: 10.101.255.2 Next-hop: 10.101.255.11 - Path not found", "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.21 - Path not found", ], }, }, - { - "name": "failure-incorrect-path", - "test": VerifyAVTSpecificPath, + (VerifyAVTSpecificPath, "failure-incorrect-path"): { "eos_data": [ { "vrfs": { @@ -590,10 +470,10 @@ DATA: list[dict[str, Any]] = [ "flags": {"directPath": False, "valid": False, "active": True}, "nexthopAddr": "10.101.255.1", "destination": "10.101.255.2", - }, + } } } - }, + } }, "data": { "avts": { @@ -625,11 +505,11 @@ DATA: list[dict[str, Any]] = [ "destination": "10.101.255.1", }, } - }, + } } }, } - }, + } ], "inputs": { "avt_paths": [ @@ -644,29 +524,21 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "AVT: DEFAULT-AVT-POLICY-CONTROL-PLANE VRF: default Destination: 10.101.255.2 Next-hop: 10.101.255.1 - " - "Incorrect path multihop:3 - Valid: False Active: True", - "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.1 - " - "Incorrect path direct:10 - Valid: False Active: True", - "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.1 - " - "Incorrect path direct:9 - Valid: True Active: False", + "AVT: DEFAULT-AVT-POLICY-CONTROL-PLANE VRF: default Destination: 10.101.255.2 Next-hop: 10.101.255.1 - Incorrect path multihop:3 - " + "Valid: False Active: True", + "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.1 - Incorrect path direct:10 - " + "Valid: False Active: True", + "AVT: DATA-AVT-POLICY-CONTROL-PLANE VRF: data Destination: 10.101.255.1 Next-hop: 10.101.255.1 - Incorrect path direct:9 - " + "Valid: True Active: False", ], }, }, - { - "name": "success", - "test": VerifyAVTRole, - "eos_data": [{"role": "edge"}], - "inputs": {"role": "edge"}, - "expected": {"result": "success"}, - }, - { - "name": "failure-incorrect-role", - "test": VerifyAVTRole, + (VerifyAVTRole, "success"): {"eos_data": [{"role": "edge"}], "inputs": {"role": "edge"}, "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyAVTRole, "failure-incorrect-role"): { "eos_data": [{"role": "transit"}], "inputs": {"role": "edge"}, - "expected": {"result": "failure", "messages": ["AVT role mismatch - Expected: edge Actual: transit"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AVT role mismatch - Expected: edge Actual: transit"]}, }, -] +} diff --git a/tests/units/anta_tests/test_bfd.py b/tests/units/anta_tests/test_bfd.py index dd67397..d08bfdc 100644 --- a/tests/units/anta_tests/test_bfd.py +++ b/tests/units/anta_tests/test_bfd.py @@ -6,15 +6,19 @@ # pylint: disable=C0302 from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.bfd import VerifyBFDPeersHealth, VerifyBFDPeersIntervals, VerifyBFDPeersRegProtocols, VerifyBFDSpecificPeers from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyBFDPeersIntervals, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyBFDPeersIntervals, "success"): { "eos_data": [ { "vrfs": { @@ -22,14 +26,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.7": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -38,14 +35,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.70": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -59,11 +49,9 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "192.0.255.70", "vrf": "MGMT", "tx_interval": 1200, "rx_interval": 1200, "multiplier": 3}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-detection-time", - "test": VerifyBFDPeersIntervals, + (VerifyBFDPeersIntervals, "success-detection-time"): { "eos_data": [ { "vrfs": { @@ -71,14 +59,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.7": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -87,14 +68,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.70": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -108,11 +82,9 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "192.0.255.70", "vrf": "MGMT", "tx_interval": 1200, "rx_interval": 1200, "multiplier": 3, "detection_time": 3600}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-peer", - "test": VerifyBFDPeersIntervals, + (VerifyBFDPeersIntervals, "failure-no-peer"): { "eos_data": [ { "vrfs": { @@ -120,14 +92,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.7": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -136,14 +101,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.71": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1200000, - "operRxInterval": 1200000, - "detectMult": 3, - "detectTime": 3600000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1200000, "operRxInterval": 1200000, "detectMult": 3, "detectTime": 3600000}} } } } @@ -157,17 +115,9 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "192.0.255.70", "vrf": "MGMT", "tx_interval": 1200, "rx_interval": 1200, "multiplier": 3, "detection_time": 3600}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 192.0.255.7 VRF: CS - Not found", - "Peer: 192.0.255.70 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 192.0.255.7 VRF: CS - Not found", "Peer: 192.0.255.70 VRF: MGMT - Not found"]}, }, - { - "name": "failure-incorrect-timers", - "test": VerifyBFDPeersIntervals, + (VerifyBFDPeersIntervals, "failure-incorrect-timers"): { "eos_data": [ { "vrfs": { @@ -175,14 +125,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.7": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1300000, - "operRxInterval": 1200000, - "detectMult": 4, - "detectTime": 4000000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1300000, "operRxInterval": 1200000, "detectMult": 4, "detectTime": 4000000}} } } } @@ -190,16 +133,7 @@ DATA: list[dict[str, Any]] = [ "MGMT": { "ipv4Neighbors": { "192.0.255.70": { - "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 120000, - "operRxInterval": 120000, - "detectMult": 5, - "detectTime": 4000000, - } - } - } + "peerStats": {"": {"peerStatsDetail": {"operTxInterval": 120000, "operRxInterval": 120000, "detectMult": 5, "detectTime": 4000000}}} } } }, @@ -213,7 +147,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Incorrect Transmit interval - Expected: 1200 Actual: 1300", "Peer: 192.0.255.7 VRF: default - Incorrect Multiplier - Expected: 3 Actual: 4", @@ -223,9 +157,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-timers-with-detection-time", - "test": VerifyBFDPeersIntervals, + (VerifyBFDPeersIntervals, "failure-incorrect-timers-with-detection-time"): { "eos_data": [ { "vrfs": { @@ -233,14 +165,7 @@ DATA: list[dict[str, Any]] = [ "ipv4Neighbors": { "192.0.255.7": { "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 1300000, - "operRxInterval": 1200000, - "detectMult": 4, - "detectTime": 4000000, - } - } + "": {"peerStatsDetail": {"operTxInterval": 1300000, "operRxInterval": 1200000, "detectMult": 4, "detectTime": 4000000}} } } } @@ -248,16 +173,7 @@ DATA: list[dict[str, Any]] = [ "MGMT": { "ipv4Neighbors": { "192.0.255.70": { - "peerStats": { - "": { - "peerStatsDetail": { - "operTxInterval": 120000, - "operRxInterval": 120000, - "detectMult": 5, - "detectTime": 4000000, - } - } - } + "peerStats": {"": {"peerStatsDetail": {"operTxInterval": 120000, "operRxInterval": 120000, "detectMult": 5, "detectTime": 4000000}}} } } }, @@ -271,7 +187,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Incorrect Transmit interval - Expected: 1200 Actual: 1300", "Peer: 192.0.255.7 VRF: default - Incorrect Multiplier - Expected: 3 Actual: 4", @@ -283,354 +199,149 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBFDSpecificPeers, + (VerifyBFDSpecificPeers, "success"): { "eos_data": [ { "vrfs": { - "default": { - "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - } - } - } - } - }, - "MGMT": { - "ipv4Neighbors": { - "192.0.255.70": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - } - } - } - } - }, + "default": {"ipv4Neighbors": {"192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132}}}}}, + "MGMT": {"ipv4Neighbors": {"192.0.255.70": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132}}}}}, } } ], "inputs": {"bfd_peers": [{"peer_address": "192.0.255.7", "vrf": "default"}, {"peer_address": "192.0.255.70", "vrf": "MGMT"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-peer", - "test": VerifyBFDSpecificPeers, + (VerifyBFDSpecificPeers, "failure-no-peer"): { "eos_data": [ { "vrfs": { - "default": { - "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - } - } - } - } - }, - "MGMT": { - "ipv4Neighbors": { - "192.0.255.71": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - } - } - } - } - }, + "default": {"ipv4Neighbors": {"192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132}}}}}, + "MGMT": {"ipv4Neighbors": {"192.0.255.71": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132}}}}}, } } ], "inputs": {"bfd_peers": [{"peer_address": "192.0.255.7", "vrf": "CS"}, {"peer_address": "192.0.255.70", "vrf": "MGMT"}]}, - "expected": { - "result": "failure", - "messages": [ - "Peer: 192.0.255.7 VRF: CS - Not found", - "Peer: 192.0.255.70 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 192.0.255.7 VRF: CS - Not found", "Peer: 192.0.255.70 VRF: MGMT - Not found"]}, }, - { - "name": "failure-session-down", - "test": VerifyBFDSpecificPeers, + (VerifyBFDSpecificPeers, "failure-session-down"): { "eos_data": [ { "vrfs": { - "default": { - "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "down", - "remoteDisc": 108328132, - } - } - } - } - }, - "MGMT": { - "ipv4Neighbors": { - "192.0.255.70": { - "peerStats": { - "": { - "status": "down", - "remoteDisc": 0, - } - } - } - } - }, + "default": {"ipv4Neighbors": {"192.0.255.7": {"peerStats": {"": {"status": "down", "remoteDisc": 108328132}}}}}, + "MGMT": {"ipv4Neighbors": {"192.0.255.70": {"peerStats": {"": {"status": "down", "remoteDisc": 0}}}}}, } } ], "inputs": {"bfd_peers": [{"peer_address": "192.0.255.7", "vrf": "default"}, {"peer_address": "192.0.255.70", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Session not properly established - State: down Remote Discriminator: 108328132", "Peer: 192.0.255.70 VRF: MGMT - Session not properly established - State: down Remote Discriminator: 0", ], }, }, - { - "name": "success", - "test": VerifyBFDPeersHealth, + (VerifyBFDPeersHealth, "success"): { "eos_data": [ { "vrfs": { "default": { "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, + "192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}} }, "ipv6Neighbors": {}, }, "MGMT": { "ipv4Neighbors": { - "192.0.255.71": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, + "192.0.255.71": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}} }, "ipv6Neighbors": {}, }, } }, - { - "utcTime": 1703667348.111288, - }, + {"utcTime": 1703667348.111288}, ], "inputs": {"down_threshold": 2}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-peer", - "test": VerifyBFDPeersHealth, + (VerifyBFDPeersHealth, "failure-no-peer"): { "eos_data": [ - { - "vrfs": { - "MGMT": { - "ipv6Neighbors": {}, - "ipv4Neighbors": {}, - }, - "default": { - "ipv6Neighbors": {}, - "ipv4Neighbors": {}, - }, - } - }, - { - "utcTime": 1703658481.8778424, - }, + {"vrfs": {"MGMT": {"ipv6Neighbors": {}, "ipv4Neighbors": {}}, "default": {"ipv6Neighbors": {}, "ipv4Neighbors": {}}}}, + {"utcTime": 1703658481.8778424}, ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["No IPv4 BFD peers are configured for any VRF"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No IPv4 BFD peers are configured for any VRF"]}, }, - { - "name": "failure-session-down", - "test": VerifyBFDPeersHealth, + (VerifyBFDPeersHealth, "failure-session-down"): { "eos_data": [ { "vrfs": { "default": { "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "down", - "remoteDisc": 0, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, - "192.0.255.70": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, + "192.0.255.7": {"peerStats": {"": {"status": "down", "remoteDisc": 0, "lastDown": 1703657258.652725, "l3intf": ""}}}, + "192.0.255.70": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}}, }, "ipv6Neighbors": {}, }, "MGMT": { - "ipv4Neighbors": { - "192.0.255.71": { - "peerStats": { - "": { - "status": "down", - "remoteDisc": 0, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, - }, + "ipv4Neighbors": {"192.0.255.71": {"peerStats": {"": {"status": "down", "remoteDisc": 0, "lastDown": 1703657258.652725, "l3intf": ""}}}}, "ipv6Neighbors": {}, }, } }, - { - "utcTime": 1703658481.8778424, - }, + {"utcTime": 1703658481.8778424}, ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Session not properly established - State: down Remote Discriminator: 0", "Peer: 192.0.255.71 VRF: MGMT - Session not properly established - State: down Remote Discriminator: 0", ], }, }, - { - "name": "failure-session-up-disc", - "test": VerifyBFDPeersHealth, + (VerifyBFDPeersHealth, "failure-session-up-disc"): { "eos_data": [ { "vrfs": { "default": { "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 0, - "lastDown": 1703657258.652725, - "l3intf": "Ethernet2", - } - } - }, - "192.0.255.71": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 0, - "lastDown": 1703657258.652725, - "l3intf": "Ethernet2", - } - } - }, + "192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 0, "lastDown": 1703657258.652725, "l3intf": "Ethernet2"}}}, + "192.0.255.71": {"peerStats": {"": {"status": "up", "remoteDisc": 0, "lastDown": 1703657258.652725, "l3intf": "Ethernet2"}}}, }, "ipv6Neighbors": {}, } } }, - { - "utcTime": 1703658481.8778424, - }, + {"utcTime": 1703658481.8778424}, ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Session not properly established - State: up Remote Discriminator: 0", "Peer: 192.0.255.71 VRF: default - Session not properly established - State: up Remote Discriminator: 0", ], }, }, - { - "name": "failure-last-down", - "test": VerifyBFDPeersHealth, + (VerifyBFDPeersHealth, "failure-last-down"): { "eos_data": [ { "vrfs": { "default": { "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, - "192.0.255.71": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, - "192.0.255.17": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 3940685114, - "lastDown": 1703657258.652725, - "l3intf": "", - } - } - }, + "192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}}, + "192.0.255.71": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}}, + "192.0.255.17": {"peerStats": {"": {"status": "up", "remoteDisc": 3940685114, "lastDown": 1703657258.652725, "l3intf": ""}}}, }, "ipv6Neighbors": {}, } } }, - { - "utcTime": 1703667348.111288, - }, + {"utcTime": 1703667348.111288}, ], "inputs": {"down_threshold": 4}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - Session failure detected within the expected uptime threshold (3 hours ago)", "Peer: 192.0.255.71 VRF: default - Session failure detected within the expected uptime threshold (3 hours ago)", @@ -638,42 +349,18 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBFDPeersRegProtocols, + (VerifyBFDPeersRegProtocols, "success"): { "eos_data": [ { "vrfs": { "default": { "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - "peerStatsDetail": { - "role": "active", - "apps": ["ospf"], - }, - } - } - } + "192.0.255.7": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132, "peerStatsDetail": {"role": "active", "apps": ["ospf"]}}}} } }, "MGMT": { "ipv4Neighbors": { - "192.0.255.70": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 108328132, - "peerStatsDetail": { - "role": "active", - "apps": ["bgp"], - }, - } - } - } + "192.0.255.70": {"peerStats": {"": {"status": "up", "remoteDisc": 108328132, "peerStatsDetail": {"role": "active", "apps": ["bgp"]}}}} } }, } @@ -685,43 +372,16 @@ DATA: list[dict[str, Any]] = [ {"peer_address": "192.0.255.70", "vrf": "MGMT", "protocols": ["bgp"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyBFDPeersRegProtocols, + (VerifyBFDPeersRegProtocols, "failure"): { "eos_data": [ { "vrfs": { - "default": { - "ipv4Neighbors": { - "192.0.255.7": { - "peerStats": { - "": { - "status": "up", - "peerStatsDetail": { - "role": "active", - "apps": ["ospf"], - }, - } - } - } - } - }, + "default": {"ipv4Neighbors": {"192.0.255.7": {"peerStats": {"": {"status": "up", "peerStatsDetail": {"role": "active", "apps": ["ospf"]}}}}}}, "MGMT": { "ipv4Neighbors": { - "192.0.255.70": { - "peerStats": { - "": { - "status": "up", - "remoteDisc": 0, - "peerStatsDetail": { - "role": "active", - "apps": ["bgp"], - }, - } - } - } + "192.0.255.70": {"peerStats": {"": {"status": "up", "remoteDisc": 0, "peerStatsDetail": {"role": "active", "apps": ["bgp"]}}}} } }, } @@ -734,36 +394,21 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 192.0.255.7 VRF: default - `isis` routing protocol(s) not configured", "Peer: 192.0.255.70 VRF: MGMT - `isis`, `ospf` routing protocol(s) not configured", ], }, }, - { - "name": "failure-not-found", - "test": VerifyBFDPeersRegProtocols, - "eos_data": [ - { - "vrfs": { - "default": {}, - "MGMT": {}, - } - } - ], + (VerifyBFDPeersRegProtocols, "failure-not-found"): { + "eos_data": [{"vrfs": {"default": {}, "MGMT": {}}}], "inputs": { "bfd_peers": [ {"peer_address": "192.0.255.7", "vrf": "default", "protocols": ["isis"]}, {"peer_address": "192.0.255.70", "vrf": "MGMT", "protocols": ["isis"]}, ] }, - "expected": { - "result": "failure", - "messages": [ - "Peer: 192.0.255.7 VRF: default - Not found", - "Peer: 192.0.255.70 VRF: MGMT - Not found", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 192.0.255.7 VRF: default - Not found", "Peer: 192.0.255.70 VRF: MGMT - Not found"]}, }, -] +} diff --git a/tests/units/anta_tests/test_configuration.py b/tests/units/anta_tests/test_configuration.py index fd48337..62bd345 100644 --- a/tests/units/anta_tests/test_configuration.py +++ b/tests/units/anta_tests/test_configuration.py @@ -5,59 +5,34 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.configuration import VerifyRunningConfigDiffs, VerifyRunningConfigLines, VerifyZeroTouch from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyZeroTouch, - "eos_data": [{"mode": "disabled"}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyZeroTouch, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyZeroTouch, "success"): {"eos_data": [{"mode": "disabled"}], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyZeroTouch, "failure"): { "eos_data": [{"mode": "enabled"}], - "inputs": None, - "expected": {"result": "failure", "messages": ["ZTP is NOT disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["ZTP is NOT disabled"]}, }, - { - "name": "success", - "test": VerifyRunningConfigDiffs, - "eos_data": [""], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyRunningConfigDiffs, - "eos_data": ["blah blah"], - "inputs": None, - "expected": {"result": "failure", "messages": ["blah blah"]}, - }, - { - "name": "success", - "test": VerifyRunningConfigLines, - "eos_data": ["blah blah"], - "inputs": {"regex_patterns": ["blah"]}, - "expected": {"result": "success"}, - }, - { - "name": "success", - "test": VerifyRunningConfigLines, + (VerifyRunningConfigDiffs, "success"): {"eos_data": [""], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyRunningConfigDiffs, "failure"): {"eos_data": ["blah blah"], "expected": {"result": AntaTestStatus.FAILURE, "messages": ["blah blah"]}}, + (VerifyRunningConfigLines, "success"): {"eos_data": ["blah blah"], "inputs": {"regex_patterns": ["blah"]}, "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyRunningConfigLines, "success-patterns"): { "eos_data": ["enable password something\nsome other line"], "inputs": {"regex_patterns": ["^enable password .*$", "^.*other line$"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyRunningConfigLines, + (VerifyRunningConfigLines, "failure"): { "eos_data": ["enable password something\nsome other line"], "inputs": {"regex_patterns": ["bla", "bleh"]}, - "expected": {"result": "failure", "messages": ["Following patterns were not found: 'bla', 'bleh'"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Following patterns were not found: 'bla', 'bleh"]}, }, -] +} diff --git a/tests/units/anta_tests/test_connectivity.py b/tests/units/anta_tests/test_connectivity.py index 7fb9a11..85a1912 100644 --- a/tests/units/anta_tests/test_connectivity.py +++ b/tests/units/anta_tests/test_connectivity.py @@ -5,313 +5,244 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.connectivity import VerifyLLDPNeighbors, VerifyReachability from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success-ip", - "test": VerifyReachability, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyReachability, "success-ip"): { "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "10.0.0.5"}, {"destination": "10.0.0.2", "source": "10.0.0.5"}]}, "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.1 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.1 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, { "messages": [ - """PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.2 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, ], - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-expected-unreachable", - "test": VerifyReachability, + (VerifyReachability, "success-expected-unreachable"): { "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data. - - --- 10.0.0.1 ping statistics --- - 2 packets transmitted, 0 received, 100% packet loss, time 10ms - """, - ], - }, + "PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data.\n\n --- 10.0.0.1 ping statistics ---\n" + " 2 packets transmitted, 0 received, 100% packet loss, time 10ms\n " + ] + } ], "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "10.0.0.5", "reachable": False}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ipv6", - "test": VerifyReachability, + (VerifyReachability, "success-ipv6"): { "eos_data": [ { "messages": [ - """PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) from fd12:3456:789a:1::1 : 52 data bytes - 60 bytes from fd12:3456:789a:1::2: icmp_seq=1 ttl=64 time=0.097 ms - 60 bytes from fd12:3456:789a:1::2: icmp_seq=2 ttl=64 time=0.033 ms - - --- fd12:3456:789a:1::2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.033/0.065/0.097/0.032 ms, ipg/ewma 0.148/0.089 ms - """, - ], - }, + "PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) from fd12:3456:789a:1::1 : 52 data bytes\n 60 bytes from fd12:3456:789a:1::2:" + " icmp_seq=1 ttl=64 time=0.097 ms\n 60 bytes from fd12:3456:789a:1::2: icmp_seq=2 ttl=64 time=0.033 ms\n\n" + " --- fd12:3456:789a:1::2 ping statistics ---\n 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n" + " rtt min/avg/max/mdev = 0.033/0.065/0.097/0.032 ms, ipg/ewma 0.148/0.089 ms\n " + ] + } ], "inputs": {"hosts": [{"destination": "fd12:3456:789a:1::2", "source": "fd12:3456:789a:1::1"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ipv6-vlan", - "test": VerifyReachability, + (VerifyReachability, "success-ipv6-vlan"): { "eos_data": [ { "messages": [ - """PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) 52 data bytes - 60 bytes from fd12:3456:789a:1::2: icmp_seq=1 ttl=64 time=0.094 ms - 60 bytes from fd12:3456:789a:1::2: icmp_seq=2 ttl=64 time=0.027 ms - - --- fd12:3456:789a:1::2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.027/0.060/0.094/0.033 ms, ipg/ewma 0.152/0.085 ms - """, - ], - }, + "PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) 52 data bytes\n 60 bytes from fd12:3456:789a:1::2: " + "icmp_seq=1 ttl=64 time=0.094 ms\n 60 bytes from fd12:3456:789a:1::2: icmp_seq=2 ttl=64 time=0.027 ms\n\n" + " --- fd12:3456:789a:1::2 ping statistics ---\n 2 packets transmitted, 2 received, 0% packet loss," + " time 0ms\n rtt min/avg/max/mdev = 0.027/0.060/0.094/0.033 ms, ipg/ewma 0.152/0.085 ms\n " + ] + } ], "inputs": {"hosts": [{"destination": "fd12:3456:789a:1::2", "source": "vl110"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-interface", - "test": VerifyReachability, + (VerifyReachability, "success-interface"): { "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "Management0"}, {"destination": "10.0.0.2", "source": "Management0"}]}, "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.1 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.1 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, { "messages": [ - """PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.2 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, ], - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-repeat", - "test": VerifyReachability, + (VerifyReachability, "success-repeat"): { "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "Management0", "repeat": 1}]}, "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms - - --- 10.0.0.1 ping statistics --- - 1 packets transmitted, 1 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], - }, + "PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms\n\n" + " --- 10.0.0.1 ping statistics ---\n 1 packets transmitted, 1 received, 0% packet loss, time 0ms\n" + " rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms\n\n " + ] + } ], - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-df-bit-size", - "test": VerifyReachability, + (VerifyReachability, "success-df-bit-size"): { "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "Management0", "repeat": 5, "size": 1500, "df_bit": True}]}, "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 172.20.20.6 : 1472(1500) bytes of data. - 1480 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.085 ms - 1480 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.020 ms - 1480 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.019 ms - 1480 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=0.018 ms - 1480 bytes from 10.0.0.1: icmp_seq=5 ttl=64 time=0.017 ms - - --- 10.0.0.1 ping statistics --- - 5 packets transmitted, 5 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.017/0.031/0.085/0.026 ms, ipg/ewma 0.061/0.057 ms""", - ], - }, + "PING 10.0.0.1 (10.0.0.1) from 172.20.20.6 : 1472(1500) bytes of data.\n 1480 bytes from 10.0.0.1: " + "icmp_seq=1 ttl=64 time=0.085 ms\n 1480 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.020 ms\n" + " 1480 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.019 ms\n 1480 bytes from 10.0.0.1:" + " icmp_seq=4 ttl=64 time=0.018 ms\n 1480 bytes from 10.0.0.1:" + " icmp_seq=5 ttl=64 time=0.017 ms\n\n --- 10.0.0.1 ping statistics ---\n 5 packets transmitted," + " 5 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.017/0.031/0.085/0.026 ms, ipg/ewma 0.061/0.057 ms" + ] + } ], - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-ip", - "test": VerifyReachability, + (VerifyReachability, "success-without-source"): { + "inputs": {"hosts": [{"destination": "10.0.0.1", "repeat": 1}]}, + "eos_data": [ + { + "messages": [ + "PING 10.0.0.1 (10.0.0.1) : 72(100) bytes of data.\n 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms\n\n" + " --- 10.0.0.1 ping statistics ---\n 1 packets transmitted, 1 received, 0% packet loss, time 0ms\n" + " rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms\n\n " + ] + } + ], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyReachability, "failure-ip"): { "inputs": {"hosts": [{"destination": "10.0.0.11", "source": "10.0.0.5"}, {"destination": "10.0.0.2", "source": "10.0.0.5"}]}, "eos_data": [ { "messages": [ - """ping: sendmsg: Network is unreachable - ping: sendmsg: Network is unreachable - PING 10.0.0.11 (10.0.0.11) from 10.0.0.5 : 72(100) bytes of data. - - --- 10.0.0.11 ping statistics --- - 2 packets transmitted, 0 received, 100% packet loss, time 10ms - - - """, - ], + "ping: sendmsg: Network is unreachable\n ping: sendmsg: Network is unreachable\n " + "PING 10.0.0.11 (10.0.0.11) from 10.0.0.5 : 72(100) bytes of data.\n\n --- 10.0.0.11 ping statistics ---\n" + " 2 packets transmitted, 0 received, 100% packet loss, time 10ms\n\n\n " + ] }, { "messages": [ - """PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.2 ping statistics ---\n " + "2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, ], - "expected": {"result": "failure", "messages": ["Host: 10.0.0.11 Source: 10.0.0.5 VRF: default - Unreachable"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: 10.0.0.11 Source: 10.0.0.5 VRF: default - Unreachable"]}, }, - { - "name": "failure-ipv6", - "test": VerifyReachability, + (VerifyReachability, "failure-ipv6"): { "eos_data": [ { "messages": [ - """PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) from fd12:3456:789a:1::1 : 52 data bytes - - --- fd12:3456:789a:1::3 ping statistics --- - 2 packets transmitted, 0 received, 100% packet loss, time 10ms - """, - ], - }, + "PING fd12:3456:789a:1::2(fd12:3456:789a:1::2) from fd12:3456:789a:1::1 : 52 data bytes\n\n --- fd12:3456:789a:1::3 " + "ping statistics ---\n 2 packets transmitted, 0 received, 100% packet loss, time 10ms\n " + ] + } ], "inputs": {"hosts": [{"destination": "fd12:3456:789a:1::2", "source": "fd12:3456:789a:1::1"}]}, - "expected": {"result": "failure", "messages": ["Host: fd12:3456:789a:1::2 Source: fd12:3456:789a:1::1 VRF: default - Unreachable"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: fd12:3456:789a:1::2 Source: fd12:3456:789a:1::1 VRF: default - Unreachable"]}, }, - { - "name": "failure-interface", - "test": VerifyReachability, + (VerifyReachability, "failure-interface"): { "inputs": {"hosts": [{"destination": "10.0.0.11", "source": "Management0"}, {"destination": "10.0.0.2", "source": "Management0"}]}, "eos_data": [ { "messages": [ - """ping: sendmsg: Network is unreachable - ping: sendmsg: Network is unreachable - PING 10.0.0.11 (10.0.0.11) from 10.0.0.5 : 72(100) bytes of data. - - --- 10.0.0.11 ping statistics --- - 2 packets transmitted, 0 received, 100% packet loss, time 10ms - - - """, - ], + "ping: sendmsg: Network is unreachable\n ping: sendmsg: Network is unreachable\n " + "PING 10.0.0.11 (10.0.0.11) from 10.0.0.5 : 72(100) bytes of data.\n\n --- 10.0.0.11 ping statistics ---\n" + " 2 packets transmitted, 0 received, 100% packet loss, time 10ms\n\n\n " + ] }, { "messages": [ - """PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.2 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], + "PING 10.0.0.2 (10.0.0.2) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.2 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] }, ], - "expected": {"result": "failure", "messages": ["Host: 10.0.0.11 Source: Management0 VRF: default - Unreachable"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: 10.0.0.11 Source: Management0 VRF: default - Unreachable"]}, }, - { - "name": "failure-size", - "test": VerifyReachability, + (VerifyReachability, "failure-size"): { "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "Management0", "repeat": 5, "size": 1501, "df_bit": True}]}, "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 172.20.20.6 : 1473(1501) bytes of data. - ping: local error: message too long, mtu=1500 - ping: local error: message too long, mtu=1500 - ping: local error: message too long, mtu=1500 - ping: local error: message too long, mtu=1500 - ping: local error: message too long, mtu=1500 - - --- 10.0.0.1 ping statistics --- - 5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 40ms - """, - ], - }, + "PING 10.0.0.1 (10.0.0.1) from 172.20.20.6 : 1473(1501) bytes of data.\n ping: local error: message too long, mtu=1500\n" + " ping: local error: message too long, mtu=1500\n" + " ping: local error: message too long, mtu=1500\n ping: local error: message too long, mtu=1500\n" + " ping: local error: message too long, mtu=1500\n\n --- 10.0.0.1 ping statistics ---\n" + " 5 packets transmitted, 0 received, +5 errors, 100% packet loss, time 40ms\n " + ] + } ], - "expected": {"result": "failure", "messages": ["Host: 10.0.0.1 Source: Management0 VRF: default - Unreachable"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: 10.0.0.1 Source: Management0 VRF: default - Unreachable"]}, }, - { - "name": "failure-expected-unreachable", - "test": VerifyReachability, + (VerifyReachability, "failure-expected-unreachable"): { "eos_data": [ { "messages": [ - """PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data. - 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms - 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms - - --- 10.0.0.1 ping statistics --- - 2 packets transmitted, 2 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms, ipg/ewma 0.370/0.225 ms - - """, - ], - }, + "PING 10.0.0.1 (10.0.0.1) from 10.0.0.5 : 72(100) bytes of data.\n 80 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.247 ms\n" + " 80 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms\n\n --- 10.0.0.1 ping statistics ---\n" + " 2 packets transmitted, 2 received, 0% packet loss, time 0ms\n rtt min/avg/max/mdev = 0.072/0.159/0.247/0.088 ms," + " ipg/ewma 0.370/0.225 ms\n\n " + ] + } ], "inputs": {"hosts": [{"destination": "10.0.0.1", "source": "10.0.0.5", "reachable": False}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Host: 10.0.0.1 Source: 10.0.0.5 VRF: default - Destination is expected to be unreachable but found reachable"], }, }, - { - "name": "success", - "test": VerifyLLDPNeighbors, + (VerifyReachability, "failure-without-source"): { + "inputs": {"hosts": [{"destination": "10.0.0.1", "repeat": 1}]}, + "eos_data": [ + { + "messages": [ + "ping: sendmsg: Network is unreachable\n PING 10.0.0.1 (10.0.0.1) : 72(100) bytes of data.\n\n" + " --- 10.0.0.11 ping statistics ---\n " + "2 packets transmitted, 0 received, 100% packet loss, time 10ms\n\n " + ] + } + ], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: 10.0.0.1 VRF: default - Unreachable"]}, + }, + (VerifyLLDPNeighbors, "success"): { "eos_data": [ { "lldpNeighbors": { @@ -323,12 +254,12 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, - }, - ], + } + ] }, "Ethernet2": { "lldpNeighborInfo": [ @@ -338,27 +269,25 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE2", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2", }, - }, - ], + } + ] }, - }, - }, + } + } ], "inputs": { "neighbors": [ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"}, {"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, - ], + ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiple-neighbors", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "success-multiple-neighbors"): { "eos_data": [ { "lldpNeighbors": { @@ -370,7 +299,7 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, @@ -381,26 +310,20 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE2", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2", }, }, - ], - }, - }, - }, + ] + } + } + } ], - "inputs": { - "neighbors": [ - {"port": "Ethernet1", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, - ], - }, - "expected": {"result": "success"}, + "inputs": {"neighbors": [{"port": "Ethernet1", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-port-not-configured", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "failure-port-not-configured"): { "eos_data": [ { "lldpNeighbors": { @@ -412,27 +335,25 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, - }, - ], - }, - }, - }, + } + ] + } + } + } ], "inputs": { "neighbors": [ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"}, {"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, - ], + ] }, - "expected": {"result": "failure", "messages": ["Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Port not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Port not found"]}, }, - { - "name": "failure-no-neighbor", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "failure-no-neighbor"): { "eos_data": [ { "lldpNeighbors": { @@ -444,28 +365,26 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, - }, - ], + } + ] }, "Ethernet2": {"lldpNeighborInfo": []}, - }, - }, + } + } ], "inputs": { "neighbors": [ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"}, {"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, - ], + ] }, - "expected": {"result": "failure", "messages": ["Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - No LLDP neighbors"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - No LLDP neighbors"]}, }, - { - "name": "failure-wrong-neighbor", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "failure-wrong-neighbor"): { "eos_data": [ { "lldpNeighbors": { @@ -477,12 +396,12 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, - }, - ], + } + ] }, "Ethernet2": { "lldpNeighborInfo": [ @@ -492,30 +411,28 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE2", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet2"', + "interfaceId": "Ethernet2", "interfaceId_v2": "Ethernet2", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2", }, - }, - ], + } + ] }, - }, - }, + } + } ], "inputs": { "neighbors": [ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"}, {"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, - ], + ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: DC1-SPINE2/Ethernet2"], }, }, - { - "name": "failure-multiple", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "failure-multiple"): { "eos_data": [ { "lldpNeighbors": { @@ -527,26 +444,26 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet2"', + "interfaceId": "Ethernet2", "interfaceId_v2": "Ethernet2", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, - }, - ], + } + ] }, "Ethernet2": {"lldpNeighborInfo": []}, - }, - }, + } + } ], "inputs": { "neighbors": [ {"port": "Ethernet1", "neighbor_device": "DC1-SPINE1", "neighbor_port": "Ethernet1"}, {"port": "Ethernet2", "neighbor_device": "DC1-SPINE2", "neighbor_port": "Ethernet1"}, {"port": "Ethernet3", "neighbor_device": "DC1-SPINE3", "neighbor_port": "Ethernet1"}, - ], + ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: DC1-SPINE1/Ethernet2", "Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - No LLDP neighbors", @@ -554,9 +471,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-multiple-neighbors", - "test": VerifyLLDPNeighbors, + (VerifyLLDPNeighbors, "failure-multiple-neighbors"): { "eos_data": [ { "lldpNeighbors": { @@ -568,7 +483,7 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE1", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet1", }, @@ -579,24 +494,20 @@ DATA: list[dict[str, Any]] = [ "systemName": "DC1-SPINE2", "neighborInterfaceInfo": { "interfaceIdType": "interfaceName", - "interfaceId": '"Ethernet1"', + "interfaceId": "Ethernet1", "interfaceId_v2": "Ethernet1", "interfaceDescription": "P2P_LINK_TO_DC1-LEAF1A_Ethernet2", }, }, - ], - }, - }, - }, + ] + } + } + } ], - "inputs": { - "neighbors": [ - {"port": "Ethernet1", "neighbor_device": "DC1-SPINE3", "neighbor_port": "Ethernet1"}, - ], - }, + "inputs": {"neighbors": [{"port": "Ethernet1", "neighbor_device": "DC1-SPINE3", "neighbor_port": "Ethernet1"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Port: Ethernet1 Neighbor: DC1-SPINE3 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: DC1-SPINE1/Ethernet1, DC1-SPINE2/Ethernet1"], }, }, -] +} diff --git a/tests/units/anta_tests/test_cvx.py b/tests/units/anta_tests/test_cvx.py index be62293..7e893a4 100644 --- a/tests/units/anta_tests/test_cvx.py +++ b/tests/units/anta_tests/test_cvx.py @@ -5,182 +5,127 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.cvx import VerifyActiveCVXConnections, VerifyCVXClusterStatus, VerifyManagementCVX, VerifyMcsClientMounts, VerifyMcsServerMounts from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyMcsClientMounts, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyMcsClientMounts, "success"): { "eos_data": [{"mountStates": [{"path": "mcs/v1/toSwitch/28-99-3a-8f-93-7b", "type": "Mcs::DeviceConfigV1", "state": "mountStateMountComplete"}]}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-haclient", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "success-haclient"): { "eos_data": [ { "mountStates": [ {"path": "mcs/v1/apiCfgRedState", "type": "Mcs::ApiConfigRedundancyState", "state": "mountStateMountComplete"}, {"path": "mcs/v1/toSwitch/00-1c-73-74-c0-8b", "type": "Mcs::DeviceConfigV1", "state": "mountStateMountComplete"}, ] - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-partial-non-mcs", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "success-partial-non-mcs"): { "eos_data": [ { "mountStates": [ {"path": "blah/blah/blah", "type": "blah::blah", "state": "mountStatePreservedUnmounted"}, {"path": "mcs/v1/toSwitch/00-1c-73-74-c0-8b", "type": "Mcs::DeviceConfigV1", "state": "mountStateMountComplete"}, ] - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-nomounts", - "test": VerifyMcsClientMounts, - "eos_data": [ - {"mountStates": []}, - ], - "inputs": None, - "expected": {"result": "failure", "messages": ["MCS Client mount states are not present"]}, + (VerifyMcsClientMounts, "failure-nomounts"): { + "eos_data": [{"mountStates": []}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not present"]}, }, - { - "name": "failure-mountStatePreservedUnmounted", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "failure-mountStatePreservedUnmounted"): { "eos_data": [{"mountStates": [{"path": "mcs/v1/toSwitch/28-99-3a-8f-93-7b", "type": "Mcs::DeviceConfigV1", "state": "mountStatePreservedUnmounted"}]}], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not valid - Expected: mountStateMountComplete Actual: mountStatePreservedUnmounted"], }, }, - { - "name": "failure-partial-haclient", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "failure-partial-haclient"): { "eos_data": [ { "mountStates": [ {"path": "mcs/v1/apiCfgRedState", "type": "Mcs::ApiConfigRedundancyState", "state": "mountStateMountComplete"}, {"path": "mcs/v1/toSwitch/00-1c-73-74-c0-8b", "type": "Mcs::DeviceConfigV1", "state": "mountStatePreservedUnmounted"}, ] - }, + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not valid - Expected: mountStateMountComplete Actual: mountStatePreservedUnmounted"], }, }, - { - "name": "failure-full-haclient", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "failure-full-haclient"): { "eos_data": [ { "mountStates": [ {"path": "blah/blah/blah", "type": "blah::blahState", "state": "mountStatePreservedUnmounted"}, {"path": "mcs/v1/toSwitch/00-1c-73-74-c0-8b", "type": "Mcs::DeviceConfigV1", "state": "mountStatePreservedUnmounted"}, ] - }, + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not valid - Expected: mountStateMountComplete Actual: mountStatePreservedUnmounted"], }, }, - { - "name": "failure-non-mcs-client", - "test": VerifyMcsClientMounts, - "eos_data": [ - {"mountStates": [{"path": "blah/blah/blah", "type": "blah::blahState", "state": "mountStatePreservedUnmounted"}]}, - ], - "inputs": None, - "expected": {"result": "failure", "messages": ["MCS Client mount states are not present"]}, + (VerifyMcsClientMounts, "failure-non-mcs-client"): { + "eos_data": [{"mountStates": [{"path": "blah/blah/blah", "type": "blah::blahState", "state": "mountStatePreservedUnmounted"}]}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not present"]}, }, - { - "name": "failure-partial-mcs-client", - "test": VerifyMcsClientMounts, + (VerifyMcsClientMounts, "failure-partial-mcs-client"): { "eos_data": [ { "mountStates": [ {"path": "blah/blah/blah", "type": "blah::blahState", "state": "mountStatePreservedUnmounted"}, {"path": "blah/blah/blah", "type": "Mcs::DeviceConfigV1", "state": "mountStatePreservedUnmounted"}, ] - }, + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["MCS Client mount states are not valid - Expected: mountStateMountComplete Actual: mountStatePreservedUnmounted"], }, }, - { - "name": "success-enabled", - "test": VerifyManagementCVX, - "eos_data": [ - { - "clusterStatus": { - "enabled": True, - } - } - ], + (VerifyManagementCVX, "success-enabled"): { + "eos_data": [{"clusterStatus": {"enabled": True}}], "inputs": {"enabled": True}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-disabled", - "test": VerifyManagementCVX, - "eos_data": [ - { - "clusterStatus": { - "enabled": False, - } - } - ], + (VerifyManagementCVX, "success-disabled"): { + "eos_data": [{"clusterStatus": {"enabled": False}}], "inputs": {"enabled": False}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-invalid-state", - "test": VerifyManagementCVX, - "eos_data": [ - { - "clusterStatus": { - "enabled": False, - } - } - ], + (VerifyManagementCVX, "failure-invalid-state"): { + "eos_data": [{"clusterStatus": {"enabled": False}}], "inputs": {"enabled": True}, - "expected": {"result": "failure", "messages": ["Management CVX status is not valid: Expected: enabled Actual: disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Management CVX status is not valid: Expected: enabled Actual: disabled"]}, }, - { - "name": "failure-no-enabled state", - "test": VerifyManagementCVX, + (VerifyManagementCVX, "failure-no-enabled state"): { "eos_data": [{"clusterStatus": {}}], "inputs": {"enabled": False}, - "expected": {"result": "failure", "messages": ["Management CVX status - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Management CVX status - Not configured"]}, }, - { - "name": "failure - no clusterStatus", - "test": VerifyManagementCVX, + (VerifyManagementCVX, "failure - no clusterStatus"): { "eos_data": [{}], "inputs": {"enabled": False}, - "expected": {"result": "failure", "messages": ["Management CVX status - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Management CVX status - Not configured"]}, }, - { - "name": "success", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "success"): { "eos_data": [ { "connections": [ @@ -205,21 +150,17 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"connections_count": 1}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-mounts", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-no-mounts"): { "eos_data": [{"connections": [{"hostname": "media-leaf-1", "mounts": []}]}], "inputs": {"connections_count": 1}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Host: media-leaf-1 - No mount status found", "Incorrect CVX successful connections count - Expected: 1 Actual: 0"], }, }, - { - "name": "failure-unexpected-number-paths", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-unexpected-number-paths"): { "eos_data": [ { "connections": [ @@ -244,17 +185,15 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"connections_count": 1}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Host: media-leaf-1 - Incorrect number of mount path states - Expected: 3 Actual: 2", - "Host: media-leaf-1 - Unexpected MCS path type - Expected: Mcs::ApiConfigRedundancyStatus, Mcs::ActiveFlows, " - "Mcs::Client::Status Actual: Mcs::ApiStatus", + "Host: media-leaf-1 - Unexpected MCS path type - Expected: Mcs::ApiConfigRedundancyStatus, " + "Mcs::ActiveFlows, Mcs::Client::Status Actual: Mcs::ApiStatus", ], }, }, - { - "name": "failure-unexpected-path-type", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-unexpected-path-type"): { "eos_data": [ { "connections": [ @@ -280,16 +219,14 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"connections_count": 1}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Host: media-leaf-1 - Unexpected MCS path type - Expected: Mcs::ApiConfigRedundancyStatus, Mcs::ActiveFlows, Mcs::Client::Status" - " Actual: Mcs::ApiStatus" + "Host: media-leaf-1 - Unexpected MCS path type - Expected: Mcs::ApiConfigRedundancyStatus, Mcs::ActiveFlows, " + "Mcs::Client::Status Actual: Mcs::ApiStatus" ], }, }, - { - "name": "failure-invalid-mount-state", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-invalid-mount-state"): { "eos_data": [ { "connections": [ @@ -315,16 +252,14 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"connections_count": 1}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Host: media-leaf-1 Path Type: Mcs::ApiConfigRedundancyStatus - MCS server mount state is not valid - Expected: mountStateMountComplete" - " Actual:mountStateMountFailed" + "Host: media-leaf-1 Path Type: Mcs::ApiConfigRedundancyStatus - MCS server mount state is not valid - " + "Expected: mountStateMountComplete Actual:mountStateMountFailed" ], }, }, - { - "name": "failure-no-mcs-mount", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-no-mcs-mount"): { "eos_data": [ { "connections": [ @@ -341,69 +276,46 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"connections_count": 1}, - "expected": {"result": "failure", "messages": ["MCS mount state not detected", "Incorrect CVX successful connections count - Expected: 1 Actual: 0"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["MCS mount state not detected", "Incorrect CVX successful connections count - Expected: 1 Actual: 0"], + }, }, - { - "name": "failure-connections", - "test": VerifyMcsServerMounts, + (VerifyMcsServerMounts, "failure-connections"): { "eos_data": [{}], "inputs": {"connections_count": 1}, - "expected": {"result": "failure", "messages": ["CVX connections are not available"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX connections are not available"]}, }, - { - "name": "success", - "test": VerifyActiveCVXConnections, + (VerifyActiveCVXConnections, "success"): { "eos_data": [ { "connections": [ - { - "switchId": "fc:bd:67:c3:16:55", - "hostname": "lyv563", - "oobConnectionActive": True, - }, - { - "switchId": "00:1c:73:3c:e3:9e", - "hostname": "tg264", - "oobConnectionActive": True, - }, + {"switchId": "fc:bd:67:c3:16:55", "hostname": "lyv563", "oobConnectionActive": True}, + {"switchId": "00:1c:73:3c:e3:9e", "hostname": "tg264", "oobConnectionActive": True}, ] } ], "inputs": {"connections_count": 2}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyActiveCVXConnections, + (VerifyActiveCVXConnections, "failure"): { "eos_data": [ { "connections": [ - { - "switchId": "fc:bd:67:c3:16:55", - "hostname": "lyv563", - "oobConnectionActive": False, - }, - { - "switchId": "00:1c:73:3c:e3:9e", - "hostname": "tg264", - "oobConnectionActive": True, - }, + {"switchId": "fc:bd:67:c3:16:55", "hostname": "lyv563", "oobConnectionActive": False}, + {"switchId": "00:1c:73:3c:e3:9e", "hostname": "tg264", "oobConnectionActive": True}, ] } ], "inputs": {"connections_count": 2}, - "expected": {"result": "failure", "messages": ["CVX active connections count - Expected: 2 Actual: 1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX active connections count - Expected: 2 Actual: 1"]}, }, - { - "name": "failure-no-connections", - "test": VerifyActiveCVXConnections, + (VerifyActiveCVXConnections, "failure-no-connections"): { "eos_data": [{}], "inputs": {"connections_count": 2}, - "expected": {"result": "failure", "messages": ["CVX connections are not available"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX connections are not available"]}, }, - { - "name": "success-all", - "test": VerifyCVXClusterStatus, + (VerifyCVXClusterStatus, "success-all"): { "eos_data": [ { "enabled": True, @@ -424,11 +336,9 @@ DATA: list[dict[str, Any]] = [ {"peer_name": "cvx-red-3", "registrationState": "Registration complete"}, ], }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-invalid-role", - "test": VerifyCVXClusterStatus, + (VerifyCVXClusterStatus, "failure-invalid-role"): { "eos_data": [ { "enabled": True, @@ -449,56 +359,24 @@ DATA: list[dict[str, Any]] = [ {"peer_name": "cvx-red-3", "registrationState": "Registration complete"}, ], }, - "expected": {"result": "failure", "messages": ["CVX Role is not valid: Expected: Master Actual: Standby"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX Role is not valid: Expected: Master Actual: Standby"]}, }, - { - "name": "failure-cvx-enabled", - "test": VerifyCVXClusterStatus, - "eos_data": [ - { - "enabled": False, - "clusterMode": True, - "clusterStatus": { - "role": "Master", - "peerStatus": {}, - }, - } - ], - "inputs": { - "role": "Master", - "peer_status": [], - }, - "expected": {"result": "failure", "messages": ["CVX Server status is not enabled"]}, + (VerifyCVXClusterStatus, "failure-cvx-enabled"): { + "eos_data": [{"enabled": False, "clusterMode": True, "clusterStatus": {"role": "Master", "peerStatus": {}}}], + "inputs": {"role": "Master", "peer_status": []}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX Server status is not enabled"]}, }, - { - "name": "failure-cluster-enabled", - "test": VerifyCVXClusterStatus, - "eos_data": [ - { - "enabled": True, - "clusterMode": False, - "clusterStatus": {}, - } - ], - "inputs": { - "role": "Master", - "peer_status": [], - }, - "expected": {"result": "failure", "messages": ["CVX Server is not a cluster"]}, + (VerifyCVXClusterStatus, "failure-cluster-enabled"): { + "eos_data": [{"enabled": True, "clusterMode": False, "clusterStatus": {}}], + "inputs": {"role": "Master", "peer_status": []}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["CVX Server is not a cluster"]}, }, - { - "name": "failure-missing-peers", - "test": VerifyCVXClusterStatus, + (VerifyCVXClusterStatus, "failure-missing-peers"): { "eos_data": [ { "enabled": True, "clusterMode": True, - "clusterStatus": { - "role": "Master", - "peerStatus": { - "cvx-red-2": {"peerName": "cvx-red-2", "registrationState": "Registration complete"}, - }, - }, + "clusterStatus": {"role": "Master", "peerStatus": {"cvx-red-2": {"peerName": "cvx-red-2", "registrationState": "Registration complete"}}}, } ], "inputs": { @@ -508,21 +386,10 @@ DATA: list[dict[str, Any]] = [ {"peer_name": "cvx-red-3", "registrationState": "Registration complete"}, ], }, - "expected": {"result": "failure", "messages": ["Unexpected number of peers - Expected: 2 Actual: 1", "cvx-red-3 - Not present"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Unexpected number of peers - Expected: 2 Actual: 1", "cvx-red-3 - Not present"]}, }, - { - "name": "failure-invalid-peers", - "test": VerifyCVXClusterStatus, - "eos_data": [ - { - "enabled": True, - "clusterMode": True, - "clusterStatus": { - "role": "Master", - "peerStatus": {}, - }, - } - ], + (VerifyCVXClusterStatus, "failure-invalid-peers"): { + "eos_data": [{"enabled": True, "clusterMode": True, "clusterStatus": {"role": "Master", "peerStatus": {}}}], "inputs": { "role": "Master", "peer_status": [ @@ -530,11 +397,12 @@ DATA: list[dict[str, Any]] = [ {"peer_name": "cvx-red-3", "registrationState": "Registration complete"}, ], }, - "expected": {"result": "failure", "messages": ["Unexpected number of peers - Expected: 2 Actual: 0", "cvx-red-2 - Not present", "cvx-red-3 - Not present"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Unexpected number of peers - Expected: 2 Actual: 0", "cvx-red-2 - Not present", "cvx-red-3 - Not present"], + }, }, - { - "name": "failure-registration-error", - "test": VerifyCVXClusterStatus, + (VerifyCVXClusterStatus, "failure-registration-error"): { "eos_data": [ { "enabled": True, @@ -555,6 +423,9 @@ DATA: list[dict[str, Any]] = [ {"peer_name": "cvx-red-3", "registrationState": "Registration complete"}, ], }, - "expected": {"result": "failure", "messages": ["cvx-red-2 - Invalid registration state - Expected: Registration complete Actual: Registration error"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["cvx-red-2 - Invalid registration state - Expected: Registration complete Actual: Registration error"], + }, }, -] +} diff --git a/tests/units/anta_tests/test_evpn.py b/tests/units/anta_tests/test_evpn.py new file mode 100644 index 0000000..3a8af74 --- /dev/null +++ b/tests/units/anta_tests/test_evpn.py @@ -0,0 +1,449 @@ +# 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. +"""Tests for anta.tests.evpn.py.""" + +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING, Any + +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus +from anta.tests.evpn import VerifyEVPNType5Routes +from tests.units.anta_tests import test + +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyEVPNType5Routes, "success-all"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": True, "valid": True}}], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": True, "valid": True}}], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + }, + ], + "inputs": { + "prefixes": [ + {"address": "10.100.0.128/31", "vni": 10}, + {"address": "10.100.0.128/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local"}]}, + {"address": "10.100.4.0/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3"}]}]}, + { + "address": "10.100.4.1/31", + "vni": 10, + "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3", "route_targets": ["10:10"]}]}], + }, + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "success-ipv6"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.21", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.21:500 ip-prefix fd00:dc:5::1/128": { + "totalPaths": 1, + "routeKeyDetail": {"ipGenPrefix": "fd00:dc:5::1/128", "domain": "local", "rd": "10.1.0.21:500", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "", + "asPathEntry": {"asPathType": "Local", "asPath": "i"}, + "reasonNotBestpath": "noReason", + "routeType": {"active": True, "valid": True}, + } + ], + }, + "RD: 10.1.0.21:500 ip-prefix fd00:dc:5::1/128 remote": { + "totalPaths": 1, + "routeKeyDetail": {"ipGenPrefix": "fd00:dc:5::1/128", "domain": "remote", "rd": "10.1.0.21:500", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "", + "asPathEntry": {"asPathType": "Local", "asPath": "i"}, + "reasonNotBestpath": "noReason", + "routeType": {"active": True, "valid": True}, + } + ], + }, + }, + } + ], + "inputs": {"prefixes": [{"address": "fd00:dc:5::1/128", "vni": 500}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "success-across-all-rds"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": True, "valid": True}}], + }, + "RD: 10.100.1.4:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.4:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": False, "valid": False}}], + }, + }, + } + ], + "inputs": {"prefixes": [{"address": "10.100.0.128/31", "vni": 10}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "success-specific-rd"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + {"nextHop": "10.100.2.3", "routeType": {"active": False, "valid": False}}, + {"nextHop": "10.100.2.4", "routeType": {"active": True, "valid": True}}, + ], + } + }, + } + ], + "inputs": {"prefixes": [{"address": "10.100.0.128/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local"}]}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "success-specific-nexthop"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + }, + { + "nextHop": "10.100.2.3", + "routeType": {"active": False, "valid": False}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + }, + ], + } + }, + } + ], + "inputs": { + "prefixes": [{"address": "10.100.4.0/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3"}]}]}] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "success-RTs"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + } + ], + "inputs": { + "prefixes": [ + { + "address": "10.100.4.1/31", + "vni": 10, + "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3", "route_targets": ["10:10"]}]}], + } + ] + }, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEVPNType5Routes, "failure-all"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": False, "valid": True}}], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [{"nextHop": "10.100.2.3", "routeType": {"active": False, "valid": True}}], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": False}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + }, + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": False, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + }, + ], + "inputs": { + "prefixes": [ + {"address": "10.100.0.128/31", "vni": 10}, + {"address": "10.100.0.128/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local"}]}, + {"address": "10.100.4.0/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3"}]}]}, + { + "address": "10.100.4.1/31", + "vni": 10, + "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3", "route_targets": ["10:10"]}]}], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Prefix: 10.100.0.128/31 VNI: 10 - No active and valid path found across all RDs", + "Prefix: 10.100.0.128/31 VNI: 10 RD: 10.100.1.3:10 - No active and valid path found", + "Prefix: 10.100.4.0/31 VNI: 10 RD: 10.100.1.3:10 Nexthop: 10.100.2.3 - No active and valid path found", + "Prefix: 10.100.4.1/31 VNI: 10 RD: 10.100.1.3:10 Nexthop: 10.100.2.3 RTs: 10:10 - No active and valid path found", + ], + }, + }, + (VerifyEVPNType5Routes, "failure-not-configured"): { + "eos_data": [ + {"vrf": "default", "routerId": "10.100.1.5", "asn": 65102, "evpnRoutes": {}}, + {"vrf": "default", "routerId": "10.100.1.5", "asn": 65102, "evpnRoutes": {}}, + ], + "inputs": { + "prefixes": [ + {"address": "10.100.0.128/31", "vni": 10}, + { + "address": "10.100.4.1/31", + "vni": 10, + "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3", "route_targets": ["10:10"]}]}], + }, + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Prefix: 10.100.0.128/31 VNI: 10 - No EVPN Type-5 routes found", "Prefix: 10.100.4.1/31 VNI: 10 - No EVPN Type-5 routes found"], + }, + }, + (VerifyEVPNType5Routes, "failure-route-not-found-with-specified-rd-domain"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.0.128/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.0.128/31", "domain": "remote", "rd": "10.100.1.4:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + {"nextHop": "10.100.2.3", "routeType": {"active": False, "valid": False}}, + {"nextHop": "10.100.2.4", "routeType": {"active": True, "valid": True}}, + ], + } + }, + } + ], + "inputs": {"prefixes": [{"address": "10.100.0.128/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "remote"}]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.100.0.128/31 VNI: 10 RD: 10.100.1.3:10 Domain: remote - Route not found"]}, + }, + (VerifyEVPNType5Routes, "failiure-specific-nexthop-path-not-found"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.4", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:10:10", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + } + ], + "inputs": { + "prefixes": [{"address": "10.100.4.0/31", "vni": 10, "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3"}]}]}] + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: 10.100.4.0/31 VNI: 10 RD: 10.100.1.3:10 Nexthop: 10.100.2.3 - Path not found"]}, + }, + (VerifyEVPNType5Routes, "failiure-specific-nexthop-RTs-path-not-found"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.100.1.5", + "asn": 65102, + "evpnRoutes": { + "RD: 10.100.1.3:10 ip-prefix 10.100.4.0/31": { + "routeKeyDetail": {"ipGenPrefix": "10.100.4.0/31", "domain": "local", "rd": "10.100.1.3:10", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "10.100.2.3", + "routeType": {"active": True, "valid": True}, + "routeDetail": {"extCommunities": ["Route-Target-AS:20:20", "TunnelEncap:tunnelTypeVxlan", "EvpnRouterMac:02:1c:73:71:73:45"]}, + } + ], + } + }, + } + ], + "inputs": { + "prefixes": [ + { + "address": "10.100.4.1/31", + "vni": 10, + "routes": [{"rd": "10.100.1.3:10", "domain": "local", "paths": [{"nexthop": "10.100.2.3", "route_targets": ["10:10"]}]}], + } + ] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Prefix: 10.100.4.1/31 VNI: 10 RD: 10.100.1.3:10 Nexthop: 10.100.2.3 RTs: 10:10 - Path not found"], + }, + }, + (VerifyEVPNType5Routes, "failure-ipv6"): { + "eos_data": [ + { + "vrf": "default", + "routerId": "10.1.0.21", + "asn": 65120, + "evpnRoutes": { + "RD: 10.1.0.21:500 ip-prefix fd00:dc:5::1/128": { + "totalPaths": 1, + "routeKeyDetail": {"ipGenPrefix": "fd00:dc:5::1/128", "domain": "local", "rd": "10.1.0.21:500", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "", + "asPathEntry": {"asPathType": "Local", "asPath": "i"}, + "reasonNotBestpath": "noReason", + "routeType": {"active": True, "valid": False}, + } + ], + }, + "RD: 10.1.0.21:500 ip-prefix fd00:dc:5::1/128 remote": { + "totalPaths": 1, + "routeKeyDetail": {"ipGenPrefix": "fd00:dc:5::1/128", "domain": "remote", "rd": "10.1.0.21:500", "nlriType": "ip-prefix"}, + "evpnRoutePaths": [ + { + "nextHop": "", + "asPathEntry": {"asPathType": "Local", "asPath": "i"}, + "reasonNotBestpath": "noReason", + "routeType": {"active": False, "valid": True}, + } + ], + }, + }, + } + ], + "inputs": {"prefixes": [{"address": "fd00:dc:5::1/128", "vni": 500}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Prefix: fd00:dc:5::1/128 VNI: 500 - No active and valid path found across all RDs"]}, + }, +} diff --git a/tests/units/anta_tests/test_field_notices.py b/tests/units/anta_tests/test_field_notices.py index f7c5fc6..0022f9d 100644 --- a/tests/units/anta_tests/test_field_notices.py +++ b/tests/units/anta_tests/test_field_notices.py @@ -5,15 +5,19 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.field_notices import VerifyFieldNotice44Resolution, VerifyFieldNotice72Resolution from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyFieldNotice44Resolution, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyFieldNotice44Resolution, "success"): { "eos_data": [ { "imageFormatVersion": "1.0", @@ -23,343 +27,200 @@ DATA: list[dict[str, Any]] = [ "deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}, {"name": "NotAboot", "version": "Aboot-veos-8.0.0-3255441"}], }, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-4.0", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "failure-4.0"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "DCS-7280QRA-C36S", - "details": { - "deviations": [], - "components": [{"name": "Aboot", "version": "Aboot-veos-4.0.1-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-4.0.1-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Device is running incorrect version of aboot 4.0.1"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is running incorrect version of aboot 4.0.1"]}, }, - { - "name": "failure-4.1", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "failure-4.1"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "DCS-7280QRA-C36S", - "details": { - "deviations": [], - "components": [{"name": "Aboot", "version": "Aboot-veos-4.1.0-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-4.1.0-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Device is running incorrect version of aboot 4.1.0"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is running incorrect version of aboot 4.1.0"]}, }, - { - "name": "failure-6.0", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "failure-6.0"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "DCS-7280QRA-C36S", - "details": { - "deviations": [], - "components": [{"name": "Aboot", "version": "Aboot-veos-6.0.1-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-6.0.1-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Device is running incorrect version of aboot 6.0.1"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is running incorrect version of aboot 6.0.1"]}, }, - { - "name": "failure-6.1", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "failure-6.1"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "DCS-7280QRA-C36S", - "details": { - "deviations": [], - "components": [{"name": "Aboot", "version": "Aboot-veos-6.1.1-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-6.1.1-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Device is running incorrect version of aboot 6.1.1"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is running incorrect version of aboot 6.1.1"]}, }, - { - "name": "skipped-model", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "skipped-model"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "vEOS-lab", - "details": { - "deviations": [], - "components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "skipped", - "messages": ["Device is not impacted by FN044"], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device is not impacted by FN044"]}, }, - { - "name": "failure-no-aboot-component", - "test": VerifyFieldNotice44Resolution, + (VerifyFieldNotice44Resolution, "failure-no-aboot-component"): { "eos_data": [ { "imageFormatVersion": "1.0", "uptime": 1109144.35, "modelName": "DCS-7280QRA-C36S", - "details": { - "deviations": [], - "components": [{"name": "NotAboot", "version": "Aboot-veos-4.0.1-3255441"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "NotAboot", "version": "Aboot-veos-4.0.1-3255441"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Aboot component not found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Aboot component not found"]}, }, - { - "name": "success-JPE", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "success-JPE"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JPE2130000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "7"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "7"}]}, + } ], - "inputs": None, - "expected": {"result": "success", "messages": ["FN72 is mitigated"]}, + "expected": {"result": AntaTestStatus.SUCCESS, "messages": ["FN72 is mitigated"]}, }, - { - "name": "success-JAS", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "success-JAS"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JAS2040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "7"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "7"}]}, + } ], - "inputs": None, - "expected": {"result": "success", "messages": ["FN72 is mitigated"]}, + "expected": {"result": AntaTestStatus.SUCCESS, "messages": ["FN72 is mitigated"]}, }, - { - "name": "success-K-JPE", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "success-K-JPE"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "JPE2133000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "7"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "7"}]}, + } ], - "inputs": None, - "expected": {"result": "success", "messages": ["FN72 is mitigated"]}, + "expected": {"result": AntaTestStatus.SUCCESS, "messages": ["FN72 is mitigated"]}, }, - { - "name": "success-K-JAS", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "success-K-JAS"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "JAS2040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "7"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "7"}]}, + } ], - "inputs": None, - "expected": {"result": "success", "messages": ["FN72 is mitigated"]}, + "expected": {"result": AntaTestStatus.SUCCESS, "messages": ["FN72 is mitigated"]}, }, - { - "name": "skipped-Serial", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-Serial"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "BAN2040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "7"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "7"}]}, + } ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Device not exposed"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device not exposed"]}, }, - { - "name": "skipped-Platform", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-Platform"): { "eos_data": [ - { - "modelName": "DCS-7150-52-CL", - "serialNumber": "JAS0040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + {"modelName": "DCS-7150-52-CL", "serialNumber": "JAS0040000", "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}} ], - "inputs": None, - "expected": { - "result": "skipped", - "messages": ["Platform is not impacted by FN072"], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Platform is not impacted by FN072"]}, }, - { - "name": "skipped-range-JPE", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-range-JPE"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JPE2131000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Device not exposed"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device not exposed"]}, }, - { - "name": "skipped-range-K-JPE", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-range-K-JPE"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "JPE2134000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Device not exposed"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device not exposed"]}, }, - { - "name": "skipped-range-JAS", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-range-JAS"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JAS2041000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Device not exposed"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device not exposed"]}, }, - { - "name": "skipped-range-K-JAS", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "skipped-range-K-JAS"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "JAS2041000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Device not exposed"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Device not exposed"]}, }, - { - "name": "failed-JPE", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "failed-JPE"): { "eos_data": [ { "modelName": "DCS-7280SR3K-48YC8", "serialNumber": "JPE2133000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device is exposed to FN72"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is exposed to FN72"]}, }, - { - "name": "failed-JAS", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "failed-JAS"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JAS2040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm1", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm1", "version": "5"}]}, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device is exposed to FN72"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device is exposed to FN72"]}, }, - { - "name": "error", - "test": VerifyFieldNotice72Resolution, + (VerifyFieldNotice72Resolution, "error"): { "eos_data": [ { "modelName": "DCS-7280SR3-48YC8", "serialNumber": "JAS2040000", - "details": { - "deviations": [], - "components": [{"name": "FixedSystemvrm2", "version": "5"}], - }, - }, + "details": {"deviations": [], "components": [{"name": "FixedSystemvrm2", "version": "5"}]}, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Error in running test - Component FixedSystemvrm1 not found in 'show version'"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Error in running test - Component FixedSystemvrm1 not found in 'show version"]}, }, -] +} diff --git a/tests/units/anta_tests/test_flow_tracking.py b/tests/units/anta_tests/test_flow_tracking.py index cdd2746..ccab2f6 100644 --- a/tests/units/anta_tests/test_flow_tracking.py +++ b/tests/units/anta_tests/test_flow_tracking.py @@ -5,15 +5,19 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.flow_tracking import VerifyHardwareFlowTrackerStatus from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyHardwareFlowTrackerStatus, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyHardwareFlowTrackerStatus, "success"): { "eos_data": [ { "trackers": { @@ -31,14 +35,12 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": {"trackers": [{"name": "FLOW-TRACKER"}, {"name": "HARDWARE-TRACKER"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-with-optional-field", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "success-with-optional-field"): { "eos_data": [ { "trackers": { @@ -56,7 +58,7 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": { "trackers": [ @@ -72,21 +74,14 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-flow-tracking-not-running", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-flow-tracking-not-running"): { "eos_data": [{"trackers": {}, "running": False}], "inputs": {"trackers": [{"name": "FLOW-TRACKER"}]}, - "expected": { - "result": "failure", - "messages": ["Hardware flow tracking is not running."], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Hardware flow tracking is not running"]}, }, - { - "name": "failure-tracker-not-configured", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-tracker-not-configured"): { "eos_data": [ { "trackers": { @@ -101,14 +96,9 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"trackers": [{"name": "FLOW-Sample"}]}, - "expected": { - "result": "failure", - "messages": ["Flow Tracker: FLOW-Sample - Not found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Flow Tracker: FLOW-Sample - Not found"]}, }, - { - "name": "failure-tracker-not-active", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-tracker-not-active"): { "eos_data": [ { "trackers": { @@ -126,7 +116,7 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": { "trackers": [ @@ -142,14 +132,9 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": { - "result": "failure", - "messages": ["Flow Tracker: FLOW-TRACKER - Disabled", "Flow Tracker: HARDWARE-TRACKER - Disabled"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Flow Tracker: FLOW-TRACKER - Disabled", "Flow Tracker: HARDWARE-TRACKER - Disabled"]}, }, - { - "name": "failure-incorrect-record-export", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-incorrect-record-export"): { "eos_data": [ { "trackers": { @@ -167,22 +152,16 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": { "trackers": [ - { - "name": "FLOW-TRACKER", - "record_export": {"on_inactive_timeout": 6000, "on_interval": 30000}, - }, - { - "name": "HARDWARE-TRACKER", - "record_export": {"on_inactive_timeout": 60000, "on_interval": 300000}, - }, + {"name": "FLOW-TRACKER", "record_export": {"on_inactive_timeout": 6000, "on_interval": 30000}}, + {"name": "HARDWARE-TRACKER", "record_export": {"on_inactive_timeout": 60000, "on_interval": 300000}}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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 - " @@ -190,9 +169,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-exporters", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-incorrect-exporters"): { "eos_data": [ { "trackers": { @@ -216,7 +193,7 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": { "trackers": [ @@ -237,7 +214,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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", @@ -247,9 +224,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-all-type", - "test": VerifyHardwareFlowTrackerStatus, + (VerifyHardwareFlowTrackerStatus, "failure-all-type"): { "eos_data": [ { "trackers": { @@ -291,7 +266,7 @@ DATA: list[dict[str, Any]] = [ }, }, "running": True, - }, + } ], "inputs": { "trackers": [ @@ -301,10 +276,7 @@ DATA: list[dict[str, Any]] = [ "record_export": {"on_inactive_timeout": 60000, "on_interval": 300000}, "exporters": [{"name": "CV-TELEMETRY", "local_interface": "Loopback0", "template_interval": 3600000}], }, - { - "name": "HARDWARE-FLOW", - "record_export": {"on_inactive_timeout": 60000, "on_interval": 300000}, - }, + {"name": "HARDWARE-FLOW", "record_export": {"on_inactive_timeout": 60000, "on_interval": 300000}}, { "name": "FLOW-TRACKER2", "exporters": [ @@ -322,7 +294,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Flow Tracker: FLOW-Sample - Not found", "Flow Tracker: FLOW-TRIGGER - Disabled", @@ -335,4 +307,4 @@ DATA: list[dict[str, Any]] = [ ], }, }, -] +} diff --git a/tests/units/anta_tests/test_greent.py b/tests/units/anta_tests/test_greent.py index 3afb240..8b51159 100644 --- a/tests/units/anta_tests/test_greent.py +++ b/tests/units/anta_tests/test_greent.py @@ -5,51 +5,45 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.greent import VerifyGreenT, VerifyGreenTCounters from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyGreenTCounters, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyGreenTCounters, "success"): { "eos_data": [{"sampleRcvd": 0, "sampleDiscarded": 0, "multiDstSampleRcvd": 0, "grePktSent": 1, "sampleSent": 0}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyGreenTCounters, + (VerifyGreenTCounters, "failure"): { "eos_data": [{"sampleRcvd": 0, "sampleDiscarded": 0, "multiDstSampleRcvd": 0, "grePktSent": 0, "sampleSent": 0}], - "inputs": None, - "expected": {"result": "failure", "messages": ["GreenT counters are not incremented"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["GreenT counters are not incremented"]}, }, - { - "name": "success", - "test": VerifyGreenT, + (VerifyGreenT, "success"): { "eos_data": [ { "profiles": { "default": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}}, "testProfile": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}}, - }, - }, + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyGreenT, + (VerifyGreenT, "failure"): { "eos_data": [ { "profiles": { - "default": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}}, - }, - }, + "default": {"interfaces": [], "appliedInterfaces": [], "samplePolicy": "default", "failures": {}, "appliedInterfaces6": [], "failures6": {}} + } + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["No GreenT policy is created"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No GreenT policy is created"]}, }, -] +} diff --git a/tests/units/anta_tests/test_hardware.py b/tests/units/anta_tests/test_hardware.py index 972b07f..7ab748d 100644 --- a/tests/units/anta_tests/test_hardware.py +++ b/tests/units/anta_tests/test_hardware.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.hardware import ( VerifyAdverseDrops, VerifyEnvironmentCooling, @@ -18,44 +21,41 @@ from anta.tests.hardware import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyTransceiversManufacturers, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyTransceiversManufacturers, "success"): { "eos_data": [ { "xcvrSlots": { "1": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501340", "hardwareRev": "21"}, "2": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501337", "hardwareRev": "21"}, - }, - }, + } + } ], "inputs": {"manufacturers": ["Arista Networks"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyTransceiversManufacturers, + (VerifyTransceiversManufacturers, "failure"): { "eos_data": [ { "xcvrSlots": { "1": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501340", "hardwareRev": "21"}, "2": {"mfgName": "Arista Networks", "modelName": "QSFP-100G-DR", "serialNum": "XKT203501337", "hardwareRev": "21"}, - }, - }, + } + } ], "inputs": {"manufacturers": ["Arista"]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: 1 - Transceiver is from unapproved manufacturers - Expected: Arista Actual: Arista Networks", "Interface: 2 - Transceiver is from unapproved manufacturers - Expected: Arista Actual: Arista Networks", ], }, }, - { - "name": "success", - "test": VerifyTemperature, + (VerifyTemperature, "success"): { "eos_data": [ { "powercycleOnOverheat": "False", @@ -64,14 +64,11 @@ DATA: list[dict[str, Any]] = [ "shutdownOnOverheat": "True", "systemStatus": "temperatureOk", "recoveryModeOnOverheat": "recoveryModeNA", - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyTemperature, + (VerifyTemperature, "failure"): { "eos_data": [ { "powercycleOnOverheat": "False", @@ -80,14 +77,14 @@ DATA: list[dict[str, Any]] = [ "shutdownOnOverheat": "True", "systemStatus": "temperatureCritical", "recoveryModeOnOverheat": "recoveryModeNA", - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device temperature exceeds acceptable limits - Expected: temperatureOk Actual: temperatureCritical"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Device temperature exceeds acceptable limits - Expected: temperatureOk Actual: temperatureCritical"], + }, }, - { - "name": "success", - "test": VerifyTransceiversTemperature, + (VerifyTransceiversTemperature, "success"): { "eos_data": [ { "tempSensors": [ @@ -107,17 +104,14 @@ DATA: list[dict[str, Any]] = [ "pidDriverCount": 0, "isPidDriver": False, "name": "DomTemperatureSensor54", - }, + } ], "cardSlots": [], - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-hwStatus", - "test": VerifyTransceiversTemperature, + (VerifyTransceiversTemperature, "failure-hwStatus"): { "eos_data": [ { "tempSensors": [ @@ -137,20 +131,14 @@ DATA: list[dict[str, Any]] = [ "pidDriverCount": 0, "isPidDriver": False, "name": "DomTemperatureSensor54", - }, + } ], "cardSlots": [], - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Sensor: DomTemperatureSensor54 - Invalid hardware state - Expected: ok Actual: ko"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Sensor: DomTemperatureSensor54 - Invalid hardware state - Expected: ok Actual: ko"]}, }, - { - "name": "failure-alertCount", - "test": VerifyTransceiversTemperature, + (VerifyTransceiversTemperature, "failure-alertCount"): { "eos_data": [ { "tempSensors": [ @@ -170,20 +158,14 @@ DATA: list[dict[str, Any]] = [ "pidDriverCount": 0, "isPidDriver": False, "name": "DomTemperatureSensor54", - }, + } ], "cardSlots": [], - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Sensor: DomTemperatureSensor54 - Incorrect alert counter - Expected: 0 Actual: 1"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Sensor: DomTemperatureSensor54 - Incorrect alert counter - Expected: 0 Actual: 1"]}, }, - { - "name": "success", - "test": VerifyEnvironmentSystemCooling, + (VerifyEnvironmentSystemCooling, "success"): { "eos_data": [ { "defaultZones": False, @@ -199,14 +181,11 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "coolingOk", - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyEnvironmentSystemCooling, + (VerifyEnvironmentSystemCooling, "failure"): { "eos_data": [ { "defaultZones": False, @@ -222,14 +201,11 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "coolingKo", - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device system cooling status invalid - Expected: coolingOk Actual: coolingKo"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device system cooling status invalid - Expected: coolingOk Actual: coolingKo"]}, }, - { - "name": "success", - "test": VerifyEnvironmentCooling, + (VerifyEnvironmentCooling, "success"): { "eos_data": [ { "defaultZones": False, @@ -253,7 +229,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply1/1", - }, + } ], "speed": 30, "label": "PowerSupply1", @@ -271,7 +247,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply2/1", - }, + } ], "speed": 30, "label": "PowerSupply2", @@ -291,7 +267,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "1/1", - }, + } ], "speed": 30, "label": "1", @@ -309,7 +285,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "2/1", - }, + } ], "speed": 30, "label": "2", @@ -327,7 +303,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "3/1", - }, + } ], "speed": 30, "label": "3", @@ -345,7 +321,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "4/1", - }, + } ], "speed": 30, "label": "4", @@ -355,14 +331,12 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "coolingOk", - }, + } ], "inputs": {"states": ["ok"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-additional-states", - "test": VerifyEnvironmentCooling, + (VerifyEnvironmentCooling, "success-additional-states"): { "eos_data": [ { "defaultZones": False, @@ -386,7 +360,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply1/1", - }, + } ], "speed": 30, "label": "PowerSupply1", @@ -395,7 +369,7 @@ DATA: list[dict[str, Any]] = [ "status": "ok", "fans": [ { - "status": "Not Inserted", + "status": "powerLoss", "uptime": 1682498935.9121106, "maxSpeed": 23000, "lastSpeedStableChangeTime": 1682499092.4665174, @@ -404,7 +378,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply2/1", - }, + } ], "speed": 30, "label": "PowerSupply2", @@ -424,7 +398,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "1/1", - }, + } ], "speed": 30, "label": "1", @@ -442,7 +416,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "2/1", - }, + } ], "speed": 30, "label": "2", @@ -460,7 +434,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "3/1", - }, + } ], "speed": 30, "label": "3", @@ -478,7 +452,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "4/1", - }, + } ], "speed": 30, "label": "4", @@ -488,14 +462,12 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "coolingOk", - }, + } ], - "inputs": {"states": ["ok", "Not Inserted"]}, - "expected": {"result": "success"}, + "inputs": {"states": ["ok", "powerLoss"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-fan-tray", - "test": VerifyEnvironmentCooling, + (VerifyEnvironmentCooling, "failure-fan-tray"): { "eos_data": [ { "defaultZones": False, @@ -519,7 +491,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply1/1", - }, + } ], "speed": 30, "label": "PowerSupply1", @@ -537,7 +509,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply2/1", - }, + } ], "speed": 30, "label": "PowerSupply2", @@ -548,7 +520,7 @@ DATA: list[dict[str, Any]] = [ "status": "ok", "fans": [ { - "status": "down", + "status": "unknownHwStatus", "uptime": 1682498923.9303148, "maxSpeed": 17500, "lastSpeedStableChangeTime": 1682498975.0139885, @@ -557,7 +529,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "1/1", - }, + } ], "speed": 30, "label": "1", @@ -575,7 +547,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "2/1", - }, + } ], "speed": 30, "label": "2", @@ -584,7 +556,7 @@ DATA: list[dict[str, Any]] = [ "status": "ok", "fans": [ { - "status": "Not Inserted", + "status": "powerLoss", "uptime": 1682498923.9383528, "maxSpeed": 17500, "lastSpeedStableChangeTime": 1682498975.0140095, @@ -593,7 +565,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "3/1", - }, + } ], "speed": 30, "label": "3", @@ -611,7 +583,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "4/1", - }, + } ], "speed": 30, "label": "4", @@ -621,14 +593,12 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "CoolingKo", - }, + } ], - "inputs": {"states": ["ok", "Not Inserted"]}, - "expected": {"result": "failure", "messages": ["Fan Tray: 1 Fan: 1/1 - Invalid state - Expected: ok, Not Inserted Actual: down"]}, + "inputs": {"states": ["ok", "powerLoss"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Fan Tray: 1 Fan: 1/1 - Invalid state - Expected: ok, powerLoss Actual: unknownHwStatus"]}, }, - { - "name": "failure-power-supply", - "test": VerifyEnvironmentCooling, + (VerifyEnvironmentCooling, "failure-power-supply"): { "eos_data": [ { "defaultZones": False, @@ -643,7 +613,7 @@ DATA: list[dict[str, Any]] = [ "status": "ok", "fans": [ { - "status": "down", + "status": "unknownHwStatus", "uptime": 1682498937.0240965, "maxSpeed": 23000, "lastSpeedStableChangeTime": 1682499033.0403435, @@ -652,7 +622,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply1/1", - }, + } ], "speed": 30, "label": "PowerSupply1", @@ -670,7 +640,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": True, "speedStable": True, "label": "PowerSupply2/1", - }, + } ], "speed": 30, "label": "PowerSupply2", @@ -690,7 +660,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "1/1", - }, + } ], "speed": 30, "label": "1", @@ -708,7 +678,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "2/1", - }, + } ], "speed": 30, "label": "2", @@ -717,7 +687,7 @@ DATA: list[dict[str, Any]] = [ "status": "ok", "fans": [ { - "status": "Not Inserted", + "status": "powerLoss", "uptime": 1682498923.9383528, "maxSpeed": 17500, "lastSpeedStableChangeTime": 1682498975.0140095, @@ -726,7 +696,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "3/1", - }, + } ], "speed": 30, "label": "3", @@ -744,7 +714,7 @@ DATA: list[dict[str, Any]] = [ "speedHwOverride": False, "speedStable": True, "label": "4/1", - }, + } ], "speed": 30, "label": "4", @@ -754,19 +724,15 @@ DATA: list[dict[str, Any]] = [ "currentZones": 1, "configuredZones": 0, "systemStatus": "CoolingKo", - }, + } ], - "inputs": {"states": ["ok", "Not Inserted"]}, + "inputs": {"states": ["ok", "powerLoss"]}, "expected": { - "result": "failure", - "messages": [ - "Power Slot: PowerSupply1 Fan: PowerSupply1/1 - Invalid state - Expected: ok, Not Inserted Actual: down", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Power Slot: PowerSupply1 Fan: PowerSupply1/1 - Invalid state - Expected: ok, powerLoss Actual: unknownHwStatus"], }, }, - { - "name": "success", - "test": VerifyEnvironmentPower, + (VerifyEnvironmentPower, "success"): { "eos_data": [ { "powerSupplies": { @@ -805,62 +771,13 @@ DATA: list[dict[str, Any]] = [ "outputCurrent": 9.828125, "managed": True, }, - }, - }, + } + } ], "inputs": {"states": ["ok"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-additional-states", - "test": VerifyEnvironmentPower, - "eos_data": [ - { - "powerSupplies": { - "1": { - "outputPower": 0.0, - "modelName": "PWR-500AC-F", - "capacity": 500.0, - "tempSensors": { - "TempSensorP1/2": {"status": "ok", "temperature": 0.0}, - "TempSensorP1/3": {"status": "ok", "temperature": 0.0}, - "TempSensorP1/1": {"status": "ok", "temperature": 0.0}, - }, - "fans": {"FanP1/1": {"status": "ok", "speed": 33}}, - "state": "Not Inserted", - "inputCurrent": 0.0, - "dominant": False, - "inputVoltage": 0.0, - "outputCurrent": 0.0, - "managed": True, - }, - "2": { - "outputPower": 117.375, - "uptime": 1682498935.9121966, - "modelName": "PWR-500AC-F", - "capacity": 500.0, - "tempSensors": { - "TempSensorP2/1": {"status": "ok", "temperature": 39.0}, - "TempSensorP2/3": {"status": "ok", "temperature": 43.0}, - "TempSensorP2/2": {"status": "ok", "temperature": 31.0}, - }, - "fans": {"FanP2/1": {"status": "ok", "speed": 33}}, - "state": "ok", - "inputCurrent": 0.572265625, - "dominant": False, - "inputVoltage": 232.5, - "outputCurrent": 9.828125, - "managed": True, - }, - }, - }, - ], - "inputs": {"states": ["ok", "Not Inserted"]}, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyEnvironmentPower, + (VerifyEnvironmentPower, "success-additional-states"): { "eos_data": [ { "powerSupplies": { @@ -899,24 +816,60 @@ DATA: list[dict[str, Any]] = [ "outputCurrent": 9.828125, "managed": True, }, - }, - }, + } + } + ], + "inputs": {"states": ["ok", "powerLoss"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyEnvironmentPower, "failure"): { + "eos_data": [ + { + "powerSupplies": { + "1": { + "outputPower": 0.0, + "modelName": "PWR-500AC-F", + "capacity": 500.0, + "tempSensors": { + "TempSensorP1/2": {"status": "ok", "temperature": 0.0}, + "TempSensorP1/3": {"status": "ok", "temperature": 0.0}, + "TempSensorP1/1": {"status": "ok", "temperature": 0.0}, + }, + "fans": {"FanP1/1": {"status": "ok", "speed": 33}}, + "state": "powerLoss", + "inputCurrent": 0.0, + "dominant": False, + "inputVoltage": 0.0, + "outputCurrent": 0.0, + "managed": True, + }, + "2": { + "outputPower": 117.375, + "uptime": 1682498935.9121966, + "modelName": "PWR-500AC-F", + "capacity": 500.0, + "tempSensors": { + "TempSensorP2/1": {"status": "ok", "temperature": 39.0}, + "TempSensorP2/3": {"status": "ok", "temperature": 43.0}, + "TempSensorP2/2": {"status": "ok", "temperature": 31.0}, + }, + "fans": {"FanP2/1": {"status": "ok", "speed": 33}}, + "state": "ok", + "inputCurrent": 0.572265625, + "dominant": False, + "inputVoltage": 232.5, + "outputCurrent": 9.828125, + "managed": True, + }, + } + } ], "inputs": {"states": ["ok"]}, - "expected": {"result": "failure", "messages": ["Power Slot: 1 - Invalid power supplies state - Expected: ok Actual: powerLoss"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Power Slot: 1 - Invalid power supplies state - Expected: ok Actual: powerLoss"]}, }, - { - "name": "success", - "test": VerifyAdverseDrops, - "eos_data": [{"totalAdverseDrops": 0}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyAdverseDrops, + (VerifyAdverseDrops, "success"): {"eos_data": [{"totalAdverseDrops": 0}], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyAdverseDrops, "failure"): { "eos_data": [{"totalAdverseDrops": 10}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Incorrect total adverse drops counter - Expected: 0 Actual: 10"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Incorrect total adverse drops counter - Expected: 0 Actual: 10"]}, }, -] +} diff --git a/tests/units/anta_tests/test_interfaces.py b/tests/units/anta_tests/test_interfaces.py index 9f4ab11..5729283 100644 --- a/tests/units/anta_tests/test_interfaces.py +++ b/tests/units/anta_tests/test_interfaces.py @@ -6,8 +6,11 @@ # pylint: disable=C0302 from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.interfaces import ( VerifyIllegalLACP, VerifyInterfaceDiscards, @@ -29,10 +32,11 @@ from anta.tests.interfaces import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyInterfaceUtilization, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyInterfaceUtilization, "success"): { "eos_data": [ { "interfaces": { @@ -170,58 +174,113 @@ DATA: list[dict[str, Any]] = [ }, ], "inputs": {"threshold": 70.0}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ignored-interface", - "test": VerifyInterfaceUtilization, + (VerifyInterfaceUtilization, "success-ignored-interface"): { "eos_data": [ { "interfaces": { - "Ethernet1/1": { - "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1", + "Ethernet1": { + "description": "MLAG Peer-link - s1-leaf2", "interval": 300, - "inBpsRate": 2242.2497205060313, - "inPktsRate": 0.00028663359326985426, - "inPpsRate": 3.9005388262031966, + "inBpsRate": 1801.8707256244886, + "inPktsRate": 0.00022136128440856573, + "inPpsRate": 2.573388240382304, + "outBpsRate": 1351.2921726055374, + "outPktsRate": 0.00017125571109710073, + "outPpsRate": 2.2579058647841856, + "lastUpdateTimestamp": 1743750428.6092474, + }, + "Ethernet2": { + "description": "L3 Uplink - s1-spine1", + "interval": 300, + "inBpsRate": 93.35295126808322, + "inPktsRate": 1.0505400223350173e-05, + "inPpsRate": 0.07313156853386583, "outBpsRate": 0.0, "outPktsRate": 0.0, "outPpsRate": 0.0, - "lastUpdateTimestamp": 1710253727.138605, + "lastUpdateTimestamp": 1743750428.6092517, }, - "Port-Channel31": { - "description": "MLAG_PEER_dc1-leaf1b_Po31", + "Ethernet3": { + "description": "L3 Uplink - s1-spine2", "interval": 300, - "inBpsRate": 1862.4876594267096, - "inPktsRate": 0.00011473185873493155, - "inPpsRate": 2.7009344704495084, - "outBpsRate": 1758.0044570479704, - "outPktsRate": 0.00010844978034772172, - "outPpsRate": 2.5686946869154013, - "lastUpdateTimestamp": 1710253726.4029949, - }, - "Port-Channel51": { - "description": "dc1-leaf1-server1", - "interval": 300, - "inBpsRate": 0.0023680437493116147, - "inPpsRate": 2.3125427239371238e-06, + "inBpsRate": 91.64440293982129, + "inPktsRate": 1.0286893435756781e-05, + "inPpsRate": 0.07015332136091573, "outBpsRate": 0.0, + "outPktsRate": 0.0, "outPpsRate": 0.0, - "lastUpdateTimestamp": 1712928643.7805147, + "lastUpdateTimestamp": 1743750428.6091988, }, - }, + "Ethernet4": { + "description": "MLAG Downlink - s1-host1", + "interval": 300, + "inBpsRate": 98.73132596805515, + "inPktsRate": 1.0928950412403655e-05, + "inPpsRate": 0.06598861347488381, + "outBpsRate": 497.70036505586484, + "outPktsRate": 5.810165469175271e-05, + "outPpsRate": 0.52072613663539, + "lastUpdateTimestamp": 1743750428.6092432, + }, + "Ethernet6": { + "description": "MLAG Peer-link - s1-leaf2", + "interval": 300, + "inBpsRate": 98.18960870790458, + "inPktsRate": 1.0859909720407048e-05, + "inPpsRate": 0.06505930310103682, + "outBpsRate": 256.359818648091, + "outPktsRate": 2.9610552696562436e-05, + "outPpsRate": 0.24841067698458383, + "lastUpdateTimestamp": 1743750428.6092384, + }, + "Management0": { + "description": "", + "interval": 300, + "inBpsRate": 7626.480173033807, + "inPktsRate": 0.0009048203095460882, + "inPpsRate": 8.885768265169219, + "outBpsRate": 9127.592145035744, + "outPktsRate": 0.001049926825271909, + "outPpsRate": 8.572975673020922, + "lastUpdateTimestamp": 1743750428.6079214, + }, + "Port-Channel1": { + "description": "MLAG Peer-link - s1-leaf2", + "interval": 300, + "inBpsRate": 1905.0055237111224, + "inPktsRate": 0.00011641274575015027, + "inPpsRate": 2.645308695574268, + "outBpsRate": 1611.693121818935, + "outPktsRate": 0.00010068646137044125, + "outPpsRate": 2.5127256599368124, + "lastUpdateTimestamp": 1743750428.0041468, + }, + "Port-Channel5": { + "description": "MLAG Downlink - s1-host1", + "interval": 300, + "inBpsRate": 99.0032866811298, + "inPktsRate": 5.479571309111963e-06, + "inPpsRate": 0.06617587188193425, + "outBpsRate": 499.030957052671, + "outPktsRate": 2.912854497590912e-05, + "outPpsRate": 0.5221246404094458, + "lastUpdateTimestamp": 1743750428.004128, + }, + } }, { "interfaces": { - "Ethernet1/1": { - "name": "Ethernet1/1", + "Ethernet2": { + "name": "Ethernet2", "forwardingModel": "routed", "lineProtocolStatus": "up", "interfaceStatus": "connected", "hardware": "ethernet", "interfaceAddress": [ { - "primaryIp": {"address": "10.255.255.1", "maskLen": 31}, + "primaryIp": {"address": "10.111.1.1", "maskLen": 31}, "secondaryIps": {}, "secondaryIpsOrderedList": [], "virtualIp": {"address": "0.0.0.0", "maskLen": 0}, @@ -231,144 +290,405 @@ DATA: list[dict[str, Any]] = [ "dhcp": False, } ], - "physicalAddress": "aa:c1:ab:7e:76:36", - "burnedInAddress": "aa:c1:ab:7e:76:36", - "description": "P2P_LINK_TO_DC1-SPINE1_Ethernet1/1", + "physicalAddress": "56:4a:04:73:1b:8f", + "burnedInAddress": "56:4a:04:73:1b:8f", + "description": "L3 Uplink - s1-spine1", "bandwidth": 1000000000, "mtu": 1500, - "l3MtuConfigured": True, + "l3MtuConfigured": False, "l2Mru": 0, - "lastStatusChangeTimestamp": 1710234511.3085763, + "lastStatusChangeTimestamp": 1743738144.3375356, "interfaceStatistics": { "updateInterval": 300.0, - "inBitsRate": 2240.0023281094, - "inPktsRate": 3.8978070399448654, + "inBitsRate": 92.4778371032985, + "inPktsRate": 0.0746926415480351, "outBitsRate": 0.0, "outPktsRate": 0.0, }, "interfaceCounters": { - "inOctets": 5413008, - "inUcastPkts": 74693, - "inMulticastPkts": 643, - "inBroadcastPkts": 1, - "inDiscards": 0, - "inTotalPkts": 75337, - "outOctets": 0, - "outUcastPkts": 0, - "outMulticastPkts": 0, - "outBroadcastPkts": 0, - "outDiscards": 0, - "outTotalPkts": 0, - "linkStatusChanges": 2, - "totalInErrors": 0, - "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, - "totalOutErrors": 0, - "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, - "counterRefreshTime": 1710253760.6489396, - }, - "duplex": "duplexFull", - "autoNegotiate": "unknown", - "loopbackMode": "loopbackNone", - "lanes": 0, - }, - "Port-Channel31": { - "name": "Port-Channel31", - "forwardingModel": "bridged", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "hardware": "portChannel", - "interfaceAddress": [], - "physicalAddress": "aa:c1:ab:72:58:40", - "description": "MLAG_PEER_dc1-leaf1b_Po31", - "bandwidth": 2000000000, - "mtu": 9214, - "l3MtuConfigured": False, - "l2Mru": 0, - "lastStatusChangeTimestamp": 1710234510.1133935, - "interfaceStatistics": { - "updateInterval": 300.0, - "inBitsRate": 1854.287898883752, - "inPktsRate": 2.6902775246495665, - "outBitsRate": 1749.1141130864632, - "outPktsRate": 2.5565618978302362, - }, - "interfaceCounters": { - "inOctets": 4475556, - "inUcastPkts": 48949, - "inMulticastPkts": 2579, - "inBroadcastPkts": 2, - "inDiscards": 0, - "inTotalPkts": 51530, - "outOctets": 4230011, - "outUcastPkts": 48982, - "outMulticastPkts": 6, - "outBroadcastPkts": 2, - "outDiscards": 0, - "outTotalPkts": 48990, - "linkStatusChanges": 2, - "totalInErrors": 0, - "totalOutErrors": 0, - "counterRefreshTime": 1710253760.6500373, - }, - "memberInterfaces": { - "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexFull"}, - "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"}, - }, - "fallbackEnabled": False, - "fallbackEnabledType": "fallbackNone", - }, - "Port-Channel51": { - "name": "Port-Channel51", - "forwardingModel": "bridged", - "lineProtocolStatus": "lowerLayerDown", - "interfaceStatus": "notconnect", - "hardware": "portChannel", - "interfaceAddress": [], - "physicalAddress": "00:00:00:00:00:00", - "description": "dc1-leaf1-server1", - "bandwidth": 0, - "mtu": 9214, - "l3MtuConfigured": False, - "l2Mru": 0, - "lastStatusChangeTimestamp": 1712925798.5035574, - "interfaceStatistics": { - "updateInterval": 300.0, - "inBitsRate": 0.00839301770723288, - "inPktsRate": 8.19630635471961e-06, - "outBitsRate": 0.0, - "outPktsRate": 0.0, - }, - "interfaceCounters": { - "inOctets": 329344, - "inUcastPkts": 0, - "inMulticastPkts": 2573, + "inOctets": 143806, + "inUcastPkts": 497, + "inMulticastPkts": 415, "inBroadcastPkts": 0, "inDiscards": 0, - "inTotalPkts": 2573, + "inTotalPkts": 912, "outOctets": 0, "outUcastPkts": 0, "outMulticastPkts": 0, "outBroadcastPkts": 0, "outDiscards": 0, "outTotalPkts": 0, + "linkStatusChanges": 2, + "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, + "totalOutErrors": 0, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.606648, + }, + "duplex": "duplexHalf", + "autoNegotiate": "unknown", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Ethernet4": { + "name": "Ethernet4", + "forwardingModel": "dataLink", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "interfaceAddress": [], + "physicalAddress": "76:65:c2:9b:b6:c6", + "burnedInAddress": "76:65:c2:9b:b6:c6", + "description": "MLAG Downlink - s1-host1", + "bandwidth": 1000000000, + "mtu": 9214, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738144.3373442, + "interfaceMembership": "Member of Port-Channel5", + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 100.7957345751666, + "inPktsRate": 0.06629448229302994, + "outBitsRate": 497.7020017231056, + "outPktsRate": 0.5202975240121512, + }, + "interfaceCounters": { + "inOctets": 157065, + "inUcastPkts": 0, + "inMulticastPkts": 833, + "inBroadcastPkts": 1, + "inDiscards": 0, + "inTotalPkts": 834, + "outOctets": 771821, + "outUcastPkts": 0, + "outMulticastPkts": 6071, + "outBroadcastPkts": 392, + "outDiscards": 0, + "outTotalPkts": 6463, + "linkStatusChanges": 2, + "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, + "totalOutErrors": 0, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.608292, + }, + "duplex": "duplexHalf", + "autoNegotiate": "unknown", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Ethernet1": { + "name": "Ethernet1", + "forwardingModel": "dataLink", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "interfaceAddress": [], + "physicalAddress": "02:42:96:67:17:36", + "burnedInAddress": "02:42:96:67:17:36", + "description": "MLAG Peer-link - s1-leaf2", + "bandwidth": 1000000000, + "mtu": 9214, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738132.4965024, + "interfaceMembership": "Member of Port-Channel1", + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 1812.7431567446233, + "inPktsRate": 2.585487950559777, + "outBitsRate": 1356.8652036248704, + "outPktsRate": 2.266164541741404, + }, + "interfaceCounters": { + "inOctets": 2853088, + "inUcastPkts": 30923, + "inMulticastPkts": 838, + "inBroadcastPkts": 394, + "inDiscards": 0, + "inTotalPkts": 32155, + "outOctets": 2150114, + "outUcastPkts": 27821, + "outMulticastPkts": 4, + "outBroadcastPkts": 394, + "outDiscards": 0, + "outTotalPkts": 28219, + "linkStatusChanges": 2, + "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, + "totalOutErrors": 0, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.609684, + }, + "duplex": "duplexFull", + "autoNegotiate": "unknown", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Ethernet3": { + "name": "Ethernet3", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "interfaceAddress": [ + { + "primaryIp": {"address": "10.111.2.1", "maskLen": 31}, + "secondaryIps": {}, + "secondaryIpsOrderedList": [], + "virtualIp": {"address": "0.0.0.0", "maskLen": 0}, + "virtualSecondaryIps": {}, + "virtualSecondaryIpsOrderedList": [], + "broadcastAddress": "255.255.255.255", + "dhcp": False, + } + ], + "physicalAddress": "06:9a:0a:bd:c4:0a", + "burnedInAddress": "06:9a:0a:bd:c4:0a", + "description": "L3 Uplink - s1-spine2", + "bandwidth": 1000000000, + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738144.3376553, + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 88.71171669451815, + "inPktsRate": 0.06881326151587384, + "outBitsRate": 0.0, + "outPktsRate": 0.0, + }, + "interfaceCounters": { + "inOctets": 143610, + "inUcastPkts": 494, + "inMulticastPkts": 415, + "inBroadcastPkts": 0, + "inDiscards": 0, + "inTotalPkts": 909, + "outOctets": 0, + "outUcastPkts": 0, + "outMulticastPkts": 0, + "outBroadcastPkts": 0, + "outDiscards": 0, + "outTotalPkts": 0, + "linkStatusChanges": 2, + "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, + "totalOutErrors": 0, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.611445, + }, + "duplex": "duplexHalf", + "autoNegotiate": "unknown", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Ethernet6": { + "name": "Ethernet6", + "forwardingModel": "dataLink", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "interfaceAddress": [], + "physicalAddress": "0e:f1:16:69:c2:24", + "burnedInAddress": "0e:f1:16:69:c2:24", + "description": "MLAG Peer-link - s1-leaf2", + "bandwidth": 1000000000, + "mtu": 9214, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738132.4881961, + "interfaceMembership": "Member of Port-Channel1", + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 100.27689321743554, + "inPktsRate": 0.06536784140527623, + "outBitsRate": 256.71341417227643, + "outPktsRate": 0.2487533083064691, + }, + "interfaceCounters": { + "inOctets": 158632, + "inUcastPkts": 0, + "inMulticastPkts": 835, + "inBroadcastPkts": 0, + "inDiscards": 0, + "inTotalPkts": 835, + "outOctets": 399513, + "outUcastPkts": 3097, + "outMulticastPkts": 0, + "outBroadcastPkts": 0, + "outDiscards": 0, + "outTotalPkts": 3097, + "linkStatusChanges": 2, + "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, + "totalOutErrors": 0, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.61287, + }, + "duplex": "duplexFull", + "autoNegotiate": "unknown", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Management0": { + "name": "Management0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "interfaceAddress": [ + { + "primaryIp": {"address": "192.168.0.12", "maskLen": 24}, + "secondaryIps": {}, + "secondaryIpsOrderedList": [], + "virtualIp": {"address": "0.0.0.0", "maskLen": 0}, + "virtualSecondaryIps": {}, + "virtualSecondaryIpsOrderedList": [], + "broadcastAddress": "255.255.255.255", + "dhcp": False, + } + ], + "physicalAddress": "12:0f:d9:6d:47:f7", + "burnedInAddress": "12:0f:d9:6d:47:f7", + "description": "", + "bandwidth": 1000000000, + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738085.9221241, + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 7012.071138622059, + "inPktsRate": 8.017886333233701, + "outBitsRate": 8498.044232124328, + "outPktsRate": 8.009337516051946, + }, + "interfaceCounters": { + "inOctets": 6411618, + "inUcastPkts": 47869, + "inMulticastPkts": 0, + "inBroadcastPkts": 0, + "inDiscards": 0, + "inTotalPkts": 47869, + "outOctets": 8628471, + "outUcastPkts": 62799, + "outMulticastPkts": 0, + "outBroadcastPkts": 0, + "outDiscards": 0, + "outTotalPkts": 62799, "linkStatusChanges": 3, "totalInErrors": 0, + "inputErrorsDetail": {"runtFrames": 0, "giantFrames": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0, "rxPause": 0}, "totalOutErrors": 0, - "counterRefreshTime": 1712928265.9816775, + "outputErrorsDetail": {"collisions": 0, "lateCollisions": 0, "deferredTransmissions": 0, "txPause": 0}, + "counterRefreshTime": 1743750532.614511, + }, + "duplex": "duplexHalf", + "autoNegotiate": "success", + "loopbackMode": "loopbackNone", + "lanes": 0, + }, + "Port-Channel1": { + "name": "Port-Channel1", + "forwardingModel": "bridged", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "portChannel", + "interfaceAddress": [], + "physicalAddress": "02:42:96:67:17:36", + "description": "MLAG Peer-link - s1-leaf2", + "bandwidth": 2000000000, + "mtu": 9214, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738138.0347695, + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 1913.0830400575996, + "inPktsRate": 2.650885401124123, + "outBitsRate": 1613.4392876435538, + "outPktsRate": 2.5147804494049457, + }, + "interfaceCounters": { + "inOctets": 3010846, + "inUcastPkts": 30923, + "inMulticastPkts": 1667, + "inBroadcastPkts": 394, + "inDiscards": 0, + "inTotalPkts": 32984, + "outOctets": 2549627, + "outUcastPkts": 30918, + "outMulticastPkts": 4, + "outBroadcastPkts": 394, + "outDiscards": 0, + "outTotalPkts": 31316, + "linkStatusChanges": 2, + "totalInErrors": 0, + "totalOutErrors": 0, + "counterRefreshTime": 1743750532.618036, + }, + "memberInterfaces": { + "Ethernet1": {"bandwidth": 1000000000, "duplex": "duplexHalf"}, + "Ethernet6": {"bandwidth": 1000000000, "duplex": "duplexHalf"}, + }, + "fallbackEnabled": False, + "fallbackEnabledType": "fallbackNone", + }, + "Port-Channel5": { + "name": "Port-Channel5", + "forwardingModel": "bridged", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "portChannel", + "interfaceAddress": [], + "physicalAddress": "76:65:c2:9b:b6:c6", + "description": "MLAG Downlink - s1-host1", + "bandwidth": 2000000000, + "mtu": 9214, + "l3MtuConfigured": False, + "l2Mru": 0, + "lastStatusChangeTimestamp": 1743738149.0576365, + "interfaceStatistics": { + "updateInterval": 300.0, + "inBitsRate": 100.7548468852208, + "inPktsRate": 0.06627334239526746, + "outBitsRate": 497.58423264927563, + "outPktsRate": 0.5201821117573231, + }, + "interfaceCounters": { + "inOctets": 156706, + "inUcastPkts": 0, + "inMulticastPkts": 831, + "inBroadcastPkts": 1, + "inDiscards": 0, + "inTotalPkts": 832, + "outOctets": 771821, + "outUcastPkts": 0, + "outMulticastPkts": 6071, + "outBroadcastPkts": 392, + "outDiscards": 0, + "outTotalPkts": 6463, + "linkStatusChanges": 2, + "totalInErrors": 0, + "totalOutErrors": 0, + "counterRefreshTime": 1743750532.619669, + }, + "memberInterfaces": { + "Ethernet4": {"bandwidth": 1000000000, "duplex": "duplexFull"}, + "PeerEthernet4": {"bandwidth": 1000000000, "duplex": "duplexFull"}, }, - "memberInterfaces": {}, "fallbackEnabled": False, "fallbackEnabledType": "fallbackNone", }, } }, ], - "inputs": {"threshold": 70.0}, - "expected": {"result": "success"}, + "inputs": {"threshold": 70.0, "ignored_interfaces": ["Ethernet", "Port-Channel1", "Management0"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyInterfaceUtilization, + (VerifyInterfaceUtilization, "failure"): { "eos_data": [ { "interfaces": { @@ -378,7 +698,7 @@ DATA: list[dict[str, Any]] = [ "inBpsRate": 100000000.0, "inPktsRate": 0.00028663359326985426, "inPpsRate": 3.9005388262031966, - "outBpsRate": 0.0, + "outBpsRate": 100000000.0, "outPktsRate": 0.0, "outPpsRate": 0.0, "lastUpdateTimestamp": 1710253727.138605, @@ -386,10 +706,10 @@ DATA: list[dict[str, Any]] = [ "Port-Channel31": { "description": "MLAG_PEER_dc1-leaf1b_Po31", "interval": 300, - "inBpsRate": 1862.4876594267096, + "inBpsRate": 100000000.0, "inPktsRate": 0.00011473185873493155, "inPpsRate": 2.7009344704495084, - "outBpsRate": 100000000.0, + "outBpsRate": 1862.4876594267096, "outPktsRate": 0.00010844978034772172, "outPpsRate": 2.5686946869154013, "lastUpdateTimestamp": 1710253726.4029949, @@ -507,16 +827,15 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"threshold": 3.0}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1/1 BPS Rate: inBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 10.0%", - "Interface: Port-Channel31 BPS Rate: outBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 5.0%", + "Interface: Ethernet1/1 BPS Rate: outBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 10.0%", + "Interface: Port-Channel31 BPS Rate: inBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 5.0%", ], }, }, - { - "name": "error-duplex-half", - "test": VerifyInterfaceUtilization, + (VerifyInterfaceUtilization, "error-duplex-half"): { "eos_data": [ { "interfaces": { @@ -655,13 +974,11 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"threshold": 70.0}, "expected": { - "result": "failure", - "messages": ["Interface Ethernet1/1 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented"], + "result": AntaTestStatus.FAILURE, + "messages": ["Interface: Ethernet1/1 - Test not implemented for non-full-duplex interfaces - Expected: duplexFull Actual: duplexHalf"], }, }, - { - "name": "error-duplex-half-po", - "test": VerifyInterfaceUtilization, + (VerifyInterfaceUtilization, "error-duplex-half-po"): { "eos_data": [ { "interfaces": { @@ -800,185 +1117,182 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"threshold": 70.0}, "expected": { - "result": "failure", - "messages": ["Interface Port-Channel31 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented"], + "result": AntaTestStatus.FAILURE, + "messages": [ + "Interface: Port-Channel31 Member Interface: Ethernet3/1 - Test not implemented for non-full-duplex interfaces - " + "Expected: duplexFull Actual: duplexHalf", + "Interface: Port-Channel31 Member Interface: Ethernet4/1 - Test not implemented for non-full-duplex interfaces - " + "Expected: duplexFull Actual: duplexHalf", + ], }, }, - { - "name": "success", - "test": VerifyInterfaceErrors, + (VerifyInterfaceErrors, "success"): { "eos_data": [ { "interfaceErrorCounters": { "Ethernet1": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, "Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, - }, - }, + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-multiple-intfs", - "test": VerifyInterfaceErrors, + (VerifyInterfaceErrors, "success-ignore-interface"): { + "eos_data": [ + { + "interfaceErrorCounters": { + "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, + "Management0": { + "inErrors": 0, + "frameTooLongs": 0, + "outErrors": 0, + "frameTooShorts": 0, + "fcsErrors": 0, + "alignmentErrors": 666, + "symbolErrors": 0, + }, + } + } + ], + "inputs": {"ignored_interfaces": ["Ethernet", "Management0"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyInterfaceErrors, "failure-ignore-interface"): { + "eos_data": [ + { + "interfaceErrorCounters": { + "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, + "Management0": { + "inErrors": 0, + "frameTooLongs": 0, + "outErrors": 0, + "frameTooShorts": 0, + "fcsErrors": 0, + "alignmentErrors": 666, + "symbolErrors": 0, + }, + "Ethernet10": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, + } + } + ], + "inputs": {"ignored_interfaces": ["Ethernet1", "Management0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet10 - Non-zero error counter(s) - inErrors: 42"]}, + }, + (VerifyInterfaceErrors, "failure-multiple-intfs"): { "eos_data": [ { "interfaceErrorCounters": { "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, "Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 666, "symbolErrors": 0}, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42", "Interface: Ethernet6 - Non-zero error counter(s) - alignmentErrors: 666", ], }, }, - { - "name": "failure-multiple-intfs-multiple-errors", - "test": VerifyInterfaceErrors, + (VerifyInterfaceErrors, "failure-multiple-intfs-multiple-errors"): { "eos_data": [ { "interfaceErrorCounters": { "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 10, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, "Ethernet6": {"inErrors": 0, "frameTooLongs": 0, "outErrors": 0, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 6, "symbolErrors": 10}, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42, outErrors: 10", "Interface: Ethernet6 - Non-zero error counter(s) - alignmentErrors: 6, symbolErrors: 10", ], }, }, - { - "name": "failure-single-intf-multiple-errors", - "test": VerifyInterfaceErrors, + (VerifyInterfaceErrors, "failure-single-intf-multiple-errors"): { "eos_data": [ { "interfaceErrorCounters": { - "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 2, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0}, - }, - }, + "Ethernet1": {"inErrors": 42, "frameTooLongs": 0, "outErrors": 2, "frameTooShorts": 0, "fcsErrors": 0, "alignmentErrors": 0, "symbolErrors": 0} + } + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42, outErrors: 2"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42, outErrors: 2"]}, }, - { - "name": "success", - "test": VerifyInterfaceDiscards, + (VerifyInterfaceDiscards, "success"): { "eos_data": [ { "inDiscardsTotal": 0, - "interfaces": { - "Ethernet2": {"outDiscards": 0, "inDiscards": 0}, - "Ethernet1": {"outDiscards": 0, "inDiscards": 0}, - }, + "interfaces": {"Ethernet2": {"outDiscards": 0, "inDiscards": 0}, "Ethernet1": {"outDiscards": 0, "inDiscards": 0}}, "outDiscardsTotal": 0, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyInterfaceDiscards, + (VerifyInterfaceDiscards, "success-ignored-interface"): { "eos_data": [ { "inDiscardsTotal": 0, "interfaces": { "Ethernet2": {"outDiscards": 42, "inDiscards": 0}, "Ethernet1": {"outDiscards": 0, "inDiscards": 42}, + "Ethernet3": {"outDiscards": 0, "inDiscards": 42}, + "Port-Channel1": {"outDiscards": 0, "inDiscards": 42}, + "Port-Channel2": {"outDiscards": 0, "inDiscards": 0}, }, "outDiscardsTotal": 0, - }, + } + ], + "inputs": {"ignored_interfaces": ["Port-Channel1", "Ethernet"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyInterfaceDiscards, "failure"): { + "eos_data": [ + { + "inDiscardsTotal": 0, + "interfaces": {"Ethernet2": {"outDiscards": 42, "inDiscards": 0}, "Ethernet1": {"outDiscards": 0, "inDiscards": 42}}, + "outDiscardsTotal": 0, + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet2 - Non-zero discard counter(s): outDiscards: 42", "Interface: Ethernet1 - Non-zero discard counter(s): inDiscards: 42", ], }, }, - { - "name": "success", - "test": VerifyInterfaceErrDisabled, - "eos_data": [ - { - "interfaceStatuses": { - "Management1": { - "linkStatus": "connected", - }, - "Ethernet8": { - "linkStatus": "connected", - }, - }, - }, - ], - "inputs": None, - "expected": {"result": "success"}, + (VerifyInterfaceErrDisabled, "success"): {"eos_data": [{"interfaceStatuses": {}}], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyInterfaceErrDisabled, "failure"): { + "eos_data": [{"interfaceStatuses": {"Ethernet2": {"description": "", "status": "errdisabled", "causes": ["bpduguard"]}}}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Error disabled - Causes: bpduguard"]}, }, - { - "name": "failure", - "test": VerifyInterfaceErrDisabled, - "eos_data": [ - { - "interfaceStatuses": { - "Management1": { - "linkStatus": "errdisabled", - }, - "Ethernet8": { - "linkStatus": "errdisabled", - }, - }, - }, - ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Interface: Management1 - Link status Error disabled", "Interface: Ethernet8 - Link status Error disabled"]}, + (VerifyInterfaceErrDisabled, "failure-no-cause"): { + "eos_data": [{"interfaceStatuses": {"Ethernet2": {"description": "", "status": "errdisabled"}}}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Error disabled"]}, }, - { - "name": "success", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "success"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, - ], - "inputs": {"interfaces": [{"name": "Ethernet2", "status": "adminDown"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, - "expected": {"result": "success"}, - }, - { - "name": "success-up-with-line-protocol-status", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "down"}, } } ], - "inputs": {"interfaces": [{"name": "Ethernet8", "status": "up", "line_protocol_status": "down"}]}, - "expected": {"result": "success"}, + "inputs": {"interfaces": [{"name": "Ethernet2", "status": "adminDown"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-with-line-protocol-status", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "success-up-with-line-protocol-status"): { + "eos_data": [{"interfaceDescriptions": {"Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "down"}}}], + "inputs": {"interfaces": [{"name": "Ethernet8", "status": "up", "line_protocol_status": "down"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyInterfacesStatus, "success-with-line-protocol-status"): { "eos_data": [ { "interfaceDescriptions": { @@ -995,181 +1309,103 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet3.10", "status": "down", "line_protocol_status": "dormant"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-lower", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "success-lower"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, + } + } ], "inputs": {"interfaces": [{"name": "ethernet2", "status": "adminDown"}, {"name": "ethernet8", "status": "up"}, {"name": "ethernet3", "status": "up"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-eth-name", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "success-eth-name"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet2": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "down"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, + } + } ], "inputs": {"interfaces": [{"name": "eth2", "status": "adminDown"}, {"name": "et8", "status": "up"}, {"name": "et3", "status": "up"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-po-name", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Port-Channel100": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, - ], + (VerifyInterfacesStatus, "success-po-name"): { + "eos_data": [{"interfaceDescriptions": {"Port-Channel100": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}}}], "inputs": {"interfaces": [{"name": "po100", "status": "up"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-sub-interfaces", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Ethernet52/1.1963": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, - ], + (VerifyInterfacesStatus, "success-sub-interfaces"): { + "eos_data": [{"interfaceDescriptions": {"Ethernet52/1.1963": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}}}], "inputs": {"interfaces": [{"name": "Ethernet52/1.1963", "status": "up"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-transceiver-down", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Ethernet49/1": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "notPresent"}, - } - } - ], + (VerifyInterfacesStatus, "success-transceiver-down"): { + "eos_data": [{"interfaceDescriptions": {"Ethernet49/1": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "notPresent"}}}], "inputs": {"interfaces": [{"name": "Ethernet49/1", "status": "adminDown"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-po-down", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Port-Channel100": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "lowerLayerDown"}, - } - } - ], + (VerifyInterfacesStatus, "success-po-down"): { + "eos_data": [{"interfaceDescriptions": {"Port-Channel100": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "lowerLayerDown"}}}], "inputs": {"interfaces": [{"name": "PortChannel100", "status": "adminDown"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-po-lowerlayerdown", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Port-Channel100": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "lowerLayerDown"}, - } - } - ], + (VerifyInterfacesStatus, "success-po-lowerlayerdown"): { + "eos_data": [{"interfaceDescriptions": {"Port-Channel100": {"interfaceStatus": "adminDown", "description": "", "lineProtocolStatus": "lowerLayerDown"}}}], "inputs": {"interfaces": [{"name": "Port-Channel100", "status": "adminDown", "line_protocol_status": "lowerLayerDown"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "failure-not-configured"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, + } + } ], "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, - "expected": { - "result": "failure", - "messages": ["Ethernet8 - Not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Ethernet8 - Not configured"]}, }, - { - "name": "failure-status-down", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "failure-status-down"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet8": {"interfaceStatus": "down", "description": "", "lineProtocolStatus": "down"}, "Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, + } + } ], "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, - "expected": { - "result": "failure", - "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: down/down"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: down/down"]}, }, - { - "name": "failure-proto-down", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "failure-proto-down"): { "eos_data": [ { "interfaceDescriptions": { "Ethernet8": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "down"}, "Ethernet2": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, "Ethernet3": {"interfaceStatus": "up", "description": "", "lineProtocolStatus": "up"}, - }, - }, - ], - "inputs": { - "interfaces": [ - {"name": "Ethernet2", "status": "up"}, - {"name": "Ethernet8", "status": "up"}, - {"name": "Ethernet3", "status": "up"}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down"], - }, - }, - { - "name": "failure-po-status-down", - "test": VerifyInterfacesStatus, - "eos_data": [ - { - "interfaceDescriptions": { - "Port-Channel100": {"interfaceStatus": "down", "description": "", "lineProtocolStatus": "lowerLayerDown"}, } } ], - "inputs": {"interfaces": [{"name": "PortChannel100", "status": "up"}]}, - "expected": { - "result": "failure", - "messages": ["Port-Channel100 - Status mismatch - Expected: up/up, Actual: down/lowerLayerDown"], - }, + "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down"]}, }, - { - "name": "failure-proto-unknown", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "failure-po-status-down"): { + "eos_data": [{"interfaceDescriptions": {"Port-Channel100": {"interfaceStatus": "down", "description": "", "lineProtocolStatus": "lowerLayerDown"}}}], + "inputs": {"interfaces": [{"name": "PortChannel100", "status": "up"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Port-Channel100 - Status mismatch - Expected: up/up, Actual: down/lowerLayerDown"]}, + }, + (VerifyInterfacesStatus, "failure-proto-unknown"): { "eos_data": [ { "interfaceDescriptions": { @@ -1187,16 +1423,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", - "messages": [ - "Ethernet2 - Status mismatch - Expected: up/down, Actual: up/unknown", - "Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Ethernet2 - Status mismatch - Expected: up/down, Actual: up/unknown", "Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down"], }, }, - { - "name": "failure-interface-status-down", - "test": VerifyInterfacesStatus, + (VerifyInterfacesStatus, "failure-interface-status-down"): { "eos_data": [ { "interfaceDescriptions": { @@ -1206,15 +1437,9 @@ DATA: list[dict[str, Any]] = [ } } ], - "inputs": { - "interfaces": [ - {"name": "Ethernet2", "status": "down"}, - {"name": "Ethernet8", "status": "down"}, - {"name": "Ethernet3", "status": "down"}, - ] - }, + "inputs": {"interfaces": [{"name": "Ethernet2", "status": "down"}, {"name": "Ethernet8", "status": "down"}, {"name": "Ethernet3", "status": "down"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Ethernet2 - Status mismatch - Expected: down, Actual: up", "Ethernet8 - Status mismatch - Expected: down, Actual: up", @@ -1222,9 +1447,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyStormControlDrops, + (VerifyStormControlDrops, "success"): { "eos_data": [ { "aggregateTrafficClasses": {}, @@ -1234,16 +1457,13 @@ DATA: list[dict[str, Any]] = [ "active": True, "reason": "", "errdisabled": False, - }, + } }, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyStormControlDrops, + (VerifyStormControlDrops, "failure"): { "eos_data": [ { "aggregateTrafficClasses": {}, @@ -1253,16 +1473,13 @@ DATA: list[dict[str, Any]] = [ "active": True, "reason": "", "errdisabled": False, - }, + } }, - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Interface: Ethernet1 - Non-zero storm-control drop counter(s) - broadcast: 666"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet1 - Non-zero storm-control drop counter(s) - broadcast: 666"]}, }, - { - "name": "success", - "test": VerifyPortChannels, + (VerifyPortChannels, "success"): { "eos_data": [ { "portChannels": { @@ -1276,16 +1493,77 @@ DATA: list[dict[str, Any]] = [ "inactivePorts": {}, "activePorts": {}, "inactiveLag": False, - }, - }, - }, + } + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyPortChannels, + (VerifyPortChannels, "success-ignored-interface"): { + "eos_data": [ + { + "portChannels": { + "Port-Channel1": { + "activePorts": {"Ethernet1": {}, "Ethernet6": {}}, + "rxPorts": {}, + "inactivePorts": {}, + "recircFeature": [], + "inactiveLag": False, + "minLinks": 0, + "minSpeed": "0 gbps", + "currWeight": 0, + "maxWeight": 16, + }, + "Port-Channel5": { + "activePorts": {"Ethernet4": {}, "PeerEthernet4": {}}, + "rxPorts": {}, + "inactivePorts": {"Ethernet8": {"reasonUnconfigured": "waiting for LACP response"}}, + "recircFeature": [], + "inactiveLag": False, + "minLinks": 0, + "minSpeed": "0 gbps", + "currWeight": 0, + "maxWeight": 16, + }, + } + } + ], + "inputs": {"ignored_interfaces": ["Port-Channel5"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyPortChannels, "success-ignored-all-interface"): { + "eos_data": [ + { + "portChannels": { + "Port-Channel1": { + "activePorts": {"Ethernet1": {}, "Ethernet6": {}}, + "rxPorts": {}, + "inactivePorts": {}, + "recircFeature": [], + "inactiveLag": False, + "minLinks": 0, + "minSpeed": "0 gbps", + "currWeight": 0, + "maxWeight": 16, + }, + "Port-Channel5": { + "activePorts": {"Ethernet4": {}, "PeerEthernet4": {}}, + "rxPorts": {}, + "inactivePorts": {"Ethernet8": {"reasonUnconfigured": "waiting for LACP response"}}, + "recircFeature": [], + "inactiveLag": False, + "minLinks": 0, + "minSpeed": "0 gbps", + "currWeight": 0, + "maxWeight": 16, + }, + } + } + ], + "inputs": {"ignored_interfaces": ["Port-Channel5"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyPortChannels, "failure"): { "eos_data": [ { "portChannels": { @@ -1299,16 +1577,13 @@ DATA: list[dict[str, Any]] = [ "inactivePorts": {"Ethernet8": {"reasonUnconfigured": "waiting for LACP response"}}, "activePorts": {}, "inactiveLag": False, - }, - }, - }, + } + } + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Port-Channel42 - Inactive port(s) - Ethernet8"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Port-Channel42 - Inactive port(s) - Ethernet8"]}, }, - { - "name": "success", - "test": VerifyIllegalLACP, + (VerifyIllegalLACP, "success"): { "eos_data": [ { "portChannels": { @@ -1323,19 +1598,67 @@ DATA: list[dict[str, Any]] = [ "lacpdusTxCount": 454, "markersTxCount": 0, "markersRxCount": 0, + } + } + } + }, + "orphanPorts": {}, + } + ], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyIllegalLACP, "success-ignored-interface"): { + "eos_data": [ + { + "portChannels": { + "Port-Channel1": { + "interfaces": { + "Ethernet1": { + "actorPortStatus": "bundled", + "lacpdusRxCount": 512, + "lacpdusTxCount": 514, + "markersRxCount": 0, + "markersTxCount": 0, + "markerResponseRxCount": 0, + "markerResponseTxCount": 0, + "illegalRxCount": 66, }, + "Ethernet6": { + "actorPortStatus": "bundled", + "lacpdusRxCount": 513, + "lacpdusTxCount": 516, + "markersRxCount": 0, + "markersTxCount": 0, + "markerResponseRxCount": 0, + "markerResponseTxCount": 0, + "illegalRxCount": 0, + }, + } + }, + "Port-Channel5": { + "markers": {"markers": ["*"]}, + "interfaces": { + "Ethernet4": { + "actorPortStatus": "bundled", + "lacpdusRxCount": 521, + "lacpdusTxCount": 15119, + "markersRxCount": 0, + "markersTxCount": 0, + "markerResponseRxCount": 0, + "markerResponseTxCount": 0, + "illegalRxCount": 66, + } }, }, }, + "markerMessages": {"markerMessages": [{"marker": "*"}]}, "orphanPorts": {}, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "inputs": {"ignored_interfaces": ["Port-Channel1", "Port-Channel5"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyIllegalLACP, + (VerifyIllegalLACP, "failure"): { "eos_data": [ { "portChannels": { @@ -1350,22 +1673,16 @@ DATA: list[dict[str, Any]] = [ "lacpdusTxCount": 454, "markersTxCount": 0, "markersRxCount": 0, - }, - }, - }, + } + } + } }, "orphanPorts": {}, - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Port-Channel42 Interface: Ethernet8 - Illegal LACP packets found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Port-Channel42 Interface: Ethernet8 - Illegal LACP packets found"]}, }, - { - "name": "success", - "test": VerifyLoopbackCount, + (VerifyLoopbackCount, "success"): { "eos_data": [ { "interfaces": { @@ -1385,7 +1702,6 @@ DATA: list[dict[str, Any]] = [ "lineProtocolStatus": "up", "mtu": 65535, }, - # Checking not loopbacks are skipped "Ethernet666": { "name": "Ethernet666", "interfaceStatus": "connected", @@ -1393,15 +1709,13 @@ DATA: list[dict[str, Any]] = [ "ipv4Routable240": False, "lineProtocolStatus": "up", }, - }, - }, + } + } ], "inputs": {"number": 2}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-loopback-down", - "test": VerifyLoopbackCount, + (VerifyLoopbackCount, "failure-loopback-down"): { "eos_data": [ { "interfaces": { @@ -1421,21 +1735,19 @@ DATA: list[dict[str, Any]] = [ "lineProtocolStatus": "down", "mtu": 65535, }, - }, - }, + } + } ], "inputs": {"number": 2}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Loopback666 - Invalid line protocol status - Expected: up Actual: down", "Interface: Loopback666 - Invalid interface status - Expected: connected Actual: notconnect", ], }, }, - { - "name": "failure-count-loopback", - "test": VerifyLoopbackCount, + (VerifyLoopbackCount, "failure-count-loopback"): { "eos_data": [ { "interfaces": { @@ -1446,16 +1758,14 @@ DATA: list[dict[str, Any]] = [ "ipv4Routable240": False, "lineProtocolStatus": "up", "mtu": 65535, - }, - }, - }, + } + } + } ], "inputs": {"number": 2}, - "expected": {"result": "failure", "messages": ["Loopback interface(s) count mismatch: Expected 2 Actual: 1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Loopback interface(s) count mismatch: Expected 2 Actual: 1"]}, }, - { - "name": "success", - "test": VerifySVI, + (VerifySVI, "success"): { "eos_data": [ { "interfaces": { @@ -1466,16 +1776,13 @@ DATA: list[dict[str, Any]] = [ "ipv4Routable240": False, "lineProtocolStatus": "up", "mtu": 1500, - }, - }, - }, + } + } + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifySVI, + (VerifySVI, "failure"): { "eos_data": [ { "interfaces": { @@ -1486,22 +1793,19 @@ DATA: list[dict[str, Any]] = [ "ipv4Routable240": False, "lineProtocolStatus": "lowerLayerDown", "mtu": 1500, - }, - }, - }, + } + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "SVI: Vlan42 - Invalid line protocol status - Expected: up Actual: lowerLayerDown", "SVI: Vlan42 - Invalid interface status - Expected: connected Actual: notconnect", ], }, }, - { - "name": "success", - "test": VerifyL3MTU, + (VerifyL3MTU, "success"): { "eos_data": [ { "interfaces": { @@ -1565,15 +1869,13 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], "inputs": {"mtu": 1500}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success", - "test": VerifyL3MTU, + (VerifyL3MTU, "success-ignored-interfaces"): { "eos_data": [ { "interfaces": { @@ -1637,15 +1939,13 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Port-Channel", "Management", "Vxlan"], "specific_mtu": [{"Ethernet10": 1501}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyL3MTU, + (VerifyL3MTU, "failure"): { "eos_data": [ { "interfaces": { @@ -1709,15 +2009,13 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], "inputs": {"mtu": 1500}, - "expected": {"result": "failure", "messages": ["Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 1600"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 1600"]}, }, - { - "name": "failure-specified-interface-mtu", - "test": VerifyL3MTU, + (VerifyL3MTU, "failure-specified-interface-mtu"): { "eos_data": [ { "interfaces": { @@ -1781,25 +2079,244 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], - "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Port-Channel", "Management", "Vxlan"], "specific_mtu": [{"Ethernet10": 1501}]}, - "expected": {"result": "failure", "messages": ["Interface: Ethernet10 - Incorrect MTU - Expected: 1501 Actual: 1502"]}, + "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Port-Channel2", "Management", "Vxlan1"], "specific_mtu": [{"Ethernet10": 1501}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet10 - Incorrect MTU - Expected: 1501 Actual: 1502"]}, }, - { - "name": "success", - "test": VerifyL2MTU, + (VerifyL3MTU, "failure-ignored-specified-interface-mtu"): { + "eos_data": [ + { + "interfaces": { + "Ethernet2": { + "name": "Ethernet2", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1503, + "l3MtuConfigured": True, + "l2Mru": 0, + }, + "Ethernet1/1": { + "name": "Ethernet1/1", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1502, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Ethernet1.100": { + "name": "Ethernet1.100", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1507, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Port-Channel2": { + "name": "Port-Channel2", + "forwardingModel": "bridged", + "lineProtocolStatus": "lowerLayerDown", + "interfaceStatus": "notconnect", + "hardware": "portChannel", + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Loopback0": { + "name": "Loopback0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "loopback", + "mtu": 65535, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Vxlan1": { + "name": "Vxlan1", + "forwardingModel": "bridged", + "lineProtocolStatus": "down", + "interfaceStatus": "notconnect", + "hardware": "vxlan", + "mtu": 0, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + } + } + ], + "inputs": { + "mtu": 1500, + "ignored_interfaces": ["Loopback", "Port-Channel2", "Management", "Vxlan1", "Ethernet1/1", "Ethernet1.100"], + "specific_mtu": [{"Ethernet1/1": 1501}], + }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 1503"]}, + }, + (VerifyL3MTU, "failure-ignored-specified-ethernet"): { + "eos_data": [ + { + "interfaces": { + "Ethernet2": { + "name": "Ethernet2", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1503, + "l3MtuConfigured": True, + "l2Mru": 0, + }, + "Ethernet1/1": { + "name": "Ethernet1/1", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1502, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Ethernet1.100": { + "name": "Ethernet1.100", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1507, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Port-Channel2": { + "name": "Port-Channel2", + "forwardingModel": "bridged", + "lineProtocolStatus": "lowerLayerDown", + "interfaceStatus": "notconnect", + "hardware": "portChannel", + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Loopback0": { + "name": "Loopback0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "loopback", + "mtu": 65535, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Vxlan1": { + "name": "Vxlan1", + "forwardingModel": "bridged", + "lineProtocolStatus": "down", + "interfaceStatus": "notconnect", + "hardware": "vxlan", + "mtu": 0, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + } + } + ], + "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Ethernet1"], "specific_mtu": [{"Ethernet1/1": 1501}]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 1503", + "Interface: Ethernet1/1 - Incorrect MTU - Expected: 1501 Actual: 1502", + "Interface: Ethernet1.100 - Incorrect MTU - Expected: 1500 Actual: 1507", + ], + }, + }, + (VerifyL3MTU, "succuss-ethernet-all"): { + "eos_data": [ + { + "interfaces": { + "Ethernet2": { + "name": "Ethernet2", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1503, + "l3MtuConfigured": True, + "l2Mru": 0, + }, + "Ethernet1/1": { + "name": "Ethernet1/1", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1502, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Ethernet1.100": { + "name": "Ethernet1.100", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1507, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Port-Channel2": { + "name": "Port-Channel2", + "forwardingModel": "bridged", + "lineProtocolStatus": "lowerLayerDown", + "interfaceStatus": "notconnect", + "hardware": "portChannel", + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Loopback0": { + "name": "Loopback0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "loopback", + "mtu": 65535, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Vxlan1": { + "name": "Vxlan1", + "forwardingModel": "bridged", + "lineProtocolStatus": "down", + "interfaceStatus": "notconnect", + "hardware": "vxlan", + "mtu": 0, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + } + } + ], + "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Ethernet"], "specific_mtu": [{"Ethernet1/1": 1501}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyL2MTU, "success"): { "eos_data": [ { "interfaces": { "Ethernet2/1": { "name": "Ethernet2/1", - "forwardingModel": "routed", + "forwardingModel": "bridged", "lineProtocolStatus": "up", "interfaceStatus": "connected", "hardware": "ethernet", - "mtu": 1500, + "mtu": 9218, "l3MtuConfigured": True, "l2Mru": 0, }, @@ -1853,15 +2370,13 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], - "inputs": {"mtu": 9214, "ignored_interfaces": ["Loopback", "Port-Channel", "Management", "Vxlan"], "specific_mtu": [{"Ethernet10": 9214}]}, - "expected": {"result": "success"}, + "inputs": {"mtu": 9214, "ignored_interfaces": ["Loopback0", "Port-Channel", "Management0", "Vxlan", "Ethernet2/1"], "specific_mtu": [{"Ethernet10": 9214}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyL2MTU, + (VerifyL2MTU, "failure"): { "eos_data": [ { "interfaces": { @@ -1925,31 +2440,29 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], "inputs": {"mtu": 1500}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Interface: Ethernet10 - Incorrect MTU configured - Expected: 1500 Actual: 9214", - "Interface: Port-Channel2 - Incorrect MTU configured - Expected: 1500 Actual: 9214", + "Interface: Ethernet10 - Incorrect MTU - Expected: 1500 Actual: 9214", + "Interface: Port-Channel2 - Incorrect MTU - Expected: 1500 Actual: 9214", ], }, }, - { - "name": "failure-specific-interface", - "test": VerifyL2MTU, + (VerifyL2MTU, "failure-specific-interface"): { "eos_data": [ { "interfaces": { - "Ethernet2": { + "Ethernet1.100": { "name": "Ethernet2", - "forwardingModel": "routed", + "forwardingModel": "bridged", "lineProtocolStatus": "up", "interfaceStatus": "connected", "hardware": "ethernet", - "mtu": 1600, + "mtu": 9218, "l3MtuConfigured": True, "l2Mru": 0, }, @@ -2003,81 +2516,42 @@ DATA: list[dict[str, Any]] = [ "l3MtuConfigured": False, "l2Mru": 0, }, - }, - }, + } + } ], - "inputs": {"specific_mtu": [{"Et10": 9214}, {"Port-Channel2": 10000}]}, - "expected": {"result": "failure", "messages": ["Interface: Port-Channel2 - Incorrect MTU configured - Expected: 10000 Actual: 9214"]}, + "inputs": {"specific_mtu": [{"Et10": 9214}, {"Port-Channel2": 10000}], "ignored_interfaces": ["Ethernet", "Vxlan1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Port-Channel2 - Incorrect MTU - Expected: 10000 Actual: 9214"]}, }, - { - "name": "success", - "test": VerifyIPProxyARP, + (VerifyIPProxyARP, "success"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "name": "Ethernet1", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "proxyArp": True, - }, - "Ethernet2": { - "name": "Ethernet2", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "proxyArp": True, - }, - }, - }, + "Ethernet1": {"name": "Ethernet1", "lineProtocolStatus": "up", "interfaceStatus": "connected", "proxyArp": True}, + "Ethernet2": {"name": "Ethernet2", "lineProtocolStatus": "up", "interfaceStatus": "connected", "proxyArp": True}, + } + } ], "inputs": {"interfaces": ["Ethernet1", "Ethernet2"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-interface-not-found", - "test": VerifyIPProxyARP, + (VerifyIPProxyARP, "failure-interface-not-found"): { + "eos_data": [{"interfaces": {"Ethernet1": {"name": "Ethernet1", "lineProtocolStatus": "up", "interfaceStatus": "connected", "proxyArp": True}}}], + "inputs": {"interfaces": ["Ethernet1", "Ethernet2"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Not found"]}, + }, + (VerifyIPProxyARP, "failure"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "name": "Ethernet1", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "proxyArp": True, - }, - }, - }, + "Ethernet1": {"name": "Ethernet1", "lineProtocolStatus": "up", "interfaceStatus": "connected", "proxyArp": True}, + "Ethernet2": {"name": "Ethernet2", "lineProtocolStatus": "up", "interfaceStatus": "connected", "proxyArp": False}, + } + } ], "inputs": {"interfaces": ["Ethernet1", "Ethernet2"]}, - "expected": {"result": "failure", "messages": ["Interface: Ethernet2 - Not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Proxy-ARP disabled"]}, }, - { - "name": "failure", - "test": VerifyIPProxyARP, - "eos_data": [ - { - "interfaces": { - "Ethernet1": { - "name": "Ethernet1", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "proxyArp": True, - }, - "Ethernet2": { - "name": "Ethernet2", - "lineProtocolStatus": "up", - "interfaceStatus": "connected", - "proxyArp": False, - }, - }, - }, - ], - "inputs": {"interfaces": ["Ethernet1", "Ethernet2"]}, - "expected": {"result": "failure", "messages": ["Interface: Ethernet2 - Proxy-ARP disabled"]}, - }, - { - "name": "success", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "success"): { "eos_data": [ { "interfaces": { @@ -2094,7 +2568,7 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], "inputs": { "interfaces": [ @@ -2102,66 +2576,31 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet12", "primary_ip": "172.30.11.10/31", "secondary_ips": ["10.10.10.10/31", "10.10.10.20/31"]}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-without-secondary-ip", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "success-without-secondary-ip"): { "eos_data": [ { "interfaces": { - "Ethernet2": { - "interfaceAddress": { - "primaryIp": {"address": "172.30.11.0", "maskLen": 31}, - "secondaryIpsOrderedList": [], - } - }, - "Ethernet12": { - "interfaceAddress": { - "primaryIp": {"address": "172.30.11.10", "maskLen": 31}, - "secondaryIpsOrderedList": [], - } - }, - } - }, - ], - "inputs": { - "interfaces": [ - {"name": "Ethernet2", "primary_ip": "172.30.11.0/31"}, - {"name": "Ethernet12", "primary_ip": "172.30.11.10/31"}, - ] - }, - "expected": {"result": "success"}, - }, - { - "name": "failure-interface-not-found", - "test": VerifyInterfaceIPv4, - "eos_data": [ - { - "interfaces": { - "Ethernet10": { - "interfaceAddress": { - "primaryIp": {"address": "172.30.11.0", "maskLen": 31}, - "secondaryIpsOrderedList": [], - } - } + "Ethernet2": {"interfaceAddress": {"primaryIp": {"address": "172.30.11.0", "maskLen": 31}, "secondaryIpsOrderedList": []}}, + "Ethernet12": {"interfaceAddress": {"primaryIp": {"address": "172.30.11.10", "maskLen": 31}, "secondaryIpsOrderedList": []}}, } } ], + "inputs": {"interfaces": [{"name": "Ethernet2", "primary_ip": "172.30.11.0/31"}, {"name": "Ethernet12", "primary_ip": "172.30.11.10/31"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyInterfaceIPv4, "failure-interface-not-found"): { + "eos_data": [{"interfaces": {"Ethernet10": {"interfaceAddress": {"primaryIp": {"address": "172.30.11.0", "maskLen": 31}, "secondaryIpsOrderedList": []}}}}], "inputs": { "interfaces": [ {"name": "Ethernet2", "primary_ip": "172.30.11.0/31", "secondary_ips": ["10.10.10.0/31", "10.10.10.10/31"]}, {"name": "Ethernet12", "primary_ip": "172.30.11.20/31", "secondary_ips": ["10.10.11.0/31", "10.10.11.10/31"]}, ] }, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet2 - Not found", "Interface: Ethernet12 - Not found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - Not found", "Interface: Ethernet12 - Not found"]}, }, - { - "name": "failure-not-l3-interface", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "failure-not-l3-interface"): { "eos_data": [{"interfaces": {"Ethernet2": {"interfaceAddress": {}}, "Ethernet12": {"interfaceAddress": {}}}}], "inputs": { "interfaces": [ @@ -2170,30 +2609,18 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet2 - IP address is not configured", "Interface: Ethernet12 - IP address is not configured"], }, }, - { - "name": "failure-ip-address-not-configured", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "failure-ip-address-not-configured"): { "eos_data": [ { "interfaces": { - "Ethernet2": { - "interfaceAddress": { - "primaryIp": {"address": "0.0.0.0", "maskLen": 0}, - "secondaryIpsOrderedList": [], - } - }, - "Ethernet12": { - "interfaceAddress": { - "primaryIp": {"address": "0.0.0.0", "maskLen": 0}, - "secondaryIpsOrderedList": [], - } - }, + "Ethernet2": {"interfaceAddress": {"primaryIp": {"address": "0.0.0.0", "maskLen": 0}, "secondaryIpsOrderedList": []}}, + "Ethernet12": {"interfaceAddress": {"primaryIp": {"address": "0.0.0.0", "maskLen": 0}, "secondaryIpsOrderedList": []}}, } - }, + } ], "inputs": { "interfaces": [ @@ -2202,7 +2629,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.0/31 Actual: 0.0.0.0/0", "Interface: Ethernet2 - Secondary IP address is not configured", @@ -2211,9 +2638,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-ip-address-missmatch", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "failure-ip-address-missmatch"): { "eos_data": [ { "interfaces": { @@ -2230,7 +2655,7 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], "inputs": { "interfaces": [ @@ -2239,7 +2664,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.2/31 Actual: 172.30.11.0/31", "Interface: Ethernet2 - Secondary IP address mismatch - Expected: 10.10.10.20/31, 10.10.10.30/31 Actual: 10.10.10.0/31, 10.10.10.10/31", @@ -2248,18 +2673,11 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-secondary-ip-address", - "test": VerifyInterfaceIPv4, + (VerifyInterfaceIPv4, "failure-secondary-ip-address"): { "eos_data": [ { "interfaces": { - "Ethernet2": { - "interfaceAddress": { - "primaryIp": {"address": "172.30.11.0", "maskLen": 31}, - "secondaryIpsOrderedList": [], - } - }, + "Ethernet2": {"interfaceAddress": {"primaryIp": {"address": "172.30.11.0", "maskLen": 31}, "secondaryIpsOrderedList": []}}, "Ethernet3": { "interfaceAddress": { "primaryIp": {"address": "172.30.10.10", "maskLen": 31}, @@ -2267,7 +2685,7 @@ DATA: list[dict[str, Any]] = [ } }, } - }, + } ], "inputs": { "interfaces": [ @@ -2276,7 +2694,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.2/31 Actual: 172.30.11.0/31", "Interface: Ethernet2 - Secondary IP address is not configured", @@ -2285,66 +2703,24 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyIpVirtualRouterMac, - "eos_data": [ - { - "virtualMacs": [ - { - "macAddress": "00:1c:73:00:dc:01", - } - ], - } - ], + (VerifyIpVirtualRouterMac, "success"): { + "eos_data": [{"virtualMacs": [{"macAddress": "00:1c:73:00:dc:01"}]}], "inputs": {"mac_address": "00:1c:73:00:dc:01"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "faliure-incorrect-mac-address", - "test": VerifyIpVirtualRouterMac, - "eos_data": [ - { - "virtualMacs": [ - { - "macAddress": "00:00:00:00:00:00", - } - ], - } - ], + (VerifyIpVirtualRouterMac, "faliure-incorrect-mac-address"): { + "eos_data": [{"virtualMacs": [{"macAddress": "00:00:00:00:00:00"}]}], "inputs": {"mac_address": "00:1c:73:00:dc:01"}, - "expected": {"result": "failure", "messages": ["IP virtual router MAC address: 00:1c:73:00:dc:01 - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["IP virtual router MAC address: 00:1c:73:00:dc:01 - Not configured"]}, }, - { - "name": "success", - "test": VerifyInterfacesSpeed, + (VerifyInterfacesSpeed, "success"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 2, - }, - "Ethernet1/1/2": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 2, - }, - "Ethernet3": { - "bandwidth": 100000000000, - "autoNegotiate": "success", - "duplex": "duplexFull", - "lanes": 8, - }, - "Ethernet4": { - "bandwidth": 2500000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 8, - }, + "Ethernet1": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 2}, + "Ethernet1/1/2": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 2}, + "Ethernet3": {"bandwidth": 100000000000, "autoNegotiate": "success", "duplex": "duplexFull", "lanes": 8}, + "Ethernet4": {"bandwidth": 2500000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 8}, } } ], @@ -2359,38 +2735,16 @@ DATA: list[dict[str, Any]] = [ {"name": "Ethernet4", "auto": False, "speed": 2.5}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-speed", - "test": VerifyInterfacesSpeed, + (VerifyInterfacesSpeed, "failure-incorrect-speed"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "bandwidth": 100000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 2, - }, - "Ethernet1/1/1": { - "bandwidth": 100000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 2, - }, - "Ethernet3": { - "bandwidth": 10000000000, - "autoNegotiate": "success", - "duplex": "duplexFull", - "lanes": 8, - }, - "Ethernet4": { - "bandwidth": 25000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 8, - }, + "Ethernet1": {"bandwidth": 100000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 2}, + "Ethernet1/1/1": {"bandwidth": 100000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 2}, + "Ethernet3": {"bandwidth": 10000000000, "autoNegotiate": "success", "duplex": "duplexFull", "lanes": 8}, + "Ethernet4": {"bandwidth": 25000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 8}, } } ], @@ -2403,7 +2757,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Bandwidth mismatch - Expected: 1.0Gbps Actual: 100Gbps", "Interface: Ethernet1/1/1 - Bandwidth mismatch - Expected: 1.0Gbps Actual: 100Gbps", @@ -2412,36 +2766,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-mode", - "test": VerifyInterfacesSpeed, + (VerifyInterfacesSpeed, "failure-incorrect-mode"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 2, - }, - "Ethernet1/2/2": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 2, - }, - "Ethernet3": { - "bandwidth": 100000000000, - "autoNegotiate": "success", - "duplex": "duplexHalf", - "lanes": 8, - }, - "Ethernet4": { - "bandwidth": 2500000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 8, - }, + "Ethernet1": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 2}, + "Ethernet1/2/2": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 2}, + "Ethernet3": {"bandwidth": 100000000000, "autoNegotiate": "success", "duplex": "duplexHalf", "lanes": 8}, + "Ethernet4": {"bandwidth": 2500000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 8}, } } ], @@ -2455,7 +2787,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Duplex mode mismatch - Expected: duplexFull Actual: duplexHalf", "Interface: Ethernet1/2/2 - Duplex mode mismatch - Expected: duplexFull Actual: duplexHalf", @@ -2465,42 +2797,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-lane", - "test": VerifyInterfacesSpeed, + (VerifyInterfacesSpeed, "failure-incorrect-lane"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 4, - }, - "Ethernet2": { - "bandwidth": 10000000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 4, - }, - "Ethernet3": { - "bandwidth": 100000000000, - "autoNegotiate": "success", - "duplex": "duplexFull", - "lanes": 4, - }, - "Ethernet4": { - "bandwidth": 2500000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 6, - }, - "Ethernet4/1/1": { - "bandwidth": 2500000000, - "autoNegotiate": "unknown", - "duplex": "duplexFull", - "lanes": 6, - }, + "Ethernet1": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 4}, + "Ethernet2": {"bandwidth": 10000000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 4}, + "Ethernet3": {"bandwidth": 100000000000, "autoNegotiate": "success", "duplex": "duplexFull", "lanes": 4}, + "Ethernet4": {"bandwidth": 2500000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 6}, + "Ethernet4/1/1": {"bandwidth": 2500000000, "autoNegotiate": "unknown", "duplex": "duplexFull", "lanes": 6}, } } ], @@ -2513,7 +2818,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Data lanes count mismatch - Expected: 2 Actual: 4", "Interface: Ethernet3 - Data lanes count mismatch - Expected: 8 Actual: 4", @@ -2522,36 +2827,14 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-all-type", - "test": VerifyInterfacesSpeed, + (VerifyInterfacesSpeed, "failure-all-type"): { "eos_data": [ { "interfaces": { - "Ethernet1": { - "bandwidth": 10000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 4, - }, - "Ethernet2/1/2": { - "bandwidth": 1000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 2, - }, - "Ethernet3": { - "bandwidth": 10000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 6, - }, - "Ethernet4": { - "bandwidth": 25000000000, - "autoNegotiate": "unknown", - "duplex": "duplexHalf", - "lanes": 4, - }, + "Ethernet1": {"bandwidth": 10000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 4}, + "Ethernet2/1/2": {"bandwidth": 1000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 2}, + "Ethernet3": {"bandwidth": 10000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 6}, + "Ethernet4": {"bandwidth": 25000000000, "autoNegotiate": "unknown", "duplex": "duplexHalf", "lanes": 4}, } } ], @@ -2564,7 +2847,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Ethernet1 - Bandwidth mismatch - Expected: 1.0Gbps Actual: 10Gbps", "Interface: Ethernet1 - Duplex mode mismatch - Expected: duplexFull Actual: duplexHalf", @@ -2580,9 +2863,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyLACPInterfacesStatus, + (VerifyLACPInterfacesStatus, "success"): { "eos_data": [ { "portChannels": { @@ -2615,11 +2896,9 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "Port-Channel5"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-short-timeout", - "test": VerifyLACPInterfacesStatus, + (VerifyLACPInterfacesStatus, "success-short-timeout"): { "eos_data": [ { "portChannels": { @@ -2652,49 +2931,21 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "Port-Channel5", "lacp_rate_fast": True}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-bundled", - "test": VerifyLACPInterfacesStatus, + (VerifyLACPInterfacesStatus, "failure-not-bundled"): { "eos_data": [ - { - "portChannels": { - "Port-Channel5": { - "interfaces": { - "Ethernet5": { - "actorPortStatus": "No Aggregate", - } - } - } - }, - "interface": "Ethernet5", - "orphanPorts": {}, - } + {"portChannels": {"Port-Channel5": {"interfaces": {"Ethernet5": {"actorPortStatus": "No Aggregate"}}}}, "interface": "Ethernet5", "orphanPorts": {}} ], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "Po5"}]}, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet5 Port-Channel: Port-Channel5 - Not bundled - Port Status: No Aggregate"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet5 Port-Channel: Port-Channel5 - Not bundled - Port Status: No Aggregate"]}, }, - { - "name": "failure-no-details-found", - "test": VerifyLACPInterfacesStatus, - "eos_data": [ - { - "portChannels": {"Port-Channel5": {"interfaces": {}}}, - } - ], + (VerifyLACPInterfacesStatus, "failure-no-details-found"): { + "eos_data": [{"portChannels": {"Port-Channel5": {"interfaces": {}}}}], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "Po 5"}]}, - "expected": { - "result": "failure", - "messages": ["Interface: Ethernet5 Port-Channel: Port-Channel5 - Not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Ethernet5 Port-Channel: Port-Channel5 - Not configured"]}, }, - { - "name": "failure-lacp-params", - "test": VerifyLACPInterfacesStatus, + (VerifyLACPInterfacesStatus, "failure-lacp-params"): { "eos_data": [ { "portChannels": { @@ -2728,18 +2979,16 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "port-channel 5"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Interface: Ethernet5 Port-Channel: Port-Channel5 - Actor port details mismatch - Activity: False, Aggregation: False, " - "Synchronization: False, Collecting: True, Distributing: True, Timeout: False", - "Interface: Ethernet5 Port-Channel: Port-Channel5 - Partner port details mismatch - Activity: False, Aggregation: False, " - "Synchronization: False, Collecting: True, Distributing: True, Timeout: False", + "Interface: Ethernet5 Port-Channel: Port-Channel5 - Actor port details mismatch - " + "Activity: False, Aggregation: False, Synchronization: False, Collecting: True, Distributing: True, Timeout: False", + "Interface: Ethernet5 Port-Channel: Port-Channel5 - Partner port details mismatch - " + "Activity: False, Aggregation: False, Synchronization: False, Collecting: True, Distributing: True, Timeout: False", ], }, }, - { - "name": "failure-short-timeout", - "test": VerifyLACPInterfacesStatus, + (VerifyLACPInterfacesStatus, "failure-short-timeout"): { "eos_data": [ { "portChannels": { @@ -2773,13 +3022,13 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"interfaces": [{"name": "Ethernet5", "portchannel": "port-channel 5", "lacp_rate_fast": True}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Interface: Ethernet5 Port-Channel: Port-Channel5 - Actor port details mismatch - Activity: True, Aggregation: True, " - "Synchronization: True, Collecting: True, Distributing: True, Timeout: False", - "Interface: Ethernet5 Port-Channel: Port-Channel5 - Partner port details mismatch - Activity: True, Aggregation: True, " - "Synchronization: True, Collecting: True, Distributing: True, Timeout: False", + "Interface: Ethernet5 Port-Channel: Port-Channel5 - Actor port details mismatch - " + "Activity: True, Aggregation: True, Synchronization: True, Collecting: True, Distributing: True, Timeout: False", + "Interface: Ethernet5 Port-Channel: Port-Channel5 - Partner port details mismatch - " + "Activity: True, Aggregation: True, Synchronization: True, Collecting: True, Distributing: True, Timeout: False", ], }, }, -] +} diff --git a/tests/units/anta_tests/test_lanz.py b/tests/units/anta_tests/test_lanz.py index 99a5771..9982a49 100644 --- a/tests/units/anta_tests/test_lanz.py +++ b/tests/units/anta_tests/test_lanz.py @@ -5,24 +5,21 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.lanz import VerifyLANZ from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyLANZ, - "eos_data": [{"lanzEnabled": True}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyLANZ, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyLANZ, "success"): {"eos_data": [{"lanzEnabled": True}], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyLANZ, "failure"): { "eos_data": [{"lanzEnabled": False}], - "inputs": None, - "expected": {"result": "failure", "messages": ["LANZ is not enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["LANZ is not enabled"]}, }, -] +} diff --git a/tests/units/anta_tests/test_logging.py b/tests/units/anta_tests/test_logging.py index 5f79589..6407f37 100644 --- a/tests/units/anta_tests/test_logging.py +++ b/tests/units/anta_tests/test_logging.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.logging import ( VerifyLoggingAccounting, VerifyLoggingEntries, @@ -21,312 +24,199 @@ from anta.tests.logging import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyLoggingPersistent, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyLoggingPersistent, "success"): { "eos_data": [ "Persistent logging: level debugging\n", - """Directory of flash:/persist/messages - - -rw- 9948 May 10 13:54 messages - - 33214693376 bytes total (10081136640 bytes free) - - """, + "Directory of flash:/persist/messages\n\n -rw- 9948 May 10 13:54 messages\n\n" + " 33214693376 bytes total (10081136640 bytes free)\n\n ", ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-disabled", - "test": VerifyLoggingPersistent, + (VerifyLoggingPersistent, "failure-disabled"): { "eos_data": [ "Persistent logging: disabled\n", - """Directory of flash:/persist/messages - - -rw- 0 Apr 13 16:29 messages - - 33214693376 bytes total (10082168832 bytes free) - - """, + "Directory of flash:/persist/messages\n\n -rw- 0 Apr 13 16:29 messages\n\n" + " 33214693376 bytes total (10082168832 bytes free)\n\n ", ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Persistent logging is disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Persistent logging is disabled"]}, }, - { - "name": "failure-not-saved", - "test": VerifyLoggingPersistent, + (VerifyLoggingPersistent, "failure-not-saved"): { "eos_data": [ "Persistent logging: level debugging\n", - """Directory of flash:/persist/messages - - -rw- 0 Apr 13 16:29 messages - - 33214693376 bytes total (10082168832 bytes free) - - """, + "Directory of flash:/persist/messages\n\n -rw- 0 Apr 13 16:29 messages\n\n" + " 33214693376 bytes total (10082168832 bytes free)\n\n ", ], - "inputs": None, - "expected": {"result": "failure", "messages": ["No persistent logs are saved in flash"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No persistent logs are saved in flash"]}, }, - { - "name": "success", - "test": VerifyLoggingSourceIntf, + (VerifyLoggingSourceIntf, "success"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.93' port 514 in VRF MGMT via tcp - Logging to '10.22.10.94' port 911 in VRF MGMT via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.93' port 514 in VRF MGMT via tcp\n" + " Logging to '10.22.10.94' port 911 in VRF MGMT via udp\n\n " ], "inputs": {"interface": "Management0", "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-intf", - "test": VerifyLoggingSourceIntf, + (VerifyLoggingSourceIntf, "failure-intf"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management1', IP Address 172.20.20.12 in VRF MGMT - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.93' port 514 in VRF MGMT via tcp - Logging to '10.22.10.94' port 911 in VRF MGMT via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management1', IP Address 172.20.20.12 in VRF MGMT\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.93' port 514 in VRF MGMT via tcp\n" + " Logging to '10.22.10.94' port 911 in VRF MGMT via udp\n\n " ], "inputs": {"interface": "Management0", "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["Source-interface: Management0 VRF: MGMT - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Source-interface: Management0 VRF: MGMT - Not configured"]}, }, - { - "name": "failure-vrf", - "test": VerifyLoggingSourceIntf, + (VerifyLoggingSourceIntf, "failure-vrf"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF default - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.93' port 514 in VRF MGMT via tcp - Logging to '10.22.10.94' port 911 in VRF MGMT via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF default\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.93' port 514 in VRF MGMT via tcp\n" + " Logging to '10.22.10.94' port 911 in VRF MGMT via udp\n\n " ], "inputs": {"interface": "Management0", "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["Source-interface: Management0 VRF: MGMT - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Source-interface: Management0 VRF: MGMT - Not configured"]}, }, - { - "name": "success", - "test": VerifyLoggingHosts, + (VerifyLoggingHosts, "success"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.93' port 514 in VRF MGMT via tcp - Logging to '10.22.10.94' port 911 in VRF MGMT via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.93' port 514 in VRF MGMT via tcp\n" + " Logging to '10.22.10.94' port 911 in VRF MGMT via udp\n\n " ], "inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-hosts", - "test": VerifyLoggingHosts, + (VerifyLoggingHosts, "failure-hosts"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management1', IP Address 172.20.20.12 in VRF MGMT - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.103' port 514 in VRF MGMT via tcp - Logging to '10.22.10.104' port 911 in VRF MGMT via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management1', IP Address 172.20.20.12 in VRF MGMT\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.103' port 514 in VRF MGMT via tcp\n" + " Logging to '10.22.10.104' port 911 in VRF MGMT via udp\n\n " ], "inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["Syslog servers 10.22.10.93, 10.22.10.94 are not configured in VRF MGMT"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Syslog servers 10.22.10.93, 10.22.10.94 are not configured in VRF MGMT"]}, }, - { - "name": "failure-vrf", - "test": VerifyLoggingHosts, + (VerifyLoggingHosts, "failure-vrf"): { "eos_data": [ - """Trap logging: level informational - Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT - Logging to '10.22.10.92' port 514 in VRF MGMT via udp - Logging to '10.22.10.93' port 514 in VRF default via tcp - Logging to '10.22.10.94' port 911 in VRF default via udp - - """, + "Trap logging: level informational\n Logging source-interface 'Management0', IP Address 172.20.20.12 in VRF MGMT\n" + " Logging to '10.22.10.92' port 514 in VRF MGMT via udp\n Logging to '10.22.10.93' port 514 in VRF default via tcp\n" + " Logging to '10.22.10.94' port 911 in VRF default via udp\n\n " ], "inputs": {"hosts": ["10.22.10.92", "10.22.10.93", "10.22.10.94"], "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["Syslog servers 10.22.10.93, 10.22.10.94 are not configured in VRF MGMT"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Syslog servers 10.22.10.93, 10.22.10.94 are not configured in VRF MGMT"]}, }, - { - "name": "success", - "test": VerifyLoggingLogsGeneration, + (VerifyLoggingLogsGeneration, "success"): { "eos_data": [ "", - "2023-05-10T13:54:21.463497-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingLogsGeneration validation\n", + "2023-05-10T13:54:21.463497-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingLogsGeneration validation\n", ], "inputs": {"severity_level": "informational"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyLoggingLogsGeneration, + (VerifyLoggingLogsGeneration, "failure"): { "eos_data": ["", "Log Buffer:\n"], "inputs": {"severity_level": "notifications"}, - "expected": {"result": "failure", "messages": ["Logs are not generated"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Logs are not generated"]}, }, - { - "name": "success", - "test": VerifyLoggingHostname, + (VerifyLoggingHostname, "success"): { "eos_data": [ {"hostname": "NW-CORE", "fqdn": "NW-CORE.example.org"}, "", - "2023-05-10T15:41:44.701810-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingHostname validation\n", + "2023-05-10T15:41:44.701810-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingHostname validation\n", ], "inputs": {"severity_level": "informational"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyLoggingHostname, + (VerifyLoggingHostname, "failure"): { "eos_data": [ {"hostname": "NW-CORE", "fqdn": "NW-CORE.example.org"}, "", - "2023-05-10T13:54:21.463497-05:00 NW-CORE ConfigAgent: %SYS-6-LOGMSG_NOTICE: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingLogsHostname validation\n", + "2023-05-10T13:54:21.463497-05:00 NW-CORE ConfigAgent: %SYS-6-LOGMSG_NOTICE: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingLogsHostname validation\n", ], "inputs": {"severity_level": "notifications"}, - "expected": {"result": "failure", "messages": ["Logs are not generated with the device FQDN"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Logs are not generated with the device FQDN"]}, }, - { - "name": "success-negative-offset", - "test": VerifyLoggingTimestamp, + (VerifyLoggingTimestamp, "success-negative-offset"): { "eos_data": [ "", - "2023-05-10T15:41:44.680813-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingTimestamp validation\n" - "2023-05-10T15:42:44.680813-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Other log\n", + "2023-05-10T15:41:44.680813-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingTimestamp validation\n2023-05-10T15:42:44.680813-05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Other log\n", ], "inputs": {"severity_level": "informational"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-positive-offset", - "test": VerifyLoggingTimestamp, + (VerifyLoggingTimestamp, "success-positive-offset"): { "eos_data": [ "", - "2023-05-10T15:41:44.680813+05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingTimestamp validation\n" - "2023-05-10T15:42:44.680813+05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: " - "Other log\n", + "2023-05-10T15:41:44.680813+05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingTimestamp validation\n2023-05-10T15:42:44.680813+05:00 NW-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_INFO: Other log\n", ], "inputs": {"severity_level": "informational"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyLoggingTimestamp, + (VerifyLoggingTimestamp, "failure"): { "eos_data": [ "", - "May 10 13:54:22 NE-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_ALERT: " - "Message from arista on command-api (10.22.1.107): ANTA VerifyLoggingTimestamp validation\n", + "May 10 13:54:22 NE-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_ALERT: Message from arista on command-api (10.22.1.107):" + " ANTA VerifyLoggingTimestamp validation\n", ], "inputs": {"severity_level": "alerts"}, - "expected": {"result": "failure", "messages": ["Logs are not generated with the appropriate timestamp format"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Logs are not generated with the appropriate timestamp format"]}, }, - { - "name": "failure-no-matching-log", - "test": VerifyLoggingTimestamp, - "eos_data": [ - "", - "May 10 13:54:22 NE-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_NOTICE: Message from arista on command-api (10.22.1.107): BLAH\n", - ], + (VerifyLoggingTimestamp, "failure-no-matching-log"): { + "eos_data": ["", "May 10 13:54:22 NE-CORE.example.org ConfigAgent: %SYS-6-LOGMSG_NOTICE: Message from arista on command-api (10.22.1.107): BLAH\n"], "inputs": {"severity_level": "notifications"}, - "expected": {"result": "failure", "messages": ["Logs are not generated with the appropriate timestamp format"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Logs are not generated with the appropriate timestamp format"]}, }, - { - "name": "success", - "test": VerifyLoggingAccounting, + (VerifyLoggingAccounting, "success"): { "eos_data": ["2023 May 10 15:50:31 arista command-api 10.22.1.107 stop service=shell priv-lvl=15 cmd=show aaa accounting logs | tail\n"], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyLoggingAccounting, + (VerifyLoggingAccounting, "failure"): { "eos_data": ["2023 May 10 15:52:26 arista vty14 10.22.1.107 stop service=shell priv-lvl=15 cmd=show bgp summary\n"], - "inputs": None, - "expected": {"result": "failure", "messages": ["AAA accounting logs are not generated"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["AAA accounting logs are not generated"]}, }, - { - "name": "success", - "test": VerifyLoggingErrors, - "eos_data": [""], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyLoggingErrors, + (VerifyLoggingErrors, "success"): {"eos_data": [""], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyLoggingErrors, "failure"): { "eos_data": [ - "Aug 2 19:57:42 DC1-LEAF1A Mlag: %FWK-3-SOCKET_CLOSE_REMOTE: Connection to Mlag (pid:27200) at tbt://192.168.0.1:4432/+n closed by peer (EOF)", + "Aug 2 19:57:42 DC1-LEAF1A Mlag: %FWK-3-SOCKET_CLOSE_REMOTE: Connection to Mlag (pid:27200) at tbt://192.168.0.1:4432/+n closed by peer (EOF)" ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device has reported syslog messages with a severity of ERRORS or higher"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "Device has reported syslog messages with a severity of ERRORS or higher:\nAug 2 19:57:42 DC1-LEAF1A Mlag:" + " %FWK-3-SOCKET_CLOSE_REMOTE: Connection to Mlag (pid:27200) at tbt://192.168.0.1:4432/+n closed by peer (EOF)" + ], + }, }, - { - "name": "success", - "test": VerifySyslogLogging, + (VerifySyslogLogging, "success"): { "eos_data": [ - """Syslog logging: enabled - Buffer logging: level debugging - - External configuration: - active: - inactive: - - Facility Severity Effective Severity - -------------------- ------------- ------------------ - aaa debugging debugging - accounting debugging debugging""", + "Syslog logging: enabled\n Buffer logging: level debugging\n\n External configuration:\n active:\n" + " inactive:\n\n Facility Severity Effective Severity\n" + " -------------------- ------------- ------------------\n aaa debugging" + " debugging\n accounting debugging debugging" ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifySyslogLogging, + (VerifySyslogLogging, "failure"): { "eos_data": [ - """Syslog logging: disabled - Buffer logging: level debugging - Console logging: level errors - Persistent logging: disabled - Monitor logging: level errors - - External configuration: - active: - inactive: - - Facility Severity Effective Severity - -------------------- ------------- ------------------ - aaa debugging debugging - accounting debugging debugging""", + "Syslog logging: disabled\n Buffer logging: level debugging\n Console logging: level errors\n" + " Persistent logging: disabled\n Monitor logging: level errors\n\n External configuration:\n" + " active:\n inactive:\n\n Facility Severity Effective Severity\n" + " -------------------- ------------- ------------------\n aaa debugging" + " debugging\n accounting debugging debugging" ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Syslog logging is disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Syslog logging is disabled"]}, }, - { - "name": "success", - "test": VerifyLoggingEntries, + (VerifyLoggingEntries, "success"): { "eos_data": [ - """Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-6-TERMINATE_RUNNING_PROCESS: Terminating deconfigured/reconfigured process 'SystemInitMonitor' (PID=859) - Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-6-PROCESS_TERMINATED: 'SystemInitMonitor' (PID=859, status=9) has terminated.""", - """Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-7-WORKER_WARMSTART_DONE: ProcMgr worker warm start done. (PID=547)""", + "Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-6-TERMINATE_RUNNING_PROCESS: Terminating deconfigured/reconfigured process 'SystemInitMonitor'" + " (PID=859)\n Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-6-PROCESS_TERMINATED: 'SystemInitMonitor' (PID=859, status=9) has terminated.", + "Mar 13 04:10:45 s1-leaf1 ProcMgr: %PROCMGR-7-WORKER_WARMSTART_DONE: ProcMgr worker warm start done. (PID=547)", ], "inputs": { "logging_entries": [ @@ -334,17 +224,15 @@ DATA: list[dict[str, Any]] = [ {"regex_match": ".*ProcMgr worker warm start.*", "last_number_messages": 2, "severity_level": "debugging"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-log-str-not-found", - "test": VerifyLoggingEntries, + (VerifyLoggingEntries, "failure-log-str-not-found"): { "eos_data": [ - """Mar 12 04:34:01 s1-leaf1 ProcMgr: %PROCMGR-7-WORKER_WARMSTART_DONE: ProcMgr worker warm start done. (PID=559) -Mar 12 04:34:01 s1-leaf1 ProcMgr: %PROCMGR-6-PROCESS_TERMINATED: 'SystemInitMonitor' (PID=867, status=9) has terminated.""", - """Mar 13 03:58:12 s1-leaf1 ConfigAgent: %SYS-5-CONFIG_SESSION_ABORTED: User cvpsystem aborted - configuration session capiVerify-612-612b34a2ffbf11ef96ba3a348d538ba0 on TerminAttr (localhost) - Mar 13 04:10:45 s1-leaf1 SystemInitMonitor: %SYS-5-SYSTEM_INITIALIZED: System is initialized""", + "Mar 12 04:34:01 s1-leaf1 ProcMgr: %PROCMGR-7-WORKER_WARMSTART_DONE: ProcMgr worker warm start done. (PID=559)\nMar 12 04:34:01 " + "s1-leaf1 ProcMgr: %PROCMGR-6-PROCESS_TERMINATED: 'SystemInitMonitor' (PID=867, status=9) has terminated.", + "Mar 13 03:58:12 s1-leaf1 ConfigAgent: %SYS-5-CONFIG_SESSION_ABORTED: User cvpsystem aborted\n " + "configuration session capiVerify-612-612b34a2ffbf11ef96ba3a348d538ba0 on TerminAttr (localhost)\n " + "Mar 13 04:10:45 s1-leaf1 SystemInitMonitor: %SYS-5-SYSTEM_INITIALIZED: System is initialized", ], "inputs": { "logging_entries": [ @@ -353,11 +241,11 @@ Mar 12 04:34:01 s1-leaf1 ProcMgr: %PROCMGR-6-PROCESS_TERMINATED: 'SystemInitMoni ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Pattern: .ACCOUNTING-5-EXEC: cvpadmin ssh. - Not found in last 3 informational log entries", - "Pattern: .*ProcMgr worker warm start.* - Not found in last 10 debugging log entries", + "Pattern: `.ACCOUNTING-5-EXEC: cvpadmin ssh.` - Not found in last 3 informational log entries", + "Pattern: `.*ProcMgr worker warm start.*` - Not found in last 10 debugging log entries", ], }, }, -] +} diff --git a/tests/units/anta_tests/test_mlag.py b/tests/units/anta_tests/test_mlag.py index c49e0f7..20fba6d 100644 --- a/tests/units/anta_tests/test_mlag.py +++ b/tests/units/anta_tests/test_mlag.py @@ -5,196 +5,103 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.mlag import VerifyMlagConfigSanity, VerifyMlagDualPrimary, VerifyMlagInterfaces, VerifyMlagPrimaryPriority, VerifyMlagReloadDelay, VerifyMlagStatus from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyMlagStatus, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyMlagStatus, "success"): { "eos_data": [{"state": "active", "negStatus": "connected", "peerLinkStatus": "up", "localIntfStatus": "up"}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped", - "test": VerifyMlagStatus, - "eos_data": [ - { - "state": "disabled", - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + (VerifyMlagStatus, "skipped"): { + "eos_data": [{"state": "disabled"}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure-negotiation-status", - "test": VerifyMlagStatus, + (VerifyMlagStatus, "failure-negotiation-status"): { "eos_data": [{"state": "active", "negStatus": "connecting", "peerLinkStatus": "up", "localIntfStatus": "up"}], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["MLAG negotiation status mismatch - Expected: connected Actual: connecting"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MLAG negotiation status mismatch - Expected: connected Actual: connecting"]}, }, - { - "name": "failure-local-interface", - "test": VerifyMlagStatus, + (VerifyMlagStatus, "failure-local-interface"): { "eos_data": [{"state": "active", "negStatus": "connected", "peerLinkStatus": "up", "localIntfStatus": "down"}], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Operational state of the MLAG local interface is not correct - Expected: up Actual: down"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Operational state of the MLAG local interface is not correct - Expected: up Actual: down"]}, }, - { - "name": "failure-peer-link", - "test": VerifyMlagStatus, + (VerifyMlagStatus, "failure-peer-link"): { "eos_data": [{"state": "active", "negStatus": "connected", "peerLinkStatus": "down", "localIntfStatus": "up"}], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Operational state of the MLAG peer link is not correct - Expected: up Actual: down"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Operational state of the MLAG peer link is not correct - Expected: up Actual: down"]}, }, - { - "name": "success", - "test": VerifyMlagInterfaces, - "eos_data": [ - { - "state": "active", - "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 0, "Active-full": 1}, - }, - ], - "inputs": None, - "expected": {"result": "success"}, + (VerifyMlagInterfaces, "success"): { + "eos_data": [{"state": "active", "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 0, "Active-full": 1}}], + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped", - "test": VerifyMlagInterfaces, - "eos_data": [ - { - "state": "disabled", - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + (VerifyMlagInterfaces, "skipped"): { + "eos_data": [{"state": "disabled"}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure-active-partial", - "test": VerifyMlagInterfaces, - "eos_data": [ - { - "state": "active", - "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 1, "Active-full": 1}, - }, - ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["MLAG status is not ok - Inactive Ports: 0 Partial Active Ports: 1"], - }, + (VerifyMlagInterfaces, "failure-active-partial"): { + "eos_data": [{"state": "active", "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 0, "Active-partial": 1, "Active-full": 1}}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MLAG status is not ok - Inactive Ports: 0 Partial Active Ports: 1"]}, }, - { - "name": "failure-inactive", - "test": VerifyMlagInterfaces, - "eos_data": [ - { - "state": "active", - "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 1, "Active-partial": 1, "Active-full": 1}, - }, - ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["MLAG status is not ok - Inactive Ports: 1 Partial Active Ports: 1"], - }, + (VerifyMlagInterfaces, "failure-inactive"): { + "eos_data": [{"state": "active", "mlagPorts": {"Disabled": 0, "Configured": 0, "Inactive": 1, "Active-partial": 1, "Active-full": 1}}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MLAG status is not ok - Inactive Ports: 1 Partial Active Ports: 1"]}, }, - { - "name": "success", - "test": VerifyMlagConfigSanity, + (VerifyMlagConfigSanity, "success"): { "eos_data": [{"globalConfiguration": {}, "interfaceConfiguration": {}, "mlagActive": True, "mlagConnected": True}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped", - "test": VerifyMlagConfigSanity, - "eos_data": [ - { - "mlagActive": False, - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + (VerifyMlagConfigSanity, "skipped"): { + "eos_data": [{"mlagActive": False}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure-global", - "test": VerifyMlagConfigSanity, + (VerifyMlagConfigSanity, "failure-global"): { "eos_data": [ { "globalConfiguration": {"mlag": {"globalParameters": {"dual-primary-detection-delay": {"localValue": "0", "peerValue": "200"}}}}, "interfaceConfiguration": {}, "mlagActive": True, "mlagConnected": True, - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["MLAG config-sanity found in global configuration"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MLAG config-sanity found in global configuration"]}, }, - { - "name": "failure-interface", - "test": VerifyMlagConfigSanity, + (VerifyMlagConfigSanity, "failure-interface"): { "eos_data": [ { "globalConfiguration": {}, "interfaceConfiguration": {"trunk-native-vlan mlag30": {"interface": {"Port-Channel30": {"localValue": "123", "peerValue": "3700"}}}}, "mlagActive": True, "mlagConnected": True, - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["MLAG config-sanity found in interface configuration"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MLAG config-sanity found in interface configuration"]}, }, - { - "name": "success", - "test": VerifyMlagReloadDelay, + (VerifyMlagReloadDelay, "success"): { "eos_data": [{"state": "active", "reloadDelay": 300, "reloadDelayNonMlag": 330}], "inputs": {"reload_delay": 300, "reload_delay_non_mlag": 330}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped-disabled", - "test": VerifyMlagReloadDelay, - "eos_data": [ - { - "state": "disabled", - }, - ], + (VerifyMlagReloadDelay, "skipped-disabled"): { + "eos_data": [{"state": "disabled"}], "inputs": {"reload_delay": 300, "reload_delay_non_mlag": 330}, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure", - "test": VerifyMlagReloadDelay, + (VerifyMlagReloadDelay, "failure"): { "eos_data": [{"state": "active", "reloadDelay": 400, "reloadDelayNonMlag": 430}], "inputs": {"reload_delay": 300, "reload_delay_non_mlag": 330}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["MLAG reload-delay mismatch - Expected: 300s Actual: 400s", "Delay for non-MLAG ports mismatch - Expected: 330s Actual: 430s"], }, }, - { - "name": "success", - "test": VerifyMlagDualPrimary, + (VerifyMlagDualPrimary, "success"): { "eos_data": [ { "state": "active", @@ -203,38 +110,22 @@ DATA: list[dict[str, Any]] = [ "dualPrimaryMlagRecoveryDelay": 60, "dualPrimaryNonMlagRecoveryDelay": 0, "detail": {"dualPrimaryDetectionDelay": 200, "dualPrimaryAction": "none"}, - }, + } ], "inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped-disabled", - "test": VerifyMlagDualPrimary, - "eos_data": [ - { - "state": "disabled", - }, - ], + (VerifyMlagDualPrimary, "skipped-disabled"): { + "eos_data": [{"state": "disabled"}], "inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0}, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure-disabled", - "test": VerifyMlagDualPrimary, - "eos_data": [ - { - "state": "active", - "dualPrimaryDetectionState": "disabled", - "dualPrimaryPortsErrdisabled": False, - }, - ], + (VerifyMlagDualPrimary, "failure-disabled"): { + "eos_data": [{"state": "active", "dualPrimaryDetectionState": "disabled", "dualPrimaryPortsErrdisabled": False}], "inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0}, - "expected": {"result": "failure", "messages": ["Dual-primary detection is disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dual-primary detection is disabled"]}, }, - { - "name": "failure-wrong-timers", - "test": VerifyMlagDualPrimary, + (VerifyMlagDualPrimary, "failure-wrong-timers"): { "eos_data": [ { "state": "active", @@ -243,20 +134,18 @@ DATA: list[dict[str, Any]] = [ "dualPrimaryMlagRecoveryDelay": 160, "dualPrimaryNonMlagRecoveryDelay": 0, "detail": {"dualPrimaryDetectionDelay": 300, "dualPrimaryAction": "none"}, - }, + } ], "inputs": {"detection_delay": 200, "errdisabled": False, "recovery_delay": 60, "recovery_delay_non_mlag": 0}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Dual-primary detection delay mismatch - Expected: 200 Actual: 300", "Dual-primary MLAG recovery delay mismatch - Expected: 60 Actual: 160", ], }, }, - { - "name": "failure-wrong-action", - "test": VerifyMlagDualPrimary, + (VerifyMlagDualPrimary, "failure-wrong-action"): { "eos_data": [ { "state": "active", @@ -265,17 +154,12 @@ DATA: list[dict[str, Any]] = [ "dualPrimaryMlagRecoveryDelay": 60, "dualPrimaryNonMlagRecoveryDelay": 0, "detail": {"dualPrimaryDetectionDelay": 200, "dualPrimaryAction": "none"}, - }, + } ], "inputs": {"detection_delay": 200, "errdisabled": True, "recovery_delay": 60, "recovery_delay_non_mlag": 0}, - "expected": { - "result": "failure", - "messages": ["Dual-primary action mismatch - Expected: errdisableAllInterfaces Actual: none"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dual-primary action mismatch - Expected: errdisableAllInterfaces Actual: none"]}, }, - { - "name": "failure-wrong-non-mlag-delay", - "test": VerifyMlagDualPrimary, + (VerifyMlagDualPrimary, "failure-wrong-non-mlag-delay"): { "eos_data": [ { "state": "active", @@ -284,67 +168,32 @@ DATA: list[dict[str, Any]] = [ "dualPrimaryMlagRecoveryDelay": 60, "dualPrimaryNonMlagRecoveryDelay": 120, "detail": {"dualPrimaryDetectionDelay": 200, "dualPrimaryAction": "errdisableAllInterfaces"}, - }, + } ], "inputs": {"detection_delay": 200, "errdisabled": True, "recovery_delay": 60, "recovery_delay_non_mlag": 60}, - "expected": { - "result": "failure", - "messages": ["Dual-primary non MLAG recovery delay mismatch - Expected: 60 Actual: 120"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dual-primary non MLAG recovery delay mismatch - Expected: 60 Actual: 120"]}, }, - { - "name": "success", - "test": VerifyMlagPrimaryPriority, - "eos_data": [ - { - "state": "active", - "detail": {"mlagState": "primary", "primaryPriority": 32767}, - } - ], - "inputs": { - "primary_priority": 32767, - }, - "expected": {"result": "success"}, - }, - { - "name": "skipped-disabled", - "test": VerifyMlagPrimaryPriority, - "eos_data": [ - { - "state": "disabled", - } - ], + (VerifyMlagPrimaryPriority, "success"): { + "eos_data": [{"state": "active", "detail": {"mlagState": "primary", "primaryPriority": 32767}}], "inputs": {"primary_priority": 32767}, - "expected": {"result": "skipped", "messages": ["MLAG is disabled"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-primary", - "test": VerifyMlagPrimaryPriority, - "eos_data": [ - { - "state": "active", - "detail": {"mlagState": "secondary", "primaryPriority": 32767}, - } - ], + (VerifyMlagPrimaryPriority, "skipped-disabled"): { + "eos_data": [{"state": "disabled"}], "inputs": {"primary_priority": 32767}, - "expected": { - "result": "failure", - "messages": ["The device is not set as MLAG primary"], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["MLAG is disabled"]}, }, - { - "name": "failure-incorrect-priority", - "test": VerifyMlagPrimaryPriority, - "eos_data": [ - { - "state": "active", - "detail": {"mlagState": "secondary", "primaryPriority": 32767}, - } - ], + (VerifyMlagPrimaryPriority, "failure-not-primary"): { + "eos_data": [{"state": "active", "detail": {"mlagState": "secondary", "primaryPriority": 32767}}], + "inputs": {"primary_priority": 32767}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The device is not set as MLAG primary"]}, + }, + (VerifyMlagPrimaryPriority, "failure-incorrect-priority"): { + "eos_data": [{"state": "active", "detail": {"mlagState": "secondary", "primaryPriority": 32767}}], "inputs": {"primary_priority": 1}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["The device is not set as MLAG primary", "MLAG primary priority mismatch - Expected: 1 Actual: 32767"], }, }, -] +} diff --git a/tests/units/anta_tests/test_multicast.py b/tests/units/anta_tests/test_multicast.py index 7e33e26..a49344f 100644 --- a/tests/units/anta_tests/test_multicast.py +++ b/tests/units/anta_tests/test_multicast.py @@ -5,15 +5,19 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.multicast import VerifyIGMPSnoopingGlobal, VerifyIGMPSnoopingVlans from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success-enabled", - "test": VerifyIGMPSnoopingVlans, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyIGMPSnoopingVlans, "success-enabled"): { "eos_data": [ { "reportFlooding": "disabled", @@ -45,14 +49,12 @@ DATA: list[dict[str, Any]] = [ "robustness": 2, "immediateLeave": "enabled", "reportFloodingSwitchPorts": [], - }, + } ], "inputs": {"vlans": {1: True, 42: True}}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-disabled", - "test": VerifyIGMPSnoopingVlans, + (VerifyIGMPSnoopingVlans, "success-disabled"): { "eos_data": [ { "reportFlooding": "disabled", @@ -68,19 +70,17 @@ DATA: list[dict[str, Any]] = [ "maxGroups": 65534, "immediateLeave": "default", "floodingTraffic": True, - }, + } }, "robustness": 2, "immediateLeave": "enabled", "reportFloodingSwitchPorts": [], - }, + } ], "inputs": {"vlans": {42: False}}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-missing-vlan", - "test": VerifyIGMPSnoopingVlans, + (VerifyIGMPSnoopingVlans, "failure-missing-vlan"): { "eos_data": [ { "reportFlooding": "disabled", @@ -96,22 +96,20 @@ DATA: list[dict[str, Any]] = [ "maxGroups": 65534, "immediateLeave": "default", "floodingTraffic": True, - }, + } }, "robustness": 2, "immediateLeave": "enabled", "reportFloodingSwitchPorts": [], - }, + } ], "inputs": {"vlans": {1: False, 42: False}}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["VLAN1 - Incorrect IGMP state - Expected: disabled Actual: enabled", "Supplied vlan 42 is not present on the device"], }, }, - { - "name": "failure-wrong-state", - "test": VerifyIGMPSnoopingVlans, + (VerifyIGMPSnoopingVlans, "failure-wrong-state"): { "eos_data": [ { "reportFlooding": "disabled", @@ -127,53 +125,29 @@ DATA: list[dict[str, Any]] = [ "maxGroups": 65534, "immediateLeave": "default", "floodingTraffic": True, - }, + } }, "robustness": 2, "immediateLeave": "enabled", "reportFloodingSwitchPorts": [], - }, + } ], "inputs": {"vlans": {1: True}}, - "expected": {"result": "failure", "messages": ["VLAN1 - Incorrect IGMP state - Expected: enabled Actual: disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN1 - Incorrect IGMP state - Expected: enabled Actual: disabled"]}, }, - { - "name": "success-enabled", - "test": VerifyIGMPSnoopingGlobal, - "eos_data": [ - { - "reportFlooding": "disabled", - "igmpSnoopingState": "enabled", - "robustness": 2, - "immediateLeave": "enabled", - "reportFloodingSwitchPorts": [], - }, - ], + (VerifyIGMPSnoopingGlobal, "success-enabled"): { + "eos_data": [{"reportFlooding": "disabled", "igmpSnoopingState": "enabled", "robustness": 2, "immediateLeave": "enabled", "reportFloodingSwitchPorts": []}], "inputs": {"enabled": True}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-disabled", - "test": VerifyIGMPSnoopingGlobal, - "eos_data": [ - { - "reportFlooding": "disabled", - "igmpSnoopingState": "disabled", - }, - ], + (VerifyIGMPSnoopingGlobal, "success-disabled"): { + "eos_data": [{"reportFlooding": "disabled", "igmpSnoopingState": "disabled"}], "inputs": {"enabled": False}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-state", - "test": VerifyIGMPSnoopingGlobal, - "eos_data": [ - { - "reportFlooding": "disabled", - "igmpSnoopingState": "disabled", - }, - ], + (VerifyIGMPSnoopingGlobal, "failure-wrong-state"): { + "eos_data": [{"reportFlooding": "disabled", "igmpSnoopingState": "disabled"}], "inputs": {"enabled": True}, - "expected": {"result": "failure", "messages": ["IGMP state is not valid - Expected: enabled Actual: disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["IGMP state is not valid - Expected: enabled Actual: disabled"]}, }, -] +} diff --git a/tests/units/anta_tests/test_path_selection.py b/tests/units/anta_tests/test_path_selection.py index 45b0e24..c75aa90 100644 --- a/tests/units/anta_tests/test_path_selection.py +++ b/tests/units/anta_tests/test_path_selection.py @@ -5,101 +5,67 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.path_selection import VerifyPathsHealth, VerifySpecificPath from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyPathsHealth, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyPathsHealth, "success"): { "eos_data": [ { "dpsPeers": { "10.255.0.1": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path3": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path4": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path3": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}}}, + "mpls": {"dpsPaths": {"path4": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}}}, + } }, "10.255.0.2": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path1": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path2": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path1": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}}}, + "mpls": {"dpsPaths": {"path2": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}}}, + } }, } - }, + } ], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-peer", - "test": VerifyPathsHealth, - "eos_data": [ - {"dpsPeers": {}}, - ], + (VerifyPathsHealth, "failure-no-peer"): { + "eos_data": [{"dpsPeers": {}}], "inputs": {}, - "expected": {"result": "failure", "messages": ["No path configured for router path-selection"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No path configured for router path-selection"]}, }, - { - "name": "failure-not-established", - "test": VerifyPathsHealth, + (VerifyPathsHealth, "failure-not-established"): { "eos_data": [ { "dpsPeers": { "10.255.0.1": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path3": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path4": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path3": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}}}, + "mpls": {"dpsPaths": {"path4": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}}}, + } }, "10.255.0.2": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path1": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path2": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path1": {"state": "ipsecEstablished", "dpsSessions": {"0": {"active": True}}}}}, + "mpls": {"dpsPaths": {"path2": {"state": "ipsecPending", "dpsSessions": {"0": {"active": False}}}}}, + } }, } - }, + } ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.255.0.1 Path Group: internet - Invalid path state - Expected: ipsecEstablished, routeResolved Actual: ipsecPending", "Peer: 10.255.0.1 Path Group: mpls - Invalid path state - Expected: ipsecEstablished, routeResolved Actual: ipsecPending", @@ -107,46 +73,28 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-inactive", - "test": VerifyPathsHealth, + (VerifyPathsHealth, "failure-inactive"): { "eos_data": [ { "dpsPeers": { "10.255.0.1": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path3": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path4": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path3": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}}}, + "mpls": {"dpsPaths": {"path4": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}}}, + } }, "10.255.0.2": { "dpsGroups": { - "internet": { - "dpsPaths": { - "path1": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}, - }, - }, - "mpls": { - "dpsPaths": { - "path2": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}, - }, - }, - }, + "internet": {"dpsPaths": {"path1": {"state": "routeResolved", "dpsSessions": {"0": {"active": True}}}}}, + "mpls": {"dpsPaths": {"path2": {"state": "routeResolved", "dpsSessions": {"0": {"active": False}}}}}, + } }, } - }, + } ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.255.0.1 Path Group internet - Telemetry state inactive", "Peer: 10.255.0.1 Path Group mpls - Telemetry state inactive", @@ -154,9 +102,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifySpecificPath, + (VerifySpecificPath, "success"): { "eos_data": [ { "dpsPeers": { @@ -200,21 +146,10 @@ DATA: list[dict[str, Any]] = [ {"peer": "10.255.0.2", "path_group": "mpls", "source_address": "172.18.13.2", "destination_address": "172.18.15.2"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-expected-path-group-not-found", - "test": VerifySpecificPath, - "eos_data": [ - { - "dpsPeers": { - "10.255.0.2": { - "dpsGroups": {"internet": {}}, - }, - "10.255.0.1": {"peerName": "", "dpsGroups": {"mpls": {}}}, - } - } - ], + (VerifySpecificPath, "failure-expected-path-group-not-found"): { + "eos_data": [{"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": "100.64.3.2", "destination_address": "100.64.1.2"}, @@ -222,30 +157,27 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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, + (VerifySpecificPath, "failure-no-router-path-configured"): { "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"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Router path-selection not configured"]}, }, - { - "name": "failure-no-specific-peer-configured", - "test": VerifySpecificPath, + (VerifySpecificPath, "failure-no-specific-peer-configured"): { "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"]}, + "expected": { + "result": AntaTestStatus.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, + (VerifySpecificPath, "failure-not-established"): { "eos_data": [ { "dpsPeers": { @@ -285,18 +217,16 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Peer: 10.255.0.1 PathGroup: internet Source: 172.18.3.2 Destination: 172.18.5.2 - Invalid state path - Expected: ipsecEstablished, routeResolved " - "Actual: ipsecPending", - "Peer: 10.255.0.2 PathGroup: mpls Source: 172.18.13.2 Destination: 172.18.15.2 - Invalid state path - Expected: ipsecEstablished, routeResolved " - "Actual: ipsecPending", + "Peer: 10.255.0.1 PathGroup: internet Source: 172.18.3.2 Destination: 172.18.5.2 - Invalid state path - " + "Expected: ipsecEstablished, routeResolved Actual: ipsecPending", + "Peer: 10.255.0.2 PathGroup: mpls Source: 172.18.13.2 Destination: 172.18.15.2 - Invalid state path - " + "Expected: ipsecEstablished, routeResolved Actual: ipsecPending", ], }, }, - { - "name": "failure-inactive", - "test": VerifySpecificPath, + (VerifySpecificPath, "failure-inactive"): { "eos_data": [ { "dpsPeers": { @@ -333,16 +263,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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-source-destination-not-configured", - "test": VerifySpecificPath, + (VerifySpecificPath, "failure-source-destination-not-configured"): { "eos_data": [ { "dpsPeers": { @@ -374,11 +302,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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", ], }, }, -] +} diff --git a/tests/units/anta_tests/test_profiles.py b/tests/units/anta_tests/test_profiles.py index 4277a95..f18c125 100644 --- a/tests/units/anta_tests/test_profiles.py +++ b/tests/units/anta_tests/test_profiles.py @@ -5,42 +5,40 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.profiles import VerifyTcamProfile, VerifyUnifiedForwardingTableMode from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyUnifiedForwardingTableMode, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyUnifiedForwardingTableMode, "success"): { "eos_data": [{"uftMode": "2", "urpfEnabled": False, "chipModel": "bcm56870", "l2TableSize": 163840, "l3TableSize": 147456, "lpmTableSize": 32768}], "inputs": {"mode": 2}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyUnifiedForwardingTableMode, + (VerifyUnifiedForwardingTableMode, "failure"): { "eos_data": [{"uftMode": "2", "urpfEnabled": False, "chipModel": "bcm56870", "l2TableSize": 163840, "l3TableSize": 147456, "lpmTableSize": 32768}], "inputs": {"mode": 3}, - "expected": {"result": "failure", "messages": ["Not running the correct UFT mode - Expected: 3 Actual: 2"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Not running the correct UFT mode - Expected: 3 Actual: 2"]}, }, - { - "name": "success", - "test": VerifyTcamProfile, + (VerifyTcamProfile, "success"): { "eos_data": [ - {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "test", "mode": "tcam"}}, "lastProgrammingStatus": {}}, + {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "test", "mode": "tcam"}}, "lastProgrammingStatus": {}} ], "inputs": {"profile": "test"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyTcamProfile, + (VerifyTcamProfile, "failure"): { "eos_data": [ - {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "default", "mode": "tcam"}}, "lastProgrammingStatus": {}}, + {"pmfProfiles": {"FixedSystem": {"config": "test", "configType": "System Profile", "status": "default", "mode": "tcam"}}, "lastProgrammingStatus": {}} ], "inputs": {"profile": "test"}, - "expected": {"result": "failure", "messages": ["Incorrect profile running on device: default"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Incorrect profile running on device: default"]}, }, -] +} diff --git a/tests/units/anta_tests/test_ptp.py b/tests/units/anta_tests/test_ptp.py index 8ff2d6c..86d3911 100644 --- a/tests/units/anta_tests/test_ptp.py +++ b/tests/units/anta_tests/test_ptp.py @@ -5,15 +5,19 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.ptp import VerifyPtpGMStatus, VerifyPtpLockStatus, VerifyPtpModeStatus, VerifyPtpOffset, VerifyPtpPortModeStatus from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyPtpModeStatus, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyPtpModeStatus, "success"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -31,26 +35,17 @@ DATA: list[dict[str, Any]] = [ "ptpIntfSummaries": {}, } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyPtpModeStatus, + (VerifyPtpModeStatus, "failure"): { "eos_data": [{"ptpMode": "ptpDisabled", "ptpIntfSummaries": {}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Not configured as a PTP Boundary Clock - Actual: ptpDisabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Not configured as a PTP Boundary Clock - Actual: ptpDisabled"]}, }, - { - "name": "skipped", - "test": VerifyPtpModeStatus, + (VerifyPtpModeStatus, "skipped"): { "eos_data": [{"ptpIntfSummaries": {}}], - "inputs": None, - "expected": {"result": "skipped", "messages": ["PTP is not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["PTP is not configured"]}, }, - { - "name": "success", - "test": VerifyPtpGMStatus, + (VerifyPtpGMStatus, "success"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -72,11 +67,9 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyPtpGMStatus, + (VerifyPtpGMStatus, "failure"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -97,22 +90,16 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"}, "expected": { - "result": "failure", - "messages": [ - "The device is locked to the incorrect Grandmaster - Expected: 0xec:46:70:ff:fe:00:ff:a8 Actual: 0x00:1c:73:ff:ff:0a:00:01", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["The device is locked to the incorrect Grandmaster - Expected: 0xec:46:70:ff:fe:00:ff:a8 Actual: 0x00:1c:73:ff:ff:0a:00:01"], }, }, - { - "name": "skipped", - "test": VerifyPtpGMStatus, + (VerifyPtpGMStatus, "skipped"): { "eos_data": [{"ptpIntfSummaries": {}}], "inputs": {"gmid": "0xec:46:70:ff:fe:00:ff:a8"}, - "expected": {"result": "skipped", "messages": ["PTP is not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["PTP is not configured"]}, }, - { - "name": "success", - "test": VerifyPtpLockStatus, + (VerifyPtpLockStatus, "success"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -133,12 +120,9 @@ DATA: list[dict[str, Any]] = [ }, } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyPtpLockStatus, + (VerifyPtpLockStatus, "failure"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -157,24 +141,13 @@ DATA: list[dict[str, Any]] = [ }, } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Lock is more than 60s old - Actual: 157s"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Lock is more than 60s old - Actual: 157s"]}, }, - { - "name": "skipped", - "test": VerifyPtpLockStatus, + (VerifyPtpLockStatus, "skipped"): { "eos_data": [{"ptpIntfSummaries": {}}], - "inputs": None, - "expected": { - "result": "skipped", - "messages": [ - "PTP is not configured", - ], - }, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["PTP is not configured"]}, }, - { - "name": "success", - "test": VerifyPtpOffset, + (VerifyPtpOffset, "success"): { "eos_data": [ { "monitorEnabled": True, @@ -201,12 +174,9 @@ DATA: list[dict[str, Any]] = [ ], } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyPtpOffset, + (VerifyPtpOffset, "failure"): { "eos_data": [ { "monitorEnabled": True, @@ -233,29 +203,16 @@ DATA: list[dict[str, Any]] = [ ], } ], - "inputs": None, "expected": { - "result": "failure", - "messages": [ - "Interface: Ethernet27/1 - Timing offset from master is greater than +/- 1000ns: Actual: 1200, -1300", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Interface: Ethernet27/1 - Timing offset from master is greater than +/- 1000ns: Actual: 1200, -1300"], }, }, - { - "name": "skipped", - "test": VerifyPtpOffset, - "eos_data": [ - { - "monitorEnabled": True, - "ptpMonitorData": [], - }, - ], - "inputs": None, - "expected": {"result": "skipped", "messages": ["PTP is not configured"]}, + (VerifyPtpOffset, "skipped"): { + "eos_data": [{"monitorEnabled": True, "ptpMonitorData": []}], + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["PTP is not configured"]}, }, - { - "name": "success", - "test": VerifyPtpPortModeStatus, + (VerifyPtpPortModeStatus, "success"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -293,19 +250,13 @@ DATA: list[dict[str, Any]] = [ }, } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-interfaces", - "test": VerifyPtpPortModeStatus, + (VerifyPtpPortModeStatus, "failure-no-interfaces"): { "eos_data": [{"ptpIntfSummaries": {}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["No interfaces are PTP enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No interfaces are PTP enabled"]}, }, - { - "name": "failure-invalid-state", - "test": VerifyPtpPortModeStatus, + (VerifyPtpPortModeStatus, "failure-invalid-state"): { "eos_data": [ { "ptpMode": "ptpBoundaryClock", @@ -336,7 +287,6 @@ DATA: list[dict[str, Any]] = [ }, } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["The following interface(s) are not in a valid PTP state: Ethernet53, Ethernet1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following interface(s) are not in a valid PTP state: Ethernet53, Ethernet1"]}, }, -] +} diff --git a/tests/units/anta_tests/test_security.py b/tests/units/anta_tests/test_security.py index 6f4d370..f992f41 100644 --- a/tests/units/anta_tests/test_security.py +++ b/tests/units/anta_tests/test_security.py @@ -5,11 +5,14 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any import pytest from pydantic import ValidationError +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.security import ( VerifyAPIHttpsSSL, VerifyAPIHttpStatus, @@ -29,107 +32,75 @@ from anta.tests.security import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifySSHStatus, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifySSHStatus, "success"): { "eos_data": ["SSHD status for Default VRF is disabled\nSSH connection limit is 50\nSSH per host connection limit is 20\nFIPS status: disabled\n\n"], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "error-missing-ssh-status", - "test": VerifySSHStatus, + (VerifySSHStatus, "error-missing-ssh-status"): { "eos_data": ["SSH per host connection limit is 20\nFIPS status: disabled\n\n"], - "inputs": None, - "expected": {"result": "failure", "messages": ["Could not find SSH status in returned output"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Could not find SSH status in returned output"]}, }, - { - "name": "failure-ssh-enabled", - "test": VerifySSHStatus, + (VerifySSHStatus, "failure-ssh-enabled"): { "eos_data": ["SSHD status for Default VRF is enabled\nSSH connection limit is 50\nSSH per host connection limit is 20\nFIPS status: disabled\n\n"], - "inputs": None, - "expected": {"result": "failure", "messages": ["SSHD status for Default VRF is enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SSHD status for Default VRF is enabled"]}, }, - { - "name": "success-4.32", - "test": VerifySSHStatus, + (VerifySSHStatus, "success-4.32"): { "eos_data": [ "User certificate authentication methods: none (neither trusted CA nor SSL profile configured)\n" "SSHD status for Default VRF: disabled\nSSH connection limit: 50\nSSH per host connection limit: 20\nFIPS status: disabled\n\n" ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-ssh-enabled-4.32", - "test": VerifySSHStatus, + (VerifySSHStatus, "failure-ssh-enabled-4.32"): { "eos_data": [ - "User certificate authentication methods: none (neither trusted CA nor SSL profile configured)\n" - "SSHD status for Default VRF: enabled\nSSH connection limit: 50\nSSH per host connection limit: 20\nFIPS status: disabled\n\n" + "User certificate authentication methods: none (neither trusted CA nor SSL profile configured)\nSSHD status for Default VRF: enabled\n" + "SSH connection limit: 50\nSSH per host connection limit: 20\nFIPS status: disabled\n\n" ], - "inputs": None, - "expected": {"result": "failure", "messages": ["SSHD status for Default VRF: enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SSHD status for Default VRF: enabled"]}, }, - { - "name": "success", - "test": VerifySSHIPv4Acl, + (VerifySSHIPv4Acl, "success"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_SSH", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifySSHIPv4Acl, + (VerifySSHIPv4Acl, "failure-wrong-number"): { "eos_data": [{"ipAclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - SSH IPv4 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - SSH IPv4 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifySSHIPv4Acl, + (VerifySSHIPv4Acl, "failure-wrong-vrf"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_SSH", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following SSH IPv4 ACL(s) not configured or active: ACL_IPV4_SSH"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following SSH IPv4 ACL(s) not configured or active: ACL_IPV4_SSH"]}, }, - { - "name": "success", - "test": VerifySSHIPv6Acl, + (VerifySSHIPv6Acl, "success"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_SSH", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifySSHIPv6Acl, + (VerifySSHIPv6Acl, "failure-wrong-number"): { "eos_data": [{"ipv6AclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - SSH IPv6 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - SSH IPv6 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifySSHIPv6Acl, + (VerifySSHIPv6Acl, "failure-wrong-vrf"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_SSH", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following SSH IPv6 ACL(s) not configured or active: ACL_IPV6_SSH"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following SSH IPv6 ACL(s) not configured or active: ACL_IPV6_SSH"]}, }, - { - "name": "success", - "test": VerifyTelnetStatus, + (VerifyTelnetStatus, "success"): { "eos_data": [{"serverState": "disabled", "vrfName": "default", "maxTelnetSessions": 20, "maxTelnetSessionsPerHost": 20}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyTelnetStatus, + (VerifyTelnetStatus, "failure"): { "eos_data": [{"serverState": "enabled", "vrfName": "default", "maxTelnetSessions": 20, "maxTelnetSessionsPerHost": 20}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Telnet status for Default VRF is enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Telnet status for Default VRF is enabled"]}, }, - { - "name": "success", - "test": VerifyAPIHttpStatus, + (VerifyAPIHttpStatus, "success"): { "eos_data": [ { "enabled": True, @@ -139,14 +110,11 @@ DATA: list[dict[str, Any]] = [ "unixSocketServer": {"configured": False, "running": False}, "sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"}, "tlsProtocol": ["1.2"], - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyAPIHttpStatus, + (VerifyAPIHttpStatus, "failure"): { "eos_data": [ { "enabled": True, @@ -156,14 +124,11 @@ DATA: list[dict[str, Any]] = [ "unixSocketServer": {"configured": False, "running": False}, "sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"}, "tlsProtocol": ["1.2"], - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["eAPI HTTP server is enabled globally"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["eAPI HTTP server is enabled globally"]}, }, - { - "name": "success", - "test": VerifyAPIHttpsSSL, + (VerifyAPIHttpsSSL, "success"): { "eos_data": [ { "enabled": True, @@ -173,14 +138,12 @@ DATA: list[dict[str, Any]] = [ "unixSocketServer": {"configured": False, "running": False}, "sslProfile": {"name": "API_SSL_Profile", "configured": True, "state": "valid"}, "tlsProtocol": ["1.2"], - }, + } ], "inputs": {"profile": "API_SSL_Profile"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifyAPIHttpsSSL, + (VerifyAPIHttpsSSL, "failure-not-configured"): { "eos_data": [ { "enabled": True, @@ -189,14 +152,12 @@ DATA: list[dict[str, Any]] = [ "httpsServer": {"configured": True, "running": True, "port": 443}, "unixSocketServer": {"configured": False, "running": False}, "tlsProtocol": ["1.2"], - }, + } ], "inputs": {"profile": "API_SSL_Profile"}, - "expected": {"result": "failure", "messages": ["eAPI HTTPS server SSL profile API_SSL_Profile is not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["eAPI HTTPS server SSL profile API_SSL_Profile is not configured"]}, }, - { - "name": "failure-misconfigured-invalid", - "test": VerifyAPIHttpsSSL, + (VerifyAPIHttpsSSL, "failure-misconfigured-invalid"): { "eos_data": [ { "enabled": True, @@ -206,80 +167,58 @@ DATA: list[dict[str, Any]] = [ "unixSocketServer": {"configured": False, "running": False}, "sslProfile": {"name": "Wrong_SSL_Profile", "configured": True, "state": "valid"}, "tlsProtocol": ["1.2"], - }, + } ], "inputs": {"profile": "API_SSL_Profile"}, - "expected": {"result": "failure", "messages": ["eAPI HTTPS server SSL profile API_SSL_Profile is misconfigured or invalid"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["eAPI HTTPS server SSL profile API_SSL_Profile is misconfigured or invalid"]}, }, - { - "name": "success", - "test": VerifyAPIIPv4Acl, + (VerifyAPIIPv4Acl, "success"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_API", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifyAPIIPv4Acl, + (VerifyAPIIPv4Acl, "failure-wrong-number"): { "eos_data": [{"ipAclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - eAPI IPv4 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - eAPI IPv4 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifyAPIIPv4Acl, + (VerifyAPIIPv4Acl, "failure-wrong-vrf"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_API", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following eAPI IPv4 ACL(s) not configured or active: ACL_IPV4_API"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following eAPI IPv4 ACL(s) not configured or active: ACL_IPV4_API"]}, }, - { - "name": "success", - "test": VerifyAPIIPv6Acl, + (VerifyAPIIPv6Acl, "success"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_API", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifyAPIIPv6Acl, + (VerifyAPIIPv6Acl, "failure-wrong-number"): { "eos_data": [{"ipv6AclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - eAPI IPv6 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - eAPI IPv6 ACL(s) count mismatch - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifyAPIIPv6Acl, + (VerifyAPIIPv6Acl, "failure-wrong-vrf"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_API", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following eAPI IPv6 ACL(s) not configured or active: ACL_IPV6_API"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following eAPI IPv6 ACL(s) not configured or active: ACL_IPV6_API"]}, }, - { - "name": "success", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "success"): { "eos_data": [ { "certificates": { "ARISTA_ROOT_CA.crt": { "subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "RSA", - "size": 4096, - }, + "publicKey": {"encryptionAlgorithm": "RSA", "size": 4096}, }, "ARISTA_SIGNING_CA.crt": { "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "ECDSA", - "size": 256, - }, + "publicKey": {"encryptionAlgorithm": "ECDSA", "size": 256}, }, } }, - { - "utcTime": 1702288467.6736515, - }, + {"utcTime": 1702288467.6736515}, ], "inputs": { "certificates": [ @@ -299,27 +238,20 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-certificate-not-configured", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-certificate-not-configured"): { "eos_data": [ { "certificates": { "ARISTA_SIGNING_CA.crt": { "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "ECDSA", - "size": 256, - }, - }, + "publicKey": {"encryptionAlgorithm": "ECDSA", "size": 256}, + } } }, - { - "utcTime": 1702288467.6736515, - }, + {"utcTime": 1702288467.6736515}, ], "inputs": { "certificates": [ @@ -339,30 +271,20 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": { - "result": "failure", - "messages": ["Certificate: ARISTA_ROOT_CA.crt - Not found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Certificate: ARISTA_ROOT_CA.crt - Not found"]}, }, - { - "name": "failure-certificate-expired", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-certificate-expired"): { "eos_data": [ { "certificates": { "ARISTA_ROOT_CA.crt": { "subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, "notAfter": 1702533518, - "publicKey": { - "encryptionAlgorithm": "RSA", - "size": 4096, - }, - }, + "publicKey": {"encryptionAlgorithm": "RSA", "size": 4096}, + } } }, - { - "utcTime": 1702622372.2240553, - }, + {"utcTime": 1702622372.2240553}, ], "inputs": { "certificates": [ @@ -372,41 +294,28 @@ DATA: list[dict[str, Any]] = [ "common_name": "Arista Networks Internal IT Root Cert Authority", "encryption_algorithm": "RSA", "key_size": 4096, - }, + } ] }, - "expected": { - "result": "failure", - "messages": ["Certificate: ARISTA_ROOT_CA.crt - certificate expired"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Certificate: ARISTA_ROOT_CA.crt - certificate expired"]}, }, - { - "name": "failure-certificate-about-to-expire", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-certificate-about-to-expire"): { "eos_data": [ { "certificates": { "ARISTA_ROOT_CA.crt": { "subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, "notAfter": 1704782709, - "publicKey": { - "encryptionAlgorithm": "RSA", - "size": 4096, - }, + "publicKey": {"encryptionAlgorithm": "RSA", "size": 4096}, }, "ARISTA_SIGNING_CA.crt": { "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, "notAfter": 1705992709, - "publicKey": { - "encryptionAlgorithm": "ECDSA", - "size": 256, - }, + "publicKey": {"encryptionAlgorithm": "ECDSA", "size": 256}, }, } }, - { - "utcTime": 1702622372.2240553, - }, + {"utcTime": 1702622372.2240553}, ], "inputs": { "certificates": [ @@ -427,39 +336,27 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", - "messages": [ - "Certificate: ARISTA_ROOT_CA.crt - set to expire within the threshold - Threshold: 30 days Actual: 25 days", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Certificate: ARISTA_ROOT_CA.crt - set to expire within the threshold - Threshold: 30 days Actual: 25 days"], }, }, - { - "name": "failure-wrong-subject-name", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-wrong-subject-name"): { "eos_data": [ { "certificates": { "ARISTA_ROOT_CA.crt": { "subject": {"commonName": "AristaIT-ICA Networks Internal IT Root Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "RSA", - "size": 4096, - }, + "publicKey": {"encryptionAlgorithm": "RSA", "size": 4096}, }, "ARISTA_SIGNING_CA.crt": { "subject": {"commonName": "Arista ECDSA Issuing Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "ECDSA", - "size": 256, - }, + "publicKey": {"encryptionAlgorithm": "ECDSA", "size": 256}, }, } }, - { - "utcTime": 1702288467.6736515, - }, + {"utcTime": 1702288467.6736515}, ], "inputs": { "certificates": [ @@ -480,42 +377,32 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "Certificate: ARISTA_SIGNING_CA.crt - incorrect common name - Expected: AristaIT-ICA ECDSA Issuing Cert Authority " - "Actual: Arista ECDSA Issuing Cert Authority", - "Certificate: ARISTA_ROOT_CA.crt - incorrect common name - Expected: Arista Networks Internal IT Root Cert Authority " - "Actual: AristaIT-ICA Networks Internal IT Root Cert Authority", + "Certificate: ARISTA_SIGNING_CA.crt - incorrect common name - " + "Expected: AristaIT-ICA ECDSA Issuing Cert Authority Actual: Arista ECDSA Issuing Cert Authority", + "Certificate: ARISTA_ROOT_CA.crt - incorrect common name - Expected: Arista Networks Internal IT " + "Root Cert Authority Actual: AristaIT-ICA Networks Internal IT Root Cert Authority", ], }, }, - { - "name": "failure-wrong-encryption-type-and-size", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-wrong-encryption-type-and-size"): { "eos_data": [ { "certificates": { "ARISTA_ROOT_CA.crt": { "subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "ECDSA", - "size": 256, - }, + "publicKey": {"encryptionAlgorithm": "ECDSA", "size": 256}, }, "ARISTA_SIGNING_CA.crt": { "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, "notAfter": 2127420899, - "publicKey": { - "encryptionAlgorithm": "RSA", - "size": 4096, - }, + "publicKey": {"encryptionAlgorithm": "RSA", "size": 4096}, }, } }, - { - "utcTime": 1702288467.6736515, - }, + {"utcTime": 1702288467.6736515}, ], "inputs": { "certificates": [ @@ -536,7 +423,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Certificate: ARISTA_SIGNING_CA.crt - incorrect encryption algorithm - Expected: ECDSA Actual: RSA", "Certificate: ARISTA_SIGNING_CA.crt - incorrect public key - Expected: 256 Actual: 4096", @@ -545,25 +432,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-missing-algorithm-details", - "test": VerifyAPISSLCertificate, + (VerifyAPISSLCertificate, "failure-missing-algorithm-details"): { "eos_data": [ { "certificates": { - "ARISTA_ROOT_CA.crt": { - "subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, - "notAfter": 2127420899, - }, - "ARISTA_SIGNING_CA.crt": { - "subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, - "notAfter": 2127420899, - }, + "ARISTA_ROOT_CA.crt": {"subject": {"commonName": "Arista Networks Internal IT Root Cert Authority"}, "notAfter": 2127420899}, + "ARISTA_SIGNING_CA.crt": {"subject": {"commonName": "AristaIT-ICA ECDSA Issuing Cert Authority"}, "notAfter": 2127420899}, } }, - { - "utcTime": 1702288467.6736515, - }, + {"utcTime": 1702288467.6736515}, ], "inputs": { "certificates": [ @@ -584,7 +461,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Certificate: ARISTA_SIGNING_CA.crt - incorrect encryption algorithm - Expected: ECDSA Actual: Not found", "Certificate: ARISTA_SIGNING_CA.crt - incorrect public key - Expected: 256 Actual: Not found", @@ -593,40 +470,33 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyBannerLogin, + (VerifyBannerLogin, "success"): { "eos_data": [ { - "loginBanner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" - "that can be found in the LICENSE file." + "loginBanner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0" + "\nthat can be found in the LICENSE file." } ], "inputs": { "login_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" "that can be found in the LICENSE file." }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiline", - "test": VerifyBannerLogin, + (VerifyBannerLogin, "success-multiline"): { "eos_data": [ { - "loginBanner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" - "that can be found in the LICENSE file." + "loginBanner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0" + "\nthat can be found in the LICENSE file." } ], "inputs": { - "login_banner": """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.""" + "login_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\n " + "Use of this source code is governed by the Apache License 2.0\n that can be found in the LICENSE file." }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-login-banner", - "test": VerifyBannerLogin, + (VerifyBannerLogin, "failure-incorrect-login-banner"): { "eos_data": [ { "loginBanner": "Copyright (c) 2023 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" @@ -638,31 +508,24 @@ DATA: list[dict[str, Any]] = [ "that can be found in the LICENSE file." }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Incorrect login banner configured - Expected: Copyright (c) 2023-2024 Arista Networks, Inc.\n" - "Use of this source code is governed by the Apache License 2.0\nthat can be found in the LICENSE file. " - "Actual: Copyright (c) 2023 Arista Networks, Inc.\n" "Use of this source code is governed by the Apache License 2.0\nthat can be found in the LICENSE file." + " Actual: Copyright (c) 2023 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0" + "\nthat can be found in the LICENSE file." ], }, }, - { - "name": "failure-login-banner-not-configured", - "test": VerifyBannerLogin, + (VerifyBannerLogin, "failure-login-banner-not-configured"): { "eos_data": [{"loginBanner": ""}], "inputs": { "login_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" "that can be found in the LICENSE file." }, - "expected": { - "result": "failure", - "messages": ["Login banner is not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Login banner is not configured"]}, }, - { - "name": "success", - "test": VerifyBannerMotd, + (VerifyBannerMotd, "success"): { "eos_data": [ { "motd": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" @@ -673,11 +536,9 @@ DATA: list[dict[str, Any]] = [ "motd_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" "that can be found in the LICENSE file." }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiline", - "test": VerifyBannerMotd, + (VerifyBannerMotd, "success-multiline"): { "eos_data": [ { "motd": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" @@ -685,15 +546,12 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": { - "motd_banner": """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.""" + "motd_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\n Use of this source code is governed " + "by the Apache License 2.0\n that can be found in the LICENSE file." }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-motd-banner", - "test": VerifyBannerMotd, + (VerifyBannerMotd, "failure-incorrect-motd-banner"): { "eos_data": [ { "motd": "Copyright (c) 2023 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" @@ -705,7 +563,7 @@ DATA: list[dict[str, Any]] = [ "that can be found in the LICENSE file." }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Incorrect MOTD banner configured - Expected: Copyright (c) 2023-2024 Arista Networks, Inc.\n" "Use of this source code is governed by the Apache License 2.0\nthat can be found in the LICENSE file. " @@ -714,22 +572,15 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-login-banner-not-configured", - "test": VerifyBannerMotd, + (VerifyBannerMotd, "failure-login-banner-not-configured"): { "eos_data": [{"motd": ""}], "inputs": { "motd_banner": "Copyright (c) 2023-2024 Arista Networks, Inc.\nUse of this source code is governed by the Apache License 2.0\n" "that can be found in the LICENSE file." }, - "expected": { - "result": "failure", - "messages": ["MOTD banner is not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["MOTD banner is not configured"]}, }, - { - "name": "success", - "test": VerifyIPv4ACL, + (VerifyIPv4ACL, "success"): { "eos_data": [ { "aclList": [ @@ -743,13 +594,10 @@ DATA: list[dict[str, Any]] = [ }, { "name": "LabTest", - "sequence": [ - {"text": "permit icmp any any", "sequenceNumber": 10}, - {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 20}, - ], + "sequence": [{"text": "permit icmp any any", "sequenceNumber": 10}, {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 20}], }, ] - }, + } ], "inputs": { "ipv4_access_lists": [ @@ -767,29 +615,14 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-acl-list", - "test": VerifyIPv4ACL, - "eos_data": [ - {"aclList": []}, - ], - "inputs": { - "ipv4_access_lists": [ - { - "name": "default-control-plane-acl", - "entries": [ - {"sequence": 10, "action": "permit icmp any any"}, - ], - }, - ] - }, - "expected": {"result": "failure", "messages": ["No Access Control List (ACL) configured"]}, + (VerifyIPv4ACL, "failure-no-acl-list"): { + "eos_data": [{"aclList": []}], + "inputs": {"ipv4_access_lists": [{"name": "default-control-plane-acl", "entries": [{"sequence": 10, "action": "permit icmp any any"}]}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No Access Control List (ACL) configured"]}, }, - { - "name": "failure-acl-not-found", - "test": VerifyIPv4ACL, + (VerifyIPv4ACL, "failure-acl-not-found"): { "eos_data": [ { "aclList": [ @@ -802,7 +635,7 @@ DATA: list[dict[str, Any]] = [ ], } ] - }, + } ], "inputs": { "ipv4_access_lists": [ @@ -820,11 +653,9 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "failure", "messages": ["ACL name: LabTest - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["ACL name: LabTest - Not configured"]}, }, - { - "name": "failure-sequence-not-found", - "test": VerifyIPv4ACL, + (VerifyIPv4ACL, "failure-sequence-not-found"): { "eos_data": [ { "aclList": [ @@ -838,13 +669,10 @@ DATA: list[dict[str, Any]] = [ }, { "name": "LabTest", - "sequence": [ - {"text": "permit icmp any any", "sequenceNumber": 10}, - {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 30}, - ], + "sequence": [{"text": "permit icmp any any", "sequenceNumber": 10}, {"text": "permit tcp any any range 5900 5910", "sequenceNumber": 30}], }, ] - }, + } ], "inputs": { "ipv4_access_lists": [ @@ -863,13 +691,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["ACL name: default-control-plane-acl Sequence: 30 - Not configured", "ACL name: LabTest Sequence: 20 - Not configured"], }, }, - { - "name": "failure-action-not-match", - "test": VerifyIPv4ACL, + (VerifyIPv4ACL, "failure-action-not-match"): { "eos_data": [ { "aclList": [ @@ -883,13 +709,10 @@ DATA: list[dict[str, Any]] = [ }, { "name": "LabTest", - "sequence": [ - {"text": "permit icmp any any", "sequenceNumber": 10}, - {"text": "permit udp any any eq bfd ttl eq 255", "sequenceNumber": 20}, - ], + "sequence": [{"text": "permit icmp any any", "sequenceNumber": 10}, {"text": "permit udp any any eq bfd ttl eq 255", "sequenceNumber": 20}], }, ] - }, + } ], "inputs": { "ipv4_access_lists": [ @@ -908,17 +731,15 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ - "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - Expected: permit udp any any eq bfd ttl eq 255 " - "Actual: permit tcp any any range 5900 5910", + "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - " + "Expected: permit udp any any eq bfd ttl eq 255 Actual: permit tcp any any range 5900 5910", "ACL name: LabTest Sequence: 20 - action mismatch - Expected: permit tcp any any range 5900 5910 Actual: permit udp any any eq bfd ttl eq 255", ], }, }, - { - "name": "failure-all-type", - "test": VerifyIPv4ACL, + (VerifyIPv4ACL, "failure-all-type"): { "eos_data": [ { "aclList": [ @@ -931,7 +752,7 @@ DATA: list[dict[str, Any]] = [ ], } ] - }, + } ], "inputs": { "ipv4_access_lists": [ @@ -950,43 +771,33 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "ACL name: default-control-plane-acl Sequence: 20 - Not configured", - "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - Expected: permit udp any any eq bfd ttl eq 255 " - "Actual: permit tcp any any range 5900 5910", + "ACL name: default-control-plane-acl Sequence: 30 - action mismatch - " + "Expected: permit udp any any eq bfd ttl eq 255 Actual: permit tcp any any range 5900 5910", "ACL name: LabTest - Not configured", ], }, }, - { - "name": "success", - "test": VerifyIPSecConnHealth, + (VerifyIPSecConnHealth, "success"): { "eos_data": [ { "connections": { - "default-172.18.3.2-172.18.5.2-srcUnused-0": { - "pathDict": {"path9": "Established"}, - }, - "default-100.64.3.2-100.64.5.2-srcUnused-0": { - "pathDict": {"path10": "Established"}, - }, + "default-172.18.3.2-172.18.5.2-srcUnused-0": {"pathDict": {"path9": "Established"}}, + "default-100.64.3.2-100.64.5.2-srcUnused-0": {"pathDict": {"path10": "Established"}}, } } ], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-connection", - "test": VerifyIPSecConnHealth, + (VerifyIPSecConnHealth, "failure-no-connection"): { "eos_data": [{"connections": {}}], "inputs": {}, - "expected": {"result": "failure", "messages": ["No IPv4 security connection configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No IPv4 security connection configured"]}, }, - { - "name": "failure-not-established", - "test": VerifyIPSecConnHealth, + (VerifyIPSecConnHealth, "failure-not-established"): { "eos_data": [ { "connections": { @@ -1002,16 +813,14 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Source: 172.18.3.2 Destination: 172.18.2.2 VRF: default - IPv4 security connection not established", "Source: 100.64.3.2 Destination: 100.64.5.2 VRF: Guest - IPv4 security connection not established", ], }, }, - { - "name": "success-with-connection", - "test": VerifySpecificIPSecConn, + (VerifySpecificIPSecConn, "success-with-connection"): { "eos_data": [ { "connections": { @@ -1039,14 +848,12 @@ DATA: list[dict[str, Any]] = [ {"source_address": "100.64.3.2", "destination_address": "100.64.2.2"}, {"source_address": "172.18.3.2", "destination_address": "172.18.2.2"}, ], - }, + } ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-without-connection", - "test": VerifySpecificIPSecConn, + (VerifySpecificIPSecConn, "success-without-connection"): { "eos_data": [ { "connections": { @@ -1060,19 +867,10 @@ DATA: list[dict[str, Any]] = [ } } ], - "inputs": { - "ip_security_connections": [ - { - "peer": "10.255.0.1", - "vrf": "default", - }, - ] - }, - "expected": {"result": "success"}, + "inputs": {"ip_security_connections": [{"peer": "10.255.0.1", "vrf": "default"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-connection", - "test": VerifySpecificIPSecConn, + (VerifySpecificIPSecConn, "failure-no-connection"): { "eos_data": [ {"connections": {}}, { @@ -1094,10 +892,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "ip_security_connections": [ - { - "peer": "10.255.0.1", - "vrf": "default", - }, + {"peer": "10.255.0.1", "vrf": "default"}, { "peer": "10.255.0.2", "vrf": "DATA", @@ -1108,11 +903,9 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "failure", "messages": ["Peer: 10.255.0.1 VRF: default - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Peer: 10.255.0.1 VRF: default - Not configured"]}, }, - { - "name": "failure-not-established", - "test": VerifySpecificIPSecConn, + (VerifySpecificIPSecConn, "failure-not-established"): { "eos_data": [ { "connections": { @@ -1128,7 +921,7 @@ DATA: list[dict[str, Any]] = [ "daddr": "100.64.1.2", "tunnelNs": "default", }, - }, + } }, { "connections": { @@ -1139,10 +932,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "ip_security_connections": [ - { - "peer": "10.255.0.1", - "vrf": "default", - }, + {"peer": "10.255.0.1", "vrf": "default"}, { "peer": "10.255.0.2", "vrf": "MGMT", @@ -1154,7 +944,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.255.0.1 VRF: default Source: 172.18.3.2 Destination: 172.18.2.2 - Connection down - Expected: Established Actual: Idle", "Peer: 10.255.0.1 VRF: default Source: 100.64.2.2 Destination: 100.64.1.2 - Connection down - Expected: Established Actual: Idle", @@ -1163,9 +953,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-missing-connection", - "test": VerifySpecificIPSecConn, + (VerifySpecificIPSecConn, "failure-missing-connection"): { "eos_data": [ { "connections": { @@ -1181,7 +969,7 @@ DATA: list[dict[str, Any]] = [ "daddr": "100.64.2.2", "tunnelNs": "default", }, - }, + } }, { "connections": { @@ -1202,10 +990,7 @@ DATA: list[dict[str, Any]] = [ ], "inputs": { "ip_security_connections": [ - { - "peer": "10.255.0.1", - "vrf": "default", - }, + {"peer": "10.255.0.1", "vrf": "default"}, { "peer": "10.255.0.2", "vrf": "default", @@ -1217,7 +1002,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Peer: 10.255.0.1 VRF: default Source: 172.18.3.2 Destination: 172.18.2.2 - Connection down - Expected: Established Actual: Idle", "Peer: 10.255.0.1 VRF: default Source: 100.64.3.2 Destination: 100.64.2.2 - Connection down - Expected: Established Actual: Idle", @@ -1226,21 +1011,17 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyHardwareEntropy, + (VerifyHardwareEntropy, "success"): { "eos_data": [{"cpuModel": "2.20GHz", "cryptoModule": "Crypto Module v3.0", "hardwareEntropyEnabled": True, "blockedNetworkProtocols": []}], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyHardwareEntropy, + (VerifyHardwareEntropy, "failure"): { "eos_data": [{"cpuModel": "2.20GHz", "cryptoModule": "Crypto Module v3.0", "hardwareEntropyEnabled": False, "blockedNetworkProtocols": []}], "inputs": {}, - "expected": {"result": "failure", "messages": ["Hardware entropy generation is disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Hardware entropy generation is disabled"]}, }, -] +} class TestAPISSLCertificate: diff --git a/tests/units/anta_tests/test_services.py b/tests/units/anta_tests/test_services.py index 2b9011c..ce8e2d9 100644 --- a/tests/units/anta_tests/test_services.py +++ b/tests/units/anta_tests/test_services.py @@ -5,65 +5,58 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.services import VerifyDNSLookup, VerifyDNSServers, VerifyErrdisableRecovery, VerifyHostname from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyHostname, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyHostname, "success"): { "eos_data": [{"hostname": "s1-spine1", "fqdn": "s1-spine1.fun.aristanetworks.com"}], "inputs": {"hostname": "s1-spine1"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-hostname", - "test": VerifyHostname, + (VerifyHostname, "failure-incorrect-hostname"): { "eos_data": [{"hostname": "s1-spine2", "fqdn": "s1-spine1.fun.aristanetworks.com"}], "inputs": {"hostname": "s1-spine1"}, - "expected": { - "result": "failure", - "messages": ["Incorrect Hostname - Expected: s1-spine1 Actual: s1-spine2"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Incorrect Hostname - Expected: s1-spine1 Actual: s1-spine2"]}, }, - { - "name": "success", - "test": VerifyDNSLookup, + (VerifyDNSLookup, "success"): { "eos_data": [ { "messages": [ - "Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\nName:\tarista.com\nAddress: 151.101.130.132\nName:\tarista.com\n" - "Address: 151.101.2.132\nName:\tarista.com\nAddress: 151.101.194.132\nName:\tarista.com\nAddress: 151.101.66.132\n\n" + "Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\nName:\tarista.com\nAddress: 151.101.130.132\n" + "Name:\tarista.com\nAddress: 151.101.2.132\nName:\tarista.com\nAddress: 151.101.194.132\nName:\tarista.com\nAddress: 151.101.66.132\n\n" ] }, {"messages": ["Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\nName:\twww.google.com\nAddress: 172.217.12.100\n\n"]}, ], "inputs": {"domain_names": ["arista.com", "www.google.com"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyDNSLookup, + (VerifyDNSLookup, "failure"): { "eos_data": [ {"messages": ["Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\n*** Can't find arista.ca: No answer\n\n"]}, {"messages": ["Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\nName:\twww.google.com\nAddress: 172.217.12.100\n\n"]}, {"messages": ["Server:\t\t127.0.0.1\nAddress:\t127.0.0.1#53\n\nNon-authoritative answer:\n*** Can't find google.ca: No answer\n\n"]}, ], "inputs": {"domain_names": ["arista.ca", "www.google.com", "google.ca"]}, - "expected": {"result": "failure", "messages": ["The following domain(s) are not resolved to an IP address: arista.ca, google.ca"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following domain(s) are not resolved to an IP address: arista.ca, google.ca"]}, }, - { - "name": "success", - "test": VerifyDNSServers, + (VerifyDNSServers, "success"): { "eos_data": [ { "nameServerConfigs": [ {"ipAddr": "10.14.0.1", "vrf": "default", "priority": 0}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}, {"ipAddr": "fd12:3456:789a::1", "vrf": "default", "priority": 0}, - ], + ] } ], "inputs": { @@ -73,32 +66,20 @@ DATA: list[dict[str, Any]] = [ {"server_address": "fd12:3456:789a::1", "vrf": "default", "priority": 0}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-dns-found", - "test": VerifyDNSServers, - "eos_data": [ - { - "nameServerConfigs": [], - } - ], + (VerifyDNSServers, "failure-no-dns-found"): { + "eos_data": [{"nameServerConfigs": []}], "inputs": { "dns_servers": [{"server_address": "10.14.0.10", "vrf": "default", "priority": 0}, {"server_address": "10.14.0.21", "vrf": "MGMT", "priority": 1}] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Server 10.14.0.10 VRF: default Priority: 0 - Not configured", "Server 10.14.0.21 VRF: MGMT Priority: 1 - Not configured"], }, }, - { - "name": "failure-incorrect-dns-details", - "test": VerifyDNSServers, - "eos_data": [ - { - "nameServerConfigs": [{"ipAddr": "10.14.0.1", "vrf": "CS", "priority": 1}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}], - } - ], + (VerifyDNSServers, "failure-incorrect-dns-details"): { + "eos_data": [{"nameServerConfigs": [{"ipAddr": "10.14.0.1", "vrf": "CS", "priority": 1}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}]}], "inputs": { "dns_servers": [ {"server_address": "10.14.0.1", "vrf": "CS", "priority": 0}, @@ -107,7 +88,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Server 10.14.0.1 VRF: CS Priority: 0 - Incorrect priority - Priority: 1", "Server 10.14.0.11 VRF: default Priority: 0 - Not configured", @@ -115,94 +96,62 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyErrdisableRecovery, + (VerifyErrdisableRecovery, "success"): { "eos_data": [ - # Adding empty line on purpose to verify they are skipped - """ - Errdisable Reason Timer Status Timer Interval - ------------------------------ ----------------- -------------- - acl Enabled 300 - - bpduguard Enabled 300 - arp-inspection Enabled 30 - """ + "\n Errdisable Reason Timer Status Timer Interval\n ------------------------------" + " ----------------- --------------\n acl Enabled 300\n\n " + " bpduguard Enabled 300\n arp-inspection " + " Enabled 30\n " ], "inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "bpduguard", "interval": 300}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-reason-missing", - "test": VerifyErrdisableRecovery, + (VerifyErrdisableRecovery, "failure-reason-missing"): { "eos_data": [ - """ - Errdisable Reason Timer Status Timer Interval - ------------------------------ ----------------- -------------- - acl Enabled 300 - bpduguard Enabled 300 - arp-inspection Enabled 30 - """ + "\n Errdisable Reason Timer Status Timer Interval\n ------------------------------" + " ----------------- --------------\n acl Enabled 300\n " + " bpduguard Enabled 300\n arp-inspection Enabled" + " 30\n " ], "inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}, {"reason": "tapagg", "interval": 30}]}, - "expected": { - "result": "failure", - "messages": ["Reason: tapagg Status: Enabled Interval: 30 - Not found"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Reason: tapagg Status: Enabled Interval: 30 - Not found"]}, }, - { - "name": "failure-reason-disabled", - "test": VerifyErrdisableRecovery, + (VerifyErrdisableRecovery, "failure-reason-disabled"): { "eos_data": [ - """ - Errdisable Reason Timer Status Timer Interval - ------------------------------ ----------------- -------------- - acl Disabled 300 - bpduguard Enabled 300 - arp-inspection Enabled 30 - """ + "\n Errdisable Reason Timer Status Timer Interval\n ------------------------------ " + "----------------- --------------\n acl Disabled 300\n " + "bpduguard Enabled 300\n arp-inspection Enabled" + " 30\n " ], "inputs": {"reasons": [{"reason": "acl", "interval": 300}, {"reason": "arp-inspection", "interval": 30}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Reason: acl Status: Enabled Interval: 300 - Incorrect configuration - Status: Disabled Interval: 300"], }, }, - { - "name": "failure-interval-not-ok", - "test": VerifyErrdisableRecovery, + (VerifyErrdisableRecovery, "failure-interval-not-ok"): { "eos_data": [ - """ - Errdisable Reason Timer Status Timer Interval - ------------------------------ ----------------- -------------- - acl Enabled 300 - bpduguard Enabled 300 - arp-inspection Enabled 30 - """ + "\n Errdisable Reason Timer Status Timer Interval\n ------------------------------ " + "----------------- --------------\n acl Enabled 300\n " + "bpduguard Enabled 300\n arp-inspection Enabled " + " 30\n " ], "inputs": {"reasons": [{"reason": "acl", "interval": 30}, {"reason": "arp-inspection", "interval": 30}]}, "expected": { - "result": "failure", - "messages": [ - "Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Enabled Interval: 300", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Enabled Interval: 300"], }, }, - { - "name": "failure-all-type", - "test": VerifyErrdisableRecovery, + (VerifyErrdisableRecovery, "failure-all-type"): { "eos_data": [ - """ - Errdisable Reason Timer Status Timer Interval - ------------------------------ ----------------- -------------- - acl Disabled 300 - bpduguard Enabled 300 - arp-inspection Enabled 30 - """ + "\n Errdisable Reason Timer Status Timer Interval\n ------------------------------ " + "----------------- --------------\n acl Disabled 300\n " + "bpduguard Enabled 300\n arp-inspection Enabled " + " 30\n " ], "inputs": {"reasons": [{"reason": "acl", "interval": 30}, {"reason": "arp-inspection", "interval": 300}, {"reason": "tapagg", "interval": 30}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300", "Reason: arp-inspection Status: Enabled Interval: 300 - Incorrect configuration - Status: Enabled Interval: 30", @@ -210,4 +159,4 @@ DATA: list[dict[str, Any]] = [ ], }, }, -] +} diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index 255443d..2c337ba 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.snmp import ( VerifySnmpContact, VerifySnmpErrorCounters, @@ -23,234 +26,117 @@ from anta.tests.snmp import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifySnmpStatus, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifySnmpStatus, "success"): { "eos_data": [{"vrfs": {"snmpVrfs": ["MGMT", "default"]}, "enabled": True}], "inputs": {"vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-vrf", - "test": VerifySnmpStatus, + (VerifySnmpStatus, "failure-wrong-vrf"): { "eos_data": [{"vrfs": {"snmpVrfs": ["default"]}, "enabled": True}], "inputs": {"vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - SNMP agent disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - SNMP agent disabled"]}, }, - { - "name": "failure-disabled", - "test": VerifySnmpStatus, + (VerifySnmpStatus, "failure-disabled"): { "eos_data": [{"vrfs": {"snmpVrfs": ["default"]}, "enabled": False}], "inputs": {"vrf": "default"}, - "expected": {"result": "failure", "messages": ["VRF: default - SNMP agent disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: default - SNMP agent disabled"]}, }, - { - "name": "success", - "test": VerifySnmpIPv4Acl, + (VerifySnmpIPv4Acl, "success"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_SNMP", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifySnmpIPv4Acl, + (VerifySnmpIPv4Acl, "failure-wrong-number"): { "eos_data": [{"ipAclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Incorrect SNMP IPv4 ACL(s) - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Incorrect SNMP IPv4 ACL(s) - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifySnmpIPv4Acl, + (VerifySnmpIPv4Acl, "failure-wrong-vrf"): { "eos_data": [{"ipAclList": {"aclList": [{"type": "Ip4Acl", "name": "ACL_IPV4_SNMP", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following SNMP IPv4 ACL(s) not configured or active: ACL_IPV4_SNMP"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following SNMP IPv4 ACL(s) not configured or active: ACL_IPV4_SNMP"]}, }, - { - "name": "success", - "test": VerifySnmpIPv6Acl, + (VerifySnmpIPv6Acl, "success"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_SNMP", "configuredVrfs": ["MGMT"], "activeVrfs": ["MGMT"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-wrong-number", - "test": VerifySnmpIPv6Acl, + (VerifySnmpIPv6Acl, "failure-wrong-number"): { "eos_data": [{"ipv6AclList": {"aclList": []}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Incorrect SNMP IPv6 ACL(s) - Expected: 1 Actual: 0"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Incorrect SNMP IPv6 ACL(s) - Expected: 1 Actual: 0"]}, }, - { - "name": "failure-wrong-vrf", - "test": VerifySnmpIPv6Acl, + (VerifySnmpIPv6Acl, "failure-wrong-vrf"): { "eos_data": [{"ipv6AclList": {"aclList": [{"type": "Ip6Acl", "name": "ACL_IPV6_SNMP", "configuredVrfs": ["default"], "activeVrfs": ["default"]}]}}], "inputs": {"number": 1, "vrf": "MGMT"}, - "expected": {"result": "failure", "messages": ["VRF: MGMT - Following SNMP IPv6 ACL(s) not configured or active: ACL_IPV6_SNMP"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VRF: MGMT - Following SNMP IPv6 ACL(s) not configured or active: ACL_IPV6_SNMP"]}, }, - { - "name": "success", - "test": VerifySnmpLocation, - "eos_data": [ - { - "location": {"location": "New York"}, - } - ], + (VerifySnmpLocation, "success"): { + "eos_data": [{"location": {"location": "New York"}}], "inputs": {"location": "New York"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-location", - "test": VerifySnmpLocation, - "eos_data": [ - { - "location": {"location": "Europe"}, - } - ], + (VerifySnmpLocation, "failure-incorrect-location"): { + "eos_data": [{"location": {"location": "Europe"}}], "inputs": {"location": "New York"}, - "expected": { - "result": "failure", - "messages": ["Incorrect SNMP location - Expected: New York Actual: Europe"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Incorrect SNMP location - Expected: New York Actual: Europe"]}, }, - { - "name": "failure-details-not-configured", - "test": VerifySnmpLocation, - "eos_data": [ - { - "location": {"location": ""}, - } - ], + (VerifySnmpLocation, "failure-details-not-configured"): { + "eos_data": [{"location": {"location": ""}}], "inputs": {"location": "New York"}, - "expected": { - "result": "failure", - "messages": ["SNMP location is not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP location is not configured"]}, }, - { - "name": "success", - "test": VerifySnmpContact, - "eos_data": [ - { - "contact": {"contact": "Jon@example.com"}, - } - ], + (VerifySnmpContact, "success"): { + "eos_data": [{"contact": {"contact": "Jon@example.com"}}], "inputs": {"contact": "Jon@example.com"}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-contact", - "test": VerifySnmpContact, - "eos_data": [ - { - "contact": {"contact": "Jon@example.com"}, - } - ], + (VerifySnmpContact, "failure-incorrect-contact"): { + "eos_data": [{"contact": {"contact": "Jon@example.com"}}], "inputs": {"contact": "Bob@example.com"}, - "expected": { - "result": "failure", - "messages": ["Incorrect SNMP contact - Expected: Bob@example.com Actual: Jon@example.com"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Incorrect SNMP contact - Expected: Bob@example.com Actual: Jon@example.com"]}, }, - { - "name": "failure-details-not-configured", - "test": VerifySnmpContact, - "eos_data": [ - { - "contact": {"contact": ""}, - } - ], + (VerifySnmpContact, "failure-details-not-configured"): { + "eos_data": [{"contact": {"contact": ""}}], "inputs": {"contact": "Bob@example.com"}, - "expected": { - "result": "failure", - "messages": ["SNMP contact is not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP contact is not configured"]}, }, - { - "name": "success", - "test": VerifySnmpPDUCounters, - "eos_data": [ - { - "counters": { - "inGetPdus": 3, - "inGetNextPdus": 2, - "inSetPdus": 3, - "outGetResponsePdus": 3, - "outTrapPdus": 9, - }, - } - ], + (VerifySnmpPDUCounters, "success"): { + "eos_data": [{"counters": {"inGetPdus": 3, "inGetNextPdus": 2, "inSetPdus": 3, "outGetResponsePdus": 3, "outTrapPdus": 9}}], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-specific-pdus", - "test": VerifySnmpPDUCounters, - "eos_data": [ - { - "counters": { - "inGetPdus": 3, - "inGetNextPdus": 0, - "inSetPdus": 0, - "outGetResponsePdus": 0, - "outTrapPdus": 9, - }, - } - ], + (VerifySnmpPDUCounters, "success-specific-pdus"): { + "eos_data": [{"counters": {"inGetPdus": 3, "inGetNextPdus": 0, "inSetPdus": 0, "outGetResponsePdus": 0, "outTrapPdus": 9}}], "inputs": {"pdus": ["inGetPdus", "outTrapPdus"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-counters-not-found", - "test": VerifySnmpPDUCounters, - "eos_data": [ - { - "counters": {}, - } - ], + (VerifySnmpPDUCounters, "failure-counters-not-found"): { + "eos_data": [{"counters": {}}], "inputs": {}, - "expected": {"result": "failure", "messages": ["SNMP counters not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP counters not found"]}, }, - { - "name": "failure-incorrect-counters", - "test": VerifySnmpPDUCounters, - "eos_data": [ - { - "counters": { - "inGetPdus": 0, - "inGetNextPdus": 2, - "inSetPdus": 0, - "outGetResponsePdus": 3, - "outTrapPdus": 9, - }, - } - ], + (VerifySnmpPDUCounters, "failure-incorrect-counters"): { + "eos_data": [{"counters": {"inGetPdus": 0, "inGetNextPdus": 2, "inSetPdus": 0, "outGetResponsePdus": 3, "outTrapPdus": 9}}], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["The following SNMP PDU counters are not found or have zero PDU counters: inGetPdus, inSetPdus"], }, }, - { - "name": "failure-pdu-not-found", - "test": VerifySnmpPDUCounters, - "eos_data": [ - { - "counters": { - "inGetNextPdus": 0, - "inSetPdus": 0, - "outGetResponsePdus": 0, - }, - } - ], + (VerifySnmpPDUCounters, "failure-pdu-not-found"): { + "eos_data": [{"counters": {"inGetNextPdus": 0, "inSetPdus": 0, "outGetResponsePdus": 0}}], "inputs": {"pdus": ["inGetPdus", "outTrapPdus"]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["The following SNMP PDU counters are not found or have zero PDU counters: inGetPdus, outTrapPdus"], }, }, - { - "name": "success", - "test": VerifySnmpErrorCounters, + (VerifySnmpErrorCounters, "success"): { "eos_data": [ { "counters": { @@ -262,15 +148,13 @@ DATA: list[dict[str, Any]] = [ "outNoSuchNameErrs": 0, "outBadValueErrs": 0, "outGeneralErrs": 0, - }, + } } ], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-specific-counters", - "test": VerifySnmpErrorCounters, + (VerifySnmpErrorCounters, "success-specific-counters"): { "eos_data": [ { "counters": { @@ -282,26 +166,18 @@ DATA: list[dict[str, Any]] = [ "outNoSuchNameErrs": 0, "outBadValueErrs": 10, "outGeneralErrs": 1, - }, + } } ], "inputs": {"error_counters": ["inVersionErrs", "inParseErrs"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-counters-not-found", - "test": VerifySnmpErrorCounters, - "eos_data": [ - { - "counters": {}, - } - ], + (VerifySnmpErrorCounters, "failure-counters-not-found"): { + "eos_data": [{"counters": {}}], "inputs": {}, - "expected": {"result": "failure", "messages": ["SNMP counters not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP counters not found"]}, }, - { - "name": "failure-incorrect-counters", - "test": VerifySnmpErrorCounters, + (VerifySnmpErrorCounters, "failure-incorrect-counters"): { "eos_data": [ { "counters": { @@ -313,18 +189,16 @@ DATA: list[dict[str, Any]] = [ "outNoSuchNameErrs": 0, "outBadValueErrs": 2, "outGeneralErrs": 0, - }, + } } ], "inputs": {}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["The following SNMP error counters are not found or have non-zero error counters: inParseErrs, inVersionErrs, outBadValueErrs"], }, }, - { - "name": "success", - "test": VerifySnmpHostLogging, + (VerifySnmpHostLogging, "success"): { "eos_data": [ { "logging": { @@ -344,61 +218,38 @@ DATA: list[dict[str, Any]] = [ {"hostname": "snmp-server-01", "vrf": "default"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-logging-disabled", - "test": VerifySnmpHostLogging, + (VerifySnmpHostLogging, "failure-logging-disabled"): { "eos_data": [{"logging": {"loggingEnabled": False}}], "inputs": {"hosts": [{"hostname": "192.168.1.100", "vrf": "default"}, {"hostname": "192.168.1.101", "vrf": "MGMT"}]}, - "expected": {"result": "failure", "messages": ["SNMP logging is disabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP logging is disabled"]}, }, - { - "name": "failure-mismatch-vrf", - "test": VerifySnmpHostLogging, + (VerifySnmpHostLogging, "failure-mismatch-vrf"): { "eos_data": [{"logging": {"loggingEnabled": True, "hosts": {"192.168.1.100": {"port": 162, "vrf": "MGMT"}, "192.168.1.101": {"port": 162, "vrf": "Test"}}}}], "inputs": {"hosts": [{"hostname": "192.168.1.100", "vrf": "default"}, {"hostname": "192.168.1.101", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Host: 192.168.1.100 VRF: default - Incorrect VRF - Actual: MGMT", "Host: 192.168.1.101 VRF: MGMT - Incorrect VRF - Actual: Test"], }, }, - { - "name": "failure-host-not-configured", - "test": VerifySnmpHostLogging, + (VerifySnmpHostLogging, "failure-host-not-configured"): { "eos_data": [{"logging": {"loggingEnabled": True, "hosts": {"192.168.1.100": {"port": 162, "vrf": "MGMT"}, "192.168.1.103": {"port": 162, "vrf": "Test"}}}}], "inputs": {"hosts": [{"hostname": "192.168.1.101", "vrf": "default"}, {"hostname": "192.168.1.102", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Host: 192.168.1.101 VRF: default - Not configured", "Host: 192.168.1.102 VRF: MGMT - Not configured"], }, }, - { - "name": "success", - "test": VerifySnmpUser, + (VerifySnmpUser, "success"): { "eos_data": [ { "usersByVersion": { - "v1": { - "users": { - "Test1": { - "groupName": "TestGroup1", - }, - } - }, - "v2c": { - "users": { - "Test2": { - "groupName": "TestGroup2", - }, - } - }, + "v1": {"users": {"Test1": {"groupName": "TestGroup1"}}}, + "v2c": {"users": {"Test2": {"groupName": "TestGroup2"}}}, "v3": { "users": { - "Test3": { - "groupName": "TestGroup3", - "v3Params": {"authType": "SHA-384", "privType": "AES-128"}, - }, + "Test3": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}}, "Test4": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-512", "privType": "AES-192"}}, } }, @@ -413,25 +264,10 @@ DATA: list[dict[str, Any]] = [ {"username": "Test4", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifySnmpUser, - "eos_data": [ - { - "usersByVersion": { - "v3": { - "users": { - "Test3": { - "groupName": "TestGroup3", - "v3Params": {"authType": "SHA-384", "privType": "AES-128"}, - }, - } - }, - } - } - ], + (VerifySnmpUser, "failure-not-configured"): { + "eos_data": [{"usersByVersion": {"v3": {"users": {"Test3": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}}}}}}], "inputs": { "snmp_users": [ {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, @@ -441,7 +277,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "User: Test1 Group: TestGroup1 Version: v1 - Not found", "User: Test2 Group: TestGroup2 Version: v2c - Not found", @@ -449,70 +285,30 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-incorrect-group", - "test": VerifySnmpUser, + (VerifySnmpUser, "failure-incorrect-group"): { "eos_data": [ - { - "usersByVersion": { - "v1": { - "users": { - "Test1": { - "groupName": "TestGroup2", - }, - } - }, - "v2c": { - "users": { - "Test2": { - "groupName": "TestGroup1", - }, - } - }, - "v3": {}, - } - } + {"usersByVersion": {"v1": {"users": {"Test1": {"groupName": "TestGroup2"}}}, "v2c": {"users": {"Test2": {"groupName": "TestGroup1"}}}, "v3": {}}} ], "inputs": { - "snmp_users": [ - {"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, - {"username": "Test2", "group_name": "TestGroup2", "version": "v2c"}, - ] + "snmp_users": [{"username": "Test1", "group_name": "TestGroup1", "version": "v1"}, {"username": "Test2", "group_name": "TestGroup2", "version": "v2c"}] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "User: Test1 Group: TestGroup1 Version: v1 - Incorrect user group - Actual: TestGroup2", "User: Test2 Group: TestGroup2 Version: v2c - Incorrect user group - Actual: TestGroup1", ], }, }, - { - "name": "failure-incorrect-auth-encryption", - "test": VerifySnmpUser, + (VerifySnmpUser, "failure-incorrect-auth-encryption"): { "eos_data": [ { "usersByVersion": { - "v1": { - "users": { - "Test1": { - "groupName": "TestGroup1", - }, - } - }, - "v2c": { - "users": { - "Test2": { - "groupName": "TestGroup2", - }, - } - }, + "v1": {"users": {"Test1": {"groupName": "TestGroup1"}}}, + "v2c": {"users": {"Test2": {"groupName": "TestGroup2"}}}, "v3": { "users": { - "Test3": { - "groupName": "TestGroup3", - "v3Params": {"authType": "SHA-512", "privType": "AES-192"}, - }, + "Test3": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-512", "privType": "AES-192"}}, "Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}}, } }, @@ -528,7 +324,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "User: Test3 Group: TestGroup3 Version: v3 - Incorrect authentication type - Expected: SHA-384 Actual: SHA-512", "User: Test3 Group: TestGroup3 Version: v3 - Incorrect privacy type - Expected: AES-128 Actual: AES-192", @@ -537,9 +333,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "success"): { "eos_data": [ { "hosts": [ @@ -568,11 +362,9 @@ DATA: list[dict[str, Any]] = [ {"hostname": "192.168.1.101", "vrf": "MGMT", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-not-configured"): { "eos_data": [{"hosts": []}], "inputs": { "notification_hosts": [ @@ -580,11 +372,9 @@ DATA: list[dict[str, Any]] = [ {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, - "expected": {"result": "failure", "messages": ["No SNMP host is configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No SNMP host is configured"]}, }, - { - "name": "failure-details-host-not-found", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-details-host-not-found"): { "eos_data": [ { "hosts": [ @@ -595,7 +385,7 @@ DATA: list[dict[str, Any]] = [ "notificationType": "trap", "protocolVersion": "v3", "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, - }, + } ] } ], @@ -605,11 +395,9 @@ DATA: list[dict[str, Any]] = [ {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, - "expected": {"result": "failure", "messages": ["Host: 192.168.1.101 VRF: default Version: v2c - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Host: 192.168.1.101 VRF: default Version: v2c - Not configured"]}, }, - { - "name": "failure-incorrect-notification-type", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-incorrect-notification-type"): { "eos_data": [ { "hosts": [ @@ -639,16 +427,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Host: 192.168.1.100 VRF: default - Incorrect notification type - Expected: inform Actual: trap", "Host: 192.168.1.101 VRF: default - Incorrect notification type - Expected: trap Actual: inform", ], }, }, - { - "name": "failure-incorrect-udp-port", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-incorrect-udp-port"): { "eos_data": [ { "hosts": [ @@ -678,16 +464,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Host: 192.168.1.100 VRF: default - Incorrect UDP port - Expected: 162 Actual: 163", "Host: 192.168.1.101 VRF: default - Incorrect UDP port - Expected: 162 Actual: 164", ], }, }, - { - "name": "failure-incorrect-community-string-version-v1-v2c", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-incorrect-community-string-version-v1-v2c"): { "eos_data": [ { "hosts": [ @@ -717,16 +501,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Host: 192.168.1.100 VRF: default Version: v1 - Incorrect community string - Expected: public Actual: private", "Host: 192.168.1.101 VRF: default Version: v2c - Incorrect community string - Expected: public Actual: private", ], }, }, - { - "name": "failure-incorrect-user-for-version-v3", - "test": VerifySnmpNotificationHost, + (VerifySnmpNotificationHost, "failure-incorrect-user-for-version-v3"): { "eos_data": [ { "hosts": [ @@ -742,58 +524,35 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": { - "notification_hosts": [ - {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, - ] + "notification_hosts": [{"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}] + }, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Host: 192.168.1.100 VRF: default Version: v3 - Incorrect user - Expected: public Actual: private"], }, - "expected": {"result": "failure", "messages": ["Host: 192.168.1.100 VRF: default Version: v3 - Incorrect user - Expected: public Actual: private"]}, }, - { - "name": "success", - "test": VerifySnmpSourceInterface, - "eos_data": [ - { - "srcIntf": {"sourceInterfaces": {"default": "Ethernet1", "MGMT": "Management0"}}, - } - ], + (VerifySnmpSourceInterface, "success"): { + "eos_data": [{"srcIntf": {"sourceInterfaces": {"default": "Ethernet1", "MGMT": "Management0"}}}], "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-not-configured", - "test": VerifySnmpSourceInterface, - "eos_data": [ - { - "srcIntf": {}, - } - ], + (VerifySnmpSourceInterface, "failure-not-configured"): { + "eos_data": [{"srcIntf": {}}], "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]}, - "expected": {"result": "failure", "messages": ["SNMP source interface(s) not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["SNMP source interface(s) not configured"]}, }, - { - "name": "failure-incorrect-interfaces", - "test": VerifySnmpSourceInterface, - "eos_data": [ - { - "srcIntf": { - "sourceInterfaces": { - "default": "Management0", - } - }, - } - ], + (VerifySnmpSourceInterface, "failure-incorrect-interfaces"): { + "eos_data": [{"srcIntf": {"sourceInterfaces": {"default": "Management0"}}}], "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Source Interface: Ethernet1 VRF: default - Incorrect source interface - Actual: Management0", "Source Interface: Management0 VRF: MGMT - Not configured", ], }, }, - { - "name": "success", - "test": VerifySnmpGroup, + (VerifySnmpGroup, "success"): { "eos_data": [ { "groups": { @@ -853,11 +612,9 @@ DATA: list[dict[str, Any]] = [ }, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-view", - "test": VerifySnmpGroup, + (VerifySnmpGroup, "failure-incorrect-view"): { "eos_data": [ { "groups": { @@ -918,7 +675,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Group: Group1 Version: v1 - Incorrect Read view - Expected: group_read_1 Actual: group_read", "Group: Group1 Version: v1 - Incorrect Write view - Expected: group_write_1 Actual: group_write", @@ -931,9 +688,7 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-view-config-not-found", - "test": VerifySnmpGroup, + (VerifySnmpGroup, "failure-view-config-not-found"): { "eos_data": [ { "groups": { @@ -983,17 +738,11 @@ DATA: list[dict[str, Any]] = [ "snmp_groups": [ {"group_name": "Group1", "version": "v1", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, {"group_name": "Group2", "version": "v2c", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, - { - "group_name": "Group3", - "version": "v3", - "write_view": "group_write", - "notify_view": "group_notify", - "authentication": "priv", - }, + {"group_name": "Group3", "version": "v3", "write_view": "group_write", "notify_view": "group_notify", "authentication": "priv"}, ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Group: Group1 Version: v1 Read View: group_read - Not configured", "Group: Group1 Version: v1 Write View: group_write - Not configured", @@ -1006,18 +755,8 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-group-version-not-configured", - "test": VerifySnmpGroup, - "eos_data": [ - { - "groups": { - "Group1": {"versions": {"v1": {}}}, - "Group2": {"versions": {"v2c": {}}}, - "Group3": {"versions": {"v3": {}}}, - } - } - ], + (VerifySnmpGroup, "failure-group-version-not-configured"): { + "eos_data": [{"groups": {"Group1": {"versions": {"v1": {}}}, "Group2": {"versions": {"v2c": {}}}, "Group3": {"versions": {"v3": {}}}}}], "inputs": { "snmp_groups": [ {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1"}, @@ -1033,17 +772,11 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", - "messages": [ - "Group: Group1 Version: v1 - Not configured", - "Group: Group2 Version: v2c - Not configured", - "Group: Group3 Version: v3 - Not configured", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Group: Group1 Version: v1 - Not configured", "Group: Group2 Version: v2c - Not configured", "Group: Group3 Version: v3 - Not configured"], }, }, - { - "name": "failure-incorrect-v3-auth-model", - "test": VerifySnmpGroup, + (VerifySnmpGroup, "failure-incorrect-v3-auth-model"): { "eos_data": [ { "groups": { @@ -1059,7 +792,7 @@ DATA: list[dict[str, Any]] = [ "notifyViewConfig": True, } } - }, + } } } ], @@ -1072,42 +805,20 @@ DATA: list[dict[str, Any]] = [ "write_view": "group_write", "notify_view": "group_notify", "authentication": "priv", - }, + } ] }, - "expected": { - "result": "failure", - "messages": [ - "Group: Group3 Version: v3 - Incorrect security model - Expected: v3Priv Actual: v3Auth", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Group: Group3 Version: v3 - Incorrect security model - Expected: v3Priv Actual: v3Auth"]}, }, - { - "name": "failure-view-not-configured", - "test": VerifySnmpGroup, + (VerifySnmpGroup, "failure-view-not-configured"): { "eos_data": [ { "groups": { - "Group3": {"versions": {"v3": {"secModel": "v3NoAuth", "readView": "group_read", "readViewConfig": True, "writeView": "", "notifyView": ""}}}, + "Group3": {"versions": {"v3": {"secModel": "v3NoAuth", "readView": "group_read", "readViewConfig": True, "writeView": "", "notifyView": ""}}} } } ], - "inputs": { - "snmp_groups": [ - { - "group_name": "Group3", - "version": "v3", - "read_view": "group_read", - "write_view": "group_write", - "authentication": "noauth", - }, - ] - }, - "expected": { - "result": "failure", - "messages": [ - "Group: Group3 Version: v3 View: write - Not configured", - ], - }, + "inputs": {"snmp_groups": [{"group_name": "Group3", "version": "v3", "read_view": "group_read", "write_view": "group_write", "authentication": "noauth"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Group: Group3 Version: v3 View: write - Not configured"]}, }, -] +} diff --git a/tests/units/anta_tests/test_software.py b/tests/units/anta_tests/test_software.py index 217cfff..2d4f825 100644 --- a/tests/units/anta_tests/test_software.py +++ b/tests/units/anta_tests/test_software.py @@ -5,41 +5,29 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.software import VerifyEOSExtensions, VerifyEOSVersion, VerifyTerminAttrVersion from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyEOSVersion, - "eos_data": [ - { - "modelName": "vEOS-lab", - "internalVersion": "4.27.0F-24305004.4270F", - "version": "4.27.0F", - }, - ], +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyEOSVersion, "success"): { + "eos_data": [{"modelName": "vEOS-lab", "internalVersion": "4.27.0F-24305004.4270F", "version": "4.27.0F"}], "inputs": {"versions": ["4.27.0F", "4.28.0F"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyEOSVersion, - "eos_data": [ - { - "modelName": "vEOS-lab", - "internalVersion": "4.27.0F-24305004.4270F", - "version": "4.27.0F", - }, - ], + (VerifyEOSVersion, "failure"): { + "eos_data": [{"modelName": "vEOS-lab", "internalVersion": "4.27.0F-24305004.4270F", "version": "4.27.0F"}], "inputs": {"versions": ["4.27.1F"]}, - "expected": {"result": "failure", "messages": ["EOS version mismatch - Actual: 4.27.0F not in Expected: 4.27.1F"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["EOS version mismatch - Actual: 4.27.0F not in Expected: 4.27.1F"]}, }, - { - "name": "success", - "test": VerifyTerminAttrVersion, + (VerifyTerminAttrVersion, "success"): { "eos_data": [ { "imageFormatVersion": "1.0", @@ -49,18 +37,14 @@ DATA: list[dict[str, Any]] = [ "deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}], "switchType": "fixedSystem", - "packages": { - "TerminAttr-core": {"release": "1", "version": "v1.17.0"}, - }, + "packages": {"TerminAttr-core": {"release": "1", "version": "v1.17.0"}}, }, - }, + } ], "inputs": {"versions": ["v1.17.0", "v1.18.1"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyTerminAttrVersion, + (VerifyTerminAttrVersion, "failure"): { "eos_data": [ { "imageFormatVersion": "1.0", @@ -70,28 +54,18 @@ DATA: list[dict[str, Any]] = [ "deviations": [], "components": [{"name": "Aboot", "version": "Aboot-veos-8.0.0-3255441"}], "switchType": "fixedSystem", - "packages": { - "TerminAttr-core": {"release": "1", "version": "v1.17.0"}, - }, + "packages": {"TerminAttr-core": {"release": "1", "version": "v1.17.0"}}, }, - }, + } ], "inputs": {"versions": ["v1.17.1", "v1.18.1"]}, - "expected": {"result": "failure", "messages": ["TerminAttr version mismatch - Actual: v1.17.0 not in Expected: v1.17.1, v1.18.1"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["TerminAttr version mismatch - Actual: v1.17.0 not in Expected: v1.17.1, v1.18.1"]}, }, - { - "name": "success-no-extensions", - "test": VerifyEOSExtensions, - "eos_data": [ - {"extensions": {}, "extensionStoredDir": "flash:", "warnings": ["No extensions are available"]}, - {"extensions": []}, - ], - "inputs": None, - "expected": {"result": "success"}, + (VerifyEOSExtensions, "success-no-extensions"): { + "eos_data": [{"extensions": {}, "extensionStoredDir": "flash:", "warnings": ["No extensions are available"]}, {"extensions": []}], + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-extensions", - "test": VerifyEOSExtensions, + (VerifyEOSExtensions, "success-extensions"): { "eos_data": [ { "extensions": { @@ -110,17 +84,14 @@ DATA: list[dict[str, Any]] = [ "description": "An extension for Arista Cloud Connect gateway", "affectedAgents": [], "agentsToRestart": [], - }, + } } }, {"extensions": ["AristaCloudGateway-1.0.1-1.swix"]}, ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyEOSExtensions, + (VerifyEOSExtensions, "failure"): { "eos_data": [ { "extensions": { @@ -139,17 +110,14 @@ DATA: list[dict[str, Any]] = [ "description": "An extension for Arista Cloud Connect gateway", "affectedAgents": [], "agentsToRestart": [], - }, + } } }, {"extensions": []}, ], - "inputs": None, - "expected": {"result": "failure", "messages": ["EOS extensions mismatch - Installed: AristaCloudGateway-1.0.1-1.swix Configured: Not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["EOS extensions mismatch - Installed: AristaCloudGateway-1.0.1-1.swix Configured: Not found"]}, }, - { - "name": "failure-multiple-extensions", - "test": VerifyEOSExtensions, + (VerifyEOSExtensions, "failure-multiple-extensions"): { "eos_data": [ { "extensions": { @@ -190,12 +158,11 @@ DATA: list[dict[str, Any]] = [ }, {"extensions": ["AristaCloudGateway-1.0.1-1.swix", "EOS-4.33.0F-NDRSensor.swix"]}, ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "EOS extensions mismatch - Installed: AristaCloudGateway-1.0.1-1.swix Configured: AristaCloudGateway-1.0.1-1.swix, EOS-4.33.0F-NDRSensor.swix" ], }, }, -] +} diff --git a/tests/units/anta_tests/test_stp.py b/tests/units/anta_tests/test_stp.py index 539c5e9..67aaf32 100644 --- a/tests/units/anta_tests/test_stp.py +++ b/tests/units/anta_tests/test_stp.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.stp import ( VerifySTPBlockedPorts, VerifySTPCounters, @@ -18,117 +21,89 @@ from anta.tests.stp import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifySTPMode, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifySTPMode, "success"): { "eos_data": [ {"spanningTreeVlanInstances": {"10": {"spanningTreeVlanInstance": {"protocol": "rstp"}}}}, {"spanningTreeVlanInstances": {"20": {"spanningTreeVlanInstance": {"protocol": "rstp"}}}}, ], "inputs": {"mode": "rstp", "vlans": [10, 20]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-instances", - "test": VerifySTPMode, - "eos_data": [ - {"spanningTreeVlanInstances": {}}, - {"spanningTreeVlanInstances": {}}, - ], + (VerifySTPMode, "failure-no-instances"): { + "eos_data": [{"spanningTreeVlanInstances": {}}, {"spanningTreeVlanInstances": {}}], "inputs": {"mode": "rstp", "vlans": [10, 20]}, - "expected": {"result": "failure", "messages": ["VLAN 10 STP mode: rstp - Not configured", "VLAN 20 STP mode: rstp - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN 10 STP mode: rstp - Not configured", "VLAN 20 STP mode: rstp - Not configured"]}, }, - { - "name": "failure-wrong-mode", - "test": VerifySTPMode, + (VerifySTPMode, "failure-wrong-mode"): { "eos_data": [ {"spanningTreeVlanInstances": {"10": {"spanningTreeVlanInstance": {"protocol": "mstp"}}}}, {"spanningTreeVlanInstances": {"20": {"spanningTreeVlanInstance": {"protocol": "mstp"}}}}, ], "inputs": {"mode": "rstp", "vlans": [10, 20]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["VLAN 10 - Incorrect STP mode - Expected: rstp Actual: mstp", "VLAN 20 - Incorrect STP mode - Expected: rstp Actual: mstp"], }, }, - { - "name": "failure-both", - "test": VerifySTPMode, - "eos_data": [ - {"spanningTreeVlanInstances": {}}, - {"spanningTreeVlanInstances": {"20": {"spanningTreeVlanInstance": {"protocol": "mstp"}}}}, - ], + (VerifySTPMode, "failure-both"): { + "eos_data": [{"spanningTreeVlanInstances": {}}, {"spanningTreeVlanInstances": {"20": {"spanningTreeVlanInstance": {"protocol": "mstp"}}}}], "inputs": {"mode": "rstp", "vlans": [10, 20]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["VLAN 10 STP mode: rstp - Not configured", "VLAN 20 - Incorrect STP mode - Expected: rstp Actual: mstp"], }, }, - { - "name": "success", - "test": VerifySTPBlockedPorts, - "eos_data": [{"spanningTreeInstances": {}}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifySTPBlockedPorts, + (VerifySTPBlockedPorts, "success"): {"eos_data": [{"spanningTreeInstances": {}}], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifySTPBlockedPorts, "failure"): { "eos_data": [{"spanningTreeInstances": {"MST0": {"spanningTreeBlockedPorts": ["Ethernet10"]}, "MST10": {"spanningTreeBlockedPorts": ["Ethernet10"]}}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["STP Instance: MST0 - Blocked ports - Ethernet10", "STP Instance: MST10 - Blocked ports - Ethernet10"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["STP Instance: MST0 - Blocked ports - Ethernet10", "STP Instance: MST10 - Blocked ports - Ethernet10"], + }, }, - { - "name": "success", - "test": VerifySTPCounters, + (VerifySTPCounters, "success"): { "eos_data": [{"interfaces": {"Ethernet10": {"bpduSent": 99, "bpduReceived": 0, "bpduTaggedError": 0, "bpduOtherError": 0, "bpduRateLimitCount": 0}}}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-bpdu-tagged-error-mismatch", - "test": VerifySTPCounters, + (VerifySTPCounters, "failure-bpdu-tagged-error-mismatch"): { "eos_data": [ { "interfaces": { "Ethernet10": {"bpduSent": 201, "bpduReceived": 0, "bpduTaggedError": 3, "bpduOtherError": 0, "bpduRateLimitCount": 0}, "Ethernet11": {"bpduSent": 99, "bpduReceived": 0, "bpduTaggedError": 3, "bpduOtherError": 0, "bpduRateLimitCount": 0}, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface Ethernet10 - STP BPDU packet tagged errors count mismatch - Expected: 0 Actual: 3", "Interface Ethernet11 - STP BPDU packet tagged errors count mismatch - Expected: 0 Actual: 3", ], }, }, - { - "name": "failure-bpdu-other-error-mismatch", - "test": VerifySTPCounters, + (VerifySTPCounters, "failure-bpdu-other-error-mismatch"): { "eos_data": [ { "interfaces": { "Ethernet10": {"bpduSent": 201, "bpduReceived": 0, "bpduTaggedError": 0, "bpduOtherError": 3, "bpduRateLimitCount": 0}, "Ethernet11": {"bpduSent": 99, "bpduReceived": 0, "bpduTaggedError": 0, "bpduOtherError": 6, "bpduRateLimitCount": 0}, - }, - }, + } + } ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface Ethernet10 - STP BPDU packet other errors count mismatch - Expected: 0 Actual: 3", "Interface Ethernet11 - STP BPDU packet other errors count mismatch - Expected: 0 Actual: 6", ], }, }, - { - "name": "success", - "test": VerifySTPForwardingPorts, + (VerifySTPForwardingPorts, "success"): { "eos_data": [ { "unmappedVlans": [], @@ -140,11 +115,9 @@ DATA: list[dict[str, Any]] = [ }, ], "inputs": {"vlans": [10, 20]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-vlan-not-in-topology", # Should it succeed really ? TODO - this output should be impossible - "test": VerifySTPForwardingPorts, + (VerifySTPForwardingPorts, "success-vlan-not-in-topology"): { "eos_data": [ { "unmappedVlans": [], @@ -156,18 +129,14 @@ DATA: list[dict[str, Any]] = [ }, ], "inputs": {"vlans": [10, 20]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-instances", - "test": VerifySTPForwardingPorts, + (VerifySTPForwardingPorts, "failure-no-instances"): { "eos_data": [{"unmappedVlans": [], "topologies": {}}, {"unmappedVlans": [], "topologies": {}}], "inputs": {"vlans": [10, 20]}, - "expected": {"result": "failure", "messages": ["VLAN 10 - STP instance is not configured", "VLAN 20 - STP instance is not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN 10 - STP instance is not configured", "VLAN 20 - STP instance is not configured"]}, }, - { - "name": "failure", - "test": VerifySTPForwardingPorts, + (VerifySTPForwardingPorts, "failure"): { "eos_data": [ { "unmappedVlans": [], @@ -180,16 +149,14 @@ DATA: list[dict[str, Any]] = [ ], "inputs": {"vlans": [10, 20]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VLAN 10 Interface: Ethernet10 - Invalid state - Expected: forwarding Actual: discarding", "VLAN 20 Interface: Ethernet10 - Invalid state - Expected: forwarding Actual: discarding", ], }, }, - { - "name": "success-specific-instances", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "success-specific-instances"): { "eos_data": [ { "instances": { @@ -201,7 +168,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL20": { "rootBridge": { @@ -211,7 +178,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL30": { "rootBridge": { @@ -221,17 +188,15 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, - }, - }, + } + } ], "inputs": {"priority": 32768, "instances": [10, 20]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-all-instances", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "success-all-instances"): { "eos_data": [ { "instances": { @@ -243,7 +208,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL20": { "rootBridge": { @@ -253,7 +218,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL30": { "rootBridge": { @@ -263,17 +228,15 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, - }, - }, + } + } ], "inputs": {"priority": 32768}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-MST", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "success-MST"): { "eos_data": [ { "instances": { @@ -285,17 +248,15 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, - }, - }, - }, + } + } + } + } ], "inputs": {"priority": 16384, "instances": [0]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-input-instance-none", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "success-input-instance-none"): { "eos_data": [ { "instances": { @@ -307,17 +268,15 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, - }, - }, - }, + } + } + } + } ], "inputs": {"priority": 16384}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-instances", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "failure-no-instances"): { "eos_data": [ { "instances": { @@ -329,24 +288,20 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, - }, - }, - }, + } + } + } + } ], "inputs": {"priority": 32768, "instances": [0]}, - "expected": {"result": "failure", "messages": ["STP Instance: WRONG0 - Unsupported STP instance type"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STP Instance: WRONG0 - Unsupported STP instance type"]}, }, - { - "name": "failure-wrong-instance-type", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "failure-wrong-instance-type"): { "eos_data": [{"instances": {}}], "inputs": {"priority": 32768, "instances": [10, 20]}, - "expected": {"result": "failure", "messages": ["No STP instances configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No STP instances configured"]}, }, - { - "name": "failure-instance-not-found", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "failure-instance-not-found"): { "eos_data": [ { "instances": { @@ -358,17 +313,15 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } } } } ], "inputs": {"priority": 32768, "instances": [11, 20]}, - "expected": {"result": "failure", "messages": ["Instance: VL11 - Not configured", "Instance: VL20 - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Instance: VL11 - Not configured", "Instance: VL20 - Not configured"]}, }, - { - "name": "failure-wrong-priority", - "test": VerifySTPRootPriority, + (VerifySTPRootPriority, "failure-wrong-priority"): { "eos_data": [ { "instances": { @@ -380,7 +333,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL20": { "rootBridge": { @@ -390,7 +343,7 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, "VL30": { "rootBridge": { @@ -400,23 +353,21 @@ DATA: list[dict[str, Any]] = [ "helloTime": 2.0, "maxAge": 20, "forwardDelay": 15, - }, + } }, - }, - }, + } + } ], "inputs": {"priority": 32768, "instances": [10, 20, 30]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "STP Instance: VL20 - Incorrect root priority - Expected: 32768 Actual: 8196", "STP Instance: VL30 - Incorrect root priority - Expected: 32768 Actual: 8196", ], }, }, - { - "name": "success-mstp", - "test": VerifyStpTopologyChanges, + (VerifyStpTopologyChanges, "success-mstp"): { "eos_data": [ { "unmappedVlans": [], @@ -434,14 +385,12 @@ DATA: list[dict[str, Any]] = [ } }, }, - }, + } ], "inputs": {"threshold": 10}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-rstp", - "test": VerifyStpTopologyChanges, + (VerifyStpTopologyChanges, "success-rstp"): { "eos_data": [ { "unmappedVlans": [], @@ -459,23 +408,19 @@ DATA: list[dict[str, Any]] = [ } }, }, - }, + } ], "inputs": {"threshold": 10}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-rapid-pvst", - "test": VerifyStpTopologyChanges, + (VerifyStpTopologyChanges, "success-rapid-pvst"): { "eos_data": [ { "unmappedVlans": [], "topologies": { "NoStp": { "vlans": [4094, 4093, 1006], - "interfaces": { - "PeerEthernet2": {"state": "forwarding", "numChanges": 1, "lastChange": 1727151356.1330667}, - }, + "interfaces": {"PeerEthernet2": {"state": "forwarding", "numChanges": 1, "lastChange": 1727151356.1330667}}, }, "Vl1": {"vlans": [1], "interfaces": {"Port-Channel5": {"state": "forwarding", "numChanges": 1, "lastChange": 1727326710.0615358}}}, "Vl10": { @@ -527,14 +472,12 @@ DATA: list[dict[str, Any]] = [ }, }, }, - }, + } ], "inputs": {"threshold": 10}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-unstable-topology", - "test": VerifyStpTopologyChanges, + (VerifyStpTopologyChanges, "failure-unstable-topology"): { "eos_data": [ { "unmappedVlans": [], @@ -544,22 +487,20 @@ DATA: list[dict[str, Any]] = [ "Cpu": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.735365}, "Port-Channel5": {"state": "forwarding", "numChanges": 15, "lastChange": 1723990624.7353542}, } - }, + } }, - }, + } ], "inputs": {"threshold": 10}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Topology: Cist Interface: Cpu - Number of changes not within the threshold - Expected: 10 Actual: 15", "Topology: Cist Interface: Port-Channel5 - Number of changes not within the threshold - Expected: 10 Actual: 15", ], }, }, - { - "name": "failure-topologies-not-configured", - "test": VerifyStpTopologyChanges, + (VerifyStpTopologyChanges, "failure-topologies-not-configured"): { "eos_data": [ { "unmappedVlans": [], @@ -571,28 +512,22 @@ DATA: list[dict[str, Any]] = [ } } }, - }, + } ], "inputs": {"threshold": 10}, - "expected": {"result": "failure", "messages": ["STP is not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STP is not configured"]}, }, - { - "name": "success", - "test": VerifySTPDisabledVlans, + (VerifySTPDisabledVlans, "success"): { "eos_data": [{"spanningTreeVlanInstances": {"1": {"spanningTreeVlanInstance": {"protocol": "mstp", "bridge": {"priority": 32768}}}, "6": {}, "4094": {}}}], "inputs": {"vlans": ["6", "4094"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-stp-not-configured", - "test": VerifySTPDisabledVlans, + (VerifySTPDisabledVlans, "failure-stp-not-configured"): { "eos_data": [{"spanningTreeVlanInstances": {}}], "inputs": {"vlans": ["6", "4094"]}, - "expected": {"result": "failure", "messages": ["STP is not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STP is not configured"]}, }, - { - "name": "failure-vlans-not-found", - "test": VerifySTPDisabledVlans, + (VerifySTPDisabledVlans, "failure-vlans-not-found"): { "eos_data": [ { "spanningTreeVlanInstances": { @@ -603,11 +538,9 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"vlans": ["16", "4093"]}, - "expected": {"result": "failure", "messages": ["VLAN: 16 - Not configured", "VLAN: 4093 - Not configured"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN: 16 - Not configured", "VLAN: 4093 - Not configured"]}, }, - { - "name": "failure-vlans-enabled", - "test": VerifySTPDisabledVlans, + (VerifySTPDisabledVlans, "failure-vlans-enabled"): { "eos_data": [ { "spanningTreeVlanInstances": { @@ -618,6 +551,6 @@ DATA: list[dict[str, Any]] = [ } ], "inputs": {"vlans": ["6", "4094"]}, - "expected": {"result": "failure", "messages": ["VLAN: 6 - STP is enabled", "VLAN: 4094 - STP is enabled"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN: 6 - STP is enabled", "VLAN: 4094 - STP is enabled"]}, }, -] +} diff --git a/tests/units/anta_tests/test_stun.py b/tests/units/anta_tests/test_stun.py index 0d1363c..a0c1001 100644 --- a/tests/units/anta_tests/test_stun.py +++ b/tests/units/anta_tests/test_stun.py @@ -5,48 +5,24 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.stun import VerifyStunClientTranslation, VerifyStunServer from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyStunClientTranslation, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyStunClientTranslation, "success"): { "eos_data": [ - { - "bindings": { - "000000010a64ff0100000000": { - "sourceAddress": {"ip": "100.64.3.2", "port": 4500}, - "publicAddress": {"ip": "192.64.3.2", "port": 6006}, - } - } - }, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.3.2", "port": 4500}, - "publicAddress": {"ip": "192.18.3.2", "port": 6006}, - } - } - }, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.4.2", "port": 4500}, - "publicAddress": {"ip": "192.18.4.2", "port": 6006}, - } - } - }, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.6.2", "port": 4500}, - "publicAddress": {"ip": "192.18.6.2", "port": 6006}, - } - } - }, + {"bindings": {"000000010a64ff0100000000": {"sourceAddress": {"ip": "100.64.3.2", "port": 4500}, "publicAddress": {"ip": "192.64.3.2", "port": 6006}}}}, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.3.2", "port": 4500}, "publicAddress": {"ip": "192.18.3.2", "port": 6006}}}}, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.4.2", "port": 4500}, "publicAddress": {"ip": "192.18.4.2", "port": 6006}}}}, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.6.2", "port": 4500}, "publicAddress": {"ip": "192.18.6.2", "port": 6006}}}}, ], "inputs": { "stun_clients": [ @@ -56,28 +32,12 @@ DATA: list[dict[str, Any]] = [ {"source_address": "172.18.6.2", "source_port": 4500, "public_port": 6006}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-public-ip", - "test": VerifyStunClientTranslation, + (VerifyStunClientTranslation, "failure-incorrect-public-ip"): { "eos_data": [ - { - "bindings": { - "000000010a64ff0100000000": { - "sourceAddress": {"ip": "100.64.3.2", "port": 4500}, - "publicAddress": {"ip": "192.64.3.2", "port": 6006}, - } - } - }, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.3.2", "port": 4500}, - "publicAddress": {"ip": "192.18.3.2", "port": 6006}, - } - } - }, + {"bindings": {"000000010a64ff0100000000": {"sourceAddress": {"ip": "100.64.3.2", "port": 4500}, "publicAddress": {"ip": "192.64.3.2", "port": 6006}}}}, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.3.2", "port": 4500}, "publicAddress": {"ip": "192.18.3.2", "port": 6006}}}}, ], "inputs": { "stun_clients": [ @@ -86,20 +46,15 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.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", ], }, }, - { - "name": "failure-no-client", - "test": VerifyStunClientTranslation, - "eos_data": [ - {"bindings": {}}, - {"bindings": {}}, - ], + (VerifyStunClientTranslation, "failure-no-client"): { + "eos_data": [{"bindings": {}}, {"bindings": {}}], "inputs": { "stun_clients": [ {"source_address": "100.64.3.2", "public_address": "192.164.3.2", "source_port": 4500, "public_port": 6006}, @@ -107,23 +62,14 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "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"], }, }, - { - "name": "failure-incorrect-public-port", - "test": VerifyStunClientTranslation, + (VerifyStunClientTranslation, "failure-incorrect-public-port"): { "eos_data": [ {"bindings": {}}, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.3.2", "port": 4500}, - "publicAddress": {"ip": "192.18.3.2", "port": 4800}, - } - } - }, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.3.2", "port": 4500}, "publicAddress": {"ip": "192.18.3.2", "port": 4800}}}}, ], "inputs": { "stun_clients": [ @@ -132,7 +78,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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", @@ -140,19 +86,10 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "failure-all-type", - "test": VerifyStunClientTranslation, + (VerifyStunClientTranslation, "failure-all-type"): { "eos_data": [ {"bindings": {}}, - { - "bindings": { - "000000040a64ff0100000000": { - "sourceAddress": {"ip": "172.18.3.2", "port": 4500}, - "publicAddress": {"ip": "192.18.3.2", "port": 4800}, - } - } - }, + {"bindings": {"000000040a64ff0100000000": {"sourceAddress": {"ip": "172.18.3.2", "port": 4500}, "publicAddress": {"ip": "192.18.3.2", "port": 4800}}}}, ], "inputs": { "stun_clients": [ @@ -161,7 +98,7 @@ DATA: list[dict[str, Any]] = [ ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "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", @@ -169,61 +106,20 @@ DATA: list[dict[str, Any]] = [ ], }, }, - { - "name": "success", - "test": VerifyStunServer, - "eos_data": [ - { - "enabled": True, - "pid": 1895, - } - ], + (VerifyStunServer, "success"): {"eos_data": [{"enabled": True, "pid": 1895}], "inputs": {}, "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyStunServer, "failure-disabled"): { + "eos_data": [{"enabled": False, "pid": 1895}], "inputs": {}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STUN server status is disabled"]}, }, - { - "name": "failure-disabled", - "test": VerifyStunServer, - "eos_data": [ - { - "enabled": False, - "pid": 1895, - } - ], + (VerifyStunServer, "failure-not-running"): { + "eos_data": [{"enabled": True, "pid": 0}], "inputs": {}, - "expected": { - "result": "failure", - "messages": ["STUN server status is disabled"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STUN server is not running"]}, }, - { - "name": "failure-not-running", - "test": VerifyStunServer, - "eos_data": [ - { - "enabled": True, - "pid": 0, - } - ], + (VerifyStunServer, "failure-not-running-disabled"): { + "eos_data": [{"enabled": False, "pid": 0}], "inputs": {}, - "expected": { - "result": "failure", - "messages": ["STUN server is not running"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["STUN server status is disabled and not running"]}, }, - { - "name": "failure-not-running-disabled", - "test": VerifyStunServer, - "eos_data": [ - { - "enabled": False, - "pid": 0, - } - ], - "inputs": {}, - "expected": { - "result": "failure", - "messages": ["STUN server status is disabled and not running"], - }, - }, -] +} diff --git a/tests/units/anta_tests/test_system.py b/tests/units/anta_tests/test_system.py index 0522029..3046846 100644 --- a/tests/units/anta_tests/test_system.py +++ b/tests/units/anta_tests/test_system.py @@ -5,8 +5,11 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.system import ( VerifyAgentLogs, VerifyCoredump, @@ -21,127 +24,137 @@ from anta.tests.system import ( ) from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyUptime, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyUptime, "success"): { "eos_data": [{"upTime": 1186689.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], "inputs": {"minimum": 666}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyUptime, + (VerifyUptime, "failure"): { "eos_data": [{"upTime": 665.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], "inputs": {"minimum": 666}, - "expected": {"result": "failure", "messages": ["Device uptime is incorrect - Expected: 666s Actual: 665.15s"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device uptime is incorrect - Expected: 666s Actual: 665.15s"]}, }, - { - "name": "success-no-reload", - "test": VerifyReloadCause, + (VerifyReloadCause, "success-no-reload"): { "eos_data": [{"kernelCrashData": [], "resetCauses": [], "full": False}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-valid-cause", - "test": VerifyReloadCause, + (VerifyReloadCause, "success-valid-cause-user"): { + "eos_data": [ + { + "resetCauses": [ + {"recommendedAction": "No action necessary.", "description": "Reload requested by the user.", "timestamp": 1683186892.0, "debugInfoIsDir": False} + ], + "full": False, + } + ], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyReloadCause, "success-valid-reload-cause-ztp"): { "eos_data": [ { "resetCauses": [ { + "description": "System reloaded due to Zero Touch Provisioning", + "timestamp": 1729856740.0, "recommendedAction": "No action necessary.", - "description": "Reload requested by the user.", - "timestamp": 1683186892.0, "debugInfoIsDir": False, - }, + } ], "full": False, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "inputs": {"allowed_causes": ["ZTP"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyReloadCause, - # The failure cause is made up + (VerifyReloadCause, "success-valid-reload-cause-fpga"): { "eos_data": [ { "resetCauses": [ - {"recommendedAction": "No action necessary.", "description": "Reload after crash.", "timestamp": 1683186892.0, "debugInfoIsDir": False}, + { + "description": "Reload requested after FPGA upgrade", + "timestamp": 1729856740.0, + "recommendedAction": "No action necessary.", + "debugInfoIsDir": False, + } ], "full": False, - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Reload cause is: Reload after crash."]}, + "inputs": {"allowed_causes": ["fpga"]}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-without-minidump", - "test": VerifyCoredump, - "eos_data": [{"mode": "compressedDeferred", "coreFiles": []}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "success-with-minidump", - "test": VerifyCoredump, - "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["minidump"]}], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure-without-minidump", - "test": VerifyCoredump, - "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["core.2344.1584483862.Mlag.gz", "core.23101.1584483867.Mlag.gz"]}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Core dump(s) have been found: core.2344.1584483862.Mlag.gz, core.23101.1584483867.Mlag.gz"]}, - }, - { - "name": "failure-with-minidump", - "test": VerifyCoredump, - "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["minidump", "core.2344.1584483862.Mlag.gz", "core.23101.1584483867.Mlag.gz"]}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Core dump(s) have been found: core.2344.1584483862.Mlag.gz, core.23101.1584483867.Mlag.gz"]}, - }, - { - "name": "success", - "test": VerifyAgentLogs, - "eos_data": [""], - "inputs": None, - "expected": {"result": "success"}, - }, - { - "name": "failure", - "test": VerifyAgentLogs, + (VerifyReloadCause, "failure-invalid-reload-cause"): { "eos_data": [ - """===> /var/log/agents/Test-666 Thu May 4 09:57:02 2023 <=== -CLI Exception: Exception -CLI Exception: Backtrace -===> /var/log/agents/Aaa-855 Fri Jul 7 15:07:00 2023 <=== -===== Output from /usr/bin/Aaa [] (PID=855) started Jul 7 15:06:11.606414 === -EntityManager::doBackoff waiting for remote sysdb version ....ok - -===> /var/log/agents/Acl-830 Fri Jul 7 15:07:00 2023 <=== -===== Output from /usr/bin/Acl [] (PID=830) started Jul 7 15:06:10.871700 === -EntityManager::doBackoff waiting for remote sysdb version ...................ok -""", + { + "resetCauses": [ + { + "description": "Reload requested after FPGA upgrade", + "timestamp": 1729856740.0, + "recommendedAction": "No action necessary.", + "debugInfoIsDir": False, + } + ], + "full": False, + } ], - "inputs": None, + "inputs": {"allowed_causes": ["ZTP"]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, + "messages": ["Invalid reload cause - Expected: 'System reloaded due to Zero Touch Provisioning' Actual: 'Reload requested after FPGA upgrade'"], + }, + }, + (VerifyReloadCause, "failure"): { + "eos_data": [ + { + "resetCauses": [ + {"recommendedAction": "No action necessary.", "description": "Reload after crash.", "timestamp": 1683186892.0, "debugInfoIsDir": False} + ], + "full": False, + } + ], + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Invalid reload cause - Expected: 'Reload requested by the user.', 'Reload requested after FPGA upgrade' Actual: 'Reload after crash.'"], + }, + }, + (VerifyCoredump, "success-without-minidump"): { + "eos_data": [{"mode": "compressedDeferred", "coreFiles": []}], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyCoredump, "success-with-minidump"): { + "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["minidump"]}], + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyCoredump, "failure-without-minidump"): { + "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["core.2344.1584483862.Mlag.gz", "core.23101.1584483867.Mlag.gz"]}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Core dump(s) have been found: core.2344.1584483862.Mlag.gz, core.23101.1584483867.Mlag.gz"]}, + }, + (VerifyCoredump, "failure-with-minidump"): { + "eos_data": [{"mode": "compressedDeferred", "coreFiles": ["minidump", "core.2344.1584483862.Mlag.gz", "core.23101.1584483867.Mlag.gz"]}], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Core dump(s) have been found: core.2344.1584483862.Mlag.gz, core.23101.1584483867.Mlag.gz"]}, + }, + (VerifyAgentLogs, "success"): {"eos_data": [""], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyAgentLogs, "failure"): { + "eos_data": [ + "===> /var/log/agents/Test-666 Thu May 4 09:57:02 2023 <===\nCLI Exception: Exception\nCLI Exception: Backtrace\n===> /var/log/agents/Aaa-855" + " Fri Jul 7 15:07:00 2023 <===\n===== Output from /usr/bin/Aaa [] (PID=855) started Jul 7 15:06:11.606414 ===\n" + "EntityManager::doBackoff waiting for remote sysdb version ....ok\n\n===> /var/log/agents/Acl-830" + " Fri Jul 7 15:07:00 2023 <===\n===== Output from /usr/bin/Acl [] (PID=830) started Jul 7 15:06:10.871700 ===\n" + "EntityManager::doBackoff waiting for remote sysdb version ...................ok\n" + ], + "expected": { + "result": AntaTestStatus.FAILURE, "messages": [ - "Device has reported agent crashes:\n" - " * /var/log/agents/Test-666 Thu May 4 09:57:02 2023\n" - " * /var/log/agents/Aaa-855 Fri Jul 7 15:07:00 2023\n" - " * /var/log/agents/Acl-830 Fri Jul 7 15:07:00 2023", + "Device has reported agent crashes:\n * /var/log/agents/Test-666 Thu May 4 09:57:02 2023\n" + " * /var/log/agents/Aaa-855 Fri Jul 7 15:07:00 2023\n * /var/log/agents/Acl-830 Fri Jul 7 15:07:00 2023" ], }, }, - { - "name": "success", - "test": VerifyCPUUtilization, + (VerifyCPUUtilization, "success"): { "eos_data": [ { "cpuInfo": {"%Cpu(s)": {"idle": 88.2, "stolen": 0.0, "user": 5.9, "swIrq": 0.0, "ioWait": 0.0, "system": 0.0, "hwIrq": 5.9, "nice": 0.0}}, @@ -159,16 +172,13 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok "activeTime": 360, "virtMem": "6644", "sharedMem": "3996", - }, + } }, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyCPUUtilization, + (VerifyCPUUtilization, "failure"): { "eos_data": [ { "cpuInfo": {"%Cpu(s)": {"idle": 24.8, "stolen": 0.0, "user": 5.9, "swIrq": 0.0, "ioWait": 0.0, "system": 0.0, "hwIrq": 5.9, "nice": 0.0}}, @@ -186,122 +196,56 @@ EntityManager::doBackoff waiting for remote sysdb version ...................ok "activeTime": 360, "virtMem": "6644", "sharedMem": "3996", - }, + } }, - }, + } ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device has reported a high CPU utilization - Expected: < 75% Actual: 75.2%"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device has reported a high CPU utilization - Expected: < 75% Actual: 75.2%"]}, }, - { - "name": "success", - "test": VerifyMemoryUtilization, + (VerifyMemoryUtilization, "success"): { "eos_data": [ - { - "uptime": 1994.67, - "modelName": "vEOS-lab", - "internalVersion": "4.27.3F-26379303.4273F", - "memTotal": 2004568, - "memFree": 879004, - "version": "4.27.3F", - }, + {"uptime": 1994.67, "modelName": "vEOS-lab", "internalVersion": "4.27.3F-26379303.4273F", "memTotal": 2004568, "memFree": 879004, "version": "4.27.3F"} ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyMemoryUtilization, + (VerifyMemoryUtilization, "failure"): { "eos_data": [ - { - "uptime": 1994.67, - "modelName": "vEOS-lab", - "internalVersion": "4.27.3F-26379303.4273F", - "memTotal": 2004568, - "memFree": 89004, - "version": "4.27.3F", - }, + {"uptime": 1994.67, "modelName": "vEOS-lab", "internalVersion": "4.27.3F-26379303.4273F", "memTotal": 2004568, "memFree": 89004, "version": "4.27.3F"} ], - "inputs": None, - "expected": {"result": "failure", "messages": ["Device has reported a high memory usage - Expected: < 75% Actual: 95.56%"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Device has reported a high memory usage - Expected: < 75% Actual: 95.56%"]}, }, - { - "name": "success", - "test": VerifyFileSystemUtilization, + (VerifyFileSystemUtilization, "success"): { "eos_data": [ - """Filesystem Size Used Avail Use% Mounted on -/dev/sda2 3.9G 988M 2.9G 26% /mnt/flash -none 294M 78M 217M 27% / -none 294M 78M 217M 27% /.overlay -/dev/loop0 461M 461M 0 100% /rootfs-i386 -""", + "Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 3.9G 988M 2.9G 26% /mnt/flash\nnone 294M 78M 217M 27% /\n" + "none 294M 78M 217M 27% /.overlay\n/dev/loop0 461M 461M 0 100% /rootfs-i386\n" ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyFileSystemUtilization, + (VerifyFileSystemUtilization, "failure"): { "eos_data": [ - """Filesystem Size Used Avail Use% Mounted on -/dev/sda2 3.9G 988M 2.9G 84% /mnt/flash -none 294M 78M 217M 27% / -none 294M 78M 217M 84% /.overlay -/dev/loop0 461M 461M 0 100% /rootfs-i386 -""", + "Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 3.9G 988M 2.9G 84% /mnt/flash\nnone 294M 78M 217M 27% /\n" + "none 294M 78M 217M 84% /.overlay\n/dev/loop0 461M 461M 0 100% /rootfs-i386\n" ], - "inputs": None, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Mount point: /dev/sda2 3.9G 988M 2.9G 84% /mnt/flash - Higher disk space utilization - Expected: 75% Actual: 84%", "Mount point: none 294M 78M 217M 84% /.overlay - Higher disk space utilization - Expected: 75% Actual: 84%", ], }, }, - { - "name": "success", - "test": VerifyNTP, - "eos_data": [ - """synchronised -poll interval unknown -""", - ], - "inputs": None, - "expected": {"result": "success"}, + (VerifyNTP, "success"): {"eos_data": ["synchronised\npoll interval unknown\n"], "expected": {"result": AntaTestStatus.SUCCESS}}, + (VerifyNTP, "failure"): { + "eos_data": ["unsynchronised\npoll interval unknown\n"], + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["NTP status mismatch - Expected: synchronised Actual: unsynchronised"]}, }, - { - "name": "failure", - "test": VerifyNTP, - "eos_data": [ - """unsynchronised -poll interval unknown -""", - ], - "inputs": None, - "expected": {"result": "failure", "messages": ["NTP status mismatch - Expected: synchronised Actual: unsynchronised"]}, - }, - { - "name": "success", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "success"): { "eos_data": [ { "peers": { - "1.1.1.1": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "2.2.2.2": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "3.3.3.3": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "1.1.1.1": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "2.2.2.2": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "3.3.3.3": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], @@ -312,29 +256,15 @@ poll interval unknown {"server_address": "3.3.3.3", "stratum": 2}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-pool-name", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "success-pool-name"): { "eos_data": [ { "peers": { - "1.ntp.networks.com": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "2.ntp.networks.com": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "3.ntp.networks.com": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "1.ntp.networks.com": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "2.ntp.networks.com": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "3.ntp.networks.com": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], @@ -345,56 +275,28 @@ poll interval unknown {"server_address": "3.ntp.networks.com", "stratum": 2}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ntp-pool-as-input", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "success-ntp-pool-as-input"): { "eos_data": [ { "peers": { - "1.1.1.1": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "2.2.2.2": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "3.3.3.3": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "1.1.1.1": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "2.2.2.2": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "3.3.3.3": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], "inputs": {"ntp_pool": {"server_addresses": ["1.1.1.1", "2.2.2.2", "3.3.3.3"], "preferred_stratum_range": [1, 2]}}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ntp-pool-hostname", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "success-ntp-pool-hostname"): { "eos_data": [ { "peers": { - "itsys-ntp010p.aristanetworks.com": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "itsys-ntp011p.aristanetworks.com": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "itsys-ntp012p.aristanetworks.com": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "itsys-ntp010p.aristanetworks.com": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "itsys-ntp011p.aristanetworks.com": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "itsys-ntp012p.aristanetworks.com": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], @@ -404,29 +306,15 @@ poll interval unknown "preferred_stratum_range": [1, 2], } }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-ip-dns", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "success-ip-dns"): { "eos_data": [ { "peers": { - "1.1.1.1 (1.ntp.networks.com)": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "2.2.2.2 (2.ntp.networks.com)": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "3.3.3.3 (3.ntp.networks.com)": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "1.1.1.1 (1.ntp.networks.com)": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "2.2.2.2 (2.ntp.networks.com)": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "3.3.3.3 (3.ntp.networks.com)": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], @@ -437,29 +325,15 @@ poll interval unknown {"server_address": "3.3.3.3", "stratum": 2}, ] }, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-ntp-server", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-ntp-server"): { "eos_data": [ { "peers": { - "1.1.1.1": { - "condition": "candidate", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 2, - }, - "2.2.2.2": { - "condition": "sys.peer", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "3.3.3.3": { - "condition": "sys.peer", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 3, - }, + "1.1.1.1": {"condition": "candidate", "peerIpAddr": "1.1.1.1", "stratumLevel": 2}, + "2.2.2.2": {"condition": "sys.peer", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "3.3.3.3": {"condition": "sys.peer", "peerIpAddr": "3.3.3.3", "stratumLevel": 3}, } } ], @@ -471,7 +345,7 @@ poll interval unknown ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Incorrect condition - Expected: sys.peer Actual: candidate", "NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Incorrect stratum level - Expected: 1 Actual: 2", @@ -481,9 +355,7 @@ poll interval unknown ], }, }, - { - "name": "failure-no-peers", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-no-peers"): { "eos_data": [{"peers": {}}], "inputs": { "ntp_servers": [ @@ -492,27 +364,14 @@ poll interval unknown {"server_address": "3.3.3.3", "stratum": 1}, ] }, - "expected": { - "result": "failure", - "messages": ["No NTP peers configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["No NTP peers configured"]}, }, - { - "name": "failure-one-peer-not-found", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-one-peer-not-found"): { "eos_data": [ { "peers": { - "1.1.1.1": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "2.2.2.2": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 1, - }, + "1.1.1.1": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "2.2.2.2": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 1}, } } ], @@ -523,25 +382,10 @@ poll interval unknown {"server_address": "3.3.3.3", "stratum": 1}, ] }, - "expected": { - "result": "failure", - "messages": ["NTP Server: 3.3.3.3 Preferred: False Stratum: 1 - Not configured"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["NTP Server: 3.3.3.3 Preferred: False Stratum: 1 - Not configured"]}, }, - { - "name": "failure-with-two-peers-not-found", - "test": VerifyNTPAssociations, - "eos_data": [ - { - "peers": { - "1.1.1.1": { - "condition": "candidate", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - } - } - } - ], + (VerifyNTPAssociations, "failure-with-two-peers-not-found"): { + "eos_data": [{"peers": {"1.1.1.1": {"condition": "candidate", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}}}], "inputs": { "ntp_servers": [ {"server_address": "1.1.1.1", "preferred": True, "stratum": 1}, @@ -550,7 +394,7 @@ poll interval unknown ] }, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Incorrect condition - Expected: sys.peer Actual: candidate", "NTP Server: 2.2.2.2 Preferred: False Stratum: 1 - Not configured", @@ -558,96 +402,51 @@ poll interval unknown ], }, }, - { - "name": "failure-ntp-pool-as-input", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-ntp-pool-as-input"): { "eos_data": [ { "peers": { - "ntp1.pool": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "ntp2.pool": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "ntp3.pool": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "ntp1.pool": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "ntp2.pool": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "ntp3.pool": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], "inputs": {"ntp_pool": {"server_addresses": ["1.1.1.1", "2.2.2.2"], "preferred_stratum_range": [1, 2]}}, - "expected": { - "result": "failure", - "messages": ["NTP Server: 3.3.3.3 Hostname: ntp3.pool - Associated but not part of the provided NTP pool"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["NTP Server: 3.3.3.3 Hostname: ntp3.pool - Associated but not part of the provided NTP pool"]}, }, - { - "name": "failure-ntp-pool-as-input-bad-association", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-ntp-pool-as-input-bad-association"): { "eos_data": [ { "peers": { - "ntp1.pool": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 1, - }, - "ntp2.pool": { - "condition": "candidate", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 2, - }, - "ntp3.pool": { - "condition": "reject", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 3, - }, + "ntp1.pool": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 1}, + "ntp2.pool": {"condition": "candidate", "peerIpAddr": "2.2.2.2", "stratumLevel": 2}, + "ntp3.pool": {"condition": "reject", "peerIpAddr": "3.3.3.3", "stratumLevel": 3}, } } ], "inputs": {"ntp_pool": {"server_addresses": ["1.1.1.1", "2.2.2.2", "3.3.3.3"], "preferred_stratum_range": [1, 2]}}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "NTP Server: 3.3.3.3 Hostname: ntp3.pool - Incorrect condition - Expected: sys.peer, candidate Actual: reject", "NTP Server: 3.3.3.3 Hostname: ntp3.pool - Incorrect stratum level - Expected Stratum Range: 1 to 2 Actual: 3", ], }, }, - { - "name": "failure-ntp-pool-hostname", - "test": VerifyNTPAssociations, + (VerifyNTPAssociations, "failure-ntp-pool-hostname"): { "eos_data": [ { "peers": { - "itsys-ntp010p.aristanetworks.com": { - "condition": "sys.peer", - "peerIpAddr": "1.1.1.1", - "stratumLevel": 5, - }, - "itsys-ntp011p.aristanetworks.com": { - "condition": "reject", - "peerIpAddr": "2.2.2.2", - "stratumLevel": 4, - }, - "itsys-ntp012p.aristanetworks.com": { - "condition": "candidate", - "peerIpAddr": "3.3.3.3", - "stratumLevel": 2, - }, + "itsys-ntp010p.aristanetworks.com": {"condition": "sys.peer", "peerIpAddr": "1.1.1.1", "stratumLevel": 5}, + "itsys-ntp011p.aristanetworks.com": {"condition": "reject", "peerIpAddr": "2.2.2.2", "stratumLevel": 4}, + "itsys-ntp012p.aristanetworks.com": {"condition": "candidate", "peerIpAddr": "3.3.3.3", "stratumLevel": 2}, } } ], "inputs": {"ntp_pool": {"server_addresses": ["itsys-ntp010p.aristanetworks.com", "itsys-ntp011p.aristanetworks.com"], "preferred_stratum_range": [1, 2]}}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "NTP Server: 1.1.1.1 Hostname: itsys-ntp010p.aristanetworks.com - Incorrect stratum level - Expected Stratum Range: 1 to 2 Actual: 5", "NTP Server: 2.2.2.2 Hostname: itsys-ntp011p.aristanetworks.com - Incorrect condition - Expected: sys.peer, candidate Actual: reject", @@ -656,23 +455,11 @@ poll interval unknown ], }, }, - { - "name": "success-no-maintenance-configured", - "test": VerifyMaintenance, - "eos_data": [ - { - "units": {}, - "interfaces": {}, - "vrfs": {}, - "warnings": ["Maintenance Mode is disabled."], - }, - ], - "inputs": None, - "expected": {"result": "success"}, + (VerifyMaintenance, "success-no-maintenance-configured"): { + "eos_data": [{"units": {}, "interfaces": {}, "vrfs": {}, "warnings": ["Maintenance Mode is disabled."]}], + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-maintenance-configured-but-not-enabled", - "test": VerifyMaintenance, + (VerifyMaintenance, "success-maintenance-configured-but-not-enabled"): { "eos_data": [ { "units": { @@ -688,14 +475,11 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "success-multiple-units-but-not-enabled", - "test": VerifyMaintenance, + (VerifyMaintenance, "success-multiple-units-but-not-enabled"): { "eos_data": [ { "units": { @@ -720,14 +504,11 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-maintenance-enabled", - "test": VerifyMaintenance, + (VerifyMaintenance, "failure-maintenance-enabled"): { "eos_data": [ { "units": { @@ -752,20 +533,11 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": [ - "Units under maintenance: 'mlag'.", - "Possible causes: 'Quiesce is configured'.", - ], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Units under maintenance: 'mlag'", "Possible causes: 'Quiesce is configured'"]}, }, - { - "name": "failure-multiple-reasons", - "test": VerifyMaintenance, + (VerifyMaintenance, "failure-multiple-reasons"): { "eos_data": [ { "units": { @@ -790,21 +562,14 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, "expected": { - "result": "failure", - "messages": [ - "Units under maintenance: 'mlag'.", - "Units entering maintenance: 'System'.", - "Possible causes: 'Quiesce is configured'.", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Units under maintenance: 'mlag'", "Units entering maintenance: 'System'", "Possible causes: 'Quiesce is configured'"], }, }, - { - "name": "failure-onboot-maintenance", - "test": VerifyMaintenance, + (VerifyMaintenance, "failure-onboot-maintenance"): { "eos_data": [ { "units": { @@ -820,20 +585,14 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, "expected": { - "result": "failure", - "messages": [ - "Units under maintenance: 'System'.", - "Possible causes: 'On-boot maintenance is configured, Quiesce is configured'.", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Units under maintenance: 'System'", "Possible causes: 'On-boot maintenance is configured, Quiesce is configured'"], }, }, - { - "name": "failure-entering-maintenance-interface-violation", - "test": VerifyMaintenance, + (VerifyMaintenance, "failure-entering-maintenance-interface-violation"): { "eos_data": [ { "units": { @@ -849,15 +608,11 @@ poll interval unknown }, "interfaces": {}, "vrfs": {}, - }, + } ], - "inputs": None, "expected": { - "result": "failure", - "messages": [ - "Units entering maintenance: 'System'.", - "Possible causes: 'Interface traffic threshold violation, Quiesce is configured'.", - ], + "result": AntaTestStatus.FAILURE, + "messages": ["Units entering maintenance: 'System'", "Possible causes: 'Interface traffic threshold violation, Quiesce is configured'"], }, }, -] +} diff --git a/tests/units/anta_tests/test_vlan.py b/tests/units/anta_tests/test_vlan.py index fd900ca..accc8ca 100644 --- a/tests/units/anta_tests/test_vlan.py +++ b/tests/units/anta_tests/test_vlan.py @@ -5,88 +5,109 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any -from anta.tests.vlan import VerifyDynamicVlanSource, VerifyVlanInternalPolicy +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus +from anta.tests.vlan import VerifyDynamicVlanSource, VerifyVlanInternalPolicy, VerifyVlanStatus from tests.units.anta_tests import test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyVlanInternalPolicy, +if TYPE_CHECKING: + from tests.units.anta_tests import AntaUnitTestDataDict + +DATA: AntaUnitTestDataDict = { + (VerifyVlanInternalPolicy, "success"): { "eos_data": [{"policy": "ascending", "startVlanId": 1006, "endVlanId": 4094}], "inputs": {"policy": "ascending", "start_vlan_id": 1006, "end_vlan_id": 4094}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-incorrect-policy", - "test": VerifyVlanInternalPolicy, + (VerifyVlanInternalPolicy, "failure-incorrect-policy"): { "eos_data": [{"policy": "descending", "startVlanId": 4094, "endVlanId": 1006}], "inputs": {"policy": "ascending", "start_vlan_id": 1006, "end_vlan_id": 4094}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": ["Incorrect VLAN internal allocation policy configured - Expected: ascending Actual: descending"], }, }, - { - "name": "failure-incorrect-start-end-id", - "test": VerifyVlanInternalPolicy, + (VerifyVlanInternalPolicy, "failure-incorrect-start-end-id"): { "eos_data": [{"policy": "ascending", "startVlanId": 4094, "endVlanId": 1006}], "inputs": {"policy": "ascending", "start_vlan_id": 1006, "end_vlan_id": 4094}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "VLAN internal allocation policy: ascending - Incorrect start VLAN id configured - Expected: 1006 Actual: 4094", "VLAN internal allocation policy: ascending - Incorrect end VLAN id configured - Expected: 4094 Actual: 1006", ], }, }, - { - "name": "success", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "success"): { "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1401]}, "vccbfd": {"vlanIds": [1501]}}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-dynamic-vlan-sources", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "failure-no-dynamic-vlan-sources"): { "eos_data": [{"dynamicVlans": {}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, - "expected": {"result": "failure", "messages": ["Dynamic VLAN source(s) not found in configuration: evpn, mlagsync"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dynamic VLAN source(s) not found in configuration: evpn, mlagsync"]}, }, - { - "name": "failure-dynamic-vlan-sources-mismatch", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "failure-dynamic-vlan-sources-mismatch"): { "eos_data": [{"dynamicVlans": {"vccbfd": {"vlanIds": [1500]}, "mlagsync": {"vlanIds": [1501]}}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": False}, - "expected": { - "result": "failure", - "messages": ["Dynamic VLAN source(s) not found in configuration: evpn"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dynamic VLAN source(s) not found in configuration: evpn"]}, }, - { - "name": "success-strict-mode", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "success-strict-mode"): { "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1502], "vccbfd": {"vlanIds": []}}}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-all-sources-exact-match-additional-source-found", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "failure-all-sources-exact-match-additional-source-found"): { "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": [1500]}, "vccbfd": {"vlanIds": [1500]}}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, - "expected": { - "result": "failure", - "messages": ["Strict mode enabled: Unexpected sources have VLANs allocated: vccbfd"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Strict mode enabled: Unexpected sources have VLANs allocated: vccbfd"]}, }, - { - "name": "failure-all-sources-exact-match-expected-source-not-found", - "test": VerifyDynamicVlanSource, + (VerifyDynamicVlanSource, "failure-all-sources-exact-match-expected-source-not-found"): { "eos_data": [{"dynamicVlans": {"evpn": {"vlanIds": [1199]}, "mlagsync": {"vlanIds": []}}}], "inputs": {"sources": ["evpn", "mlagsync"], "strict": True}, - "expected": {"result": "failure", "messages": ["Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync"]}, }, -] + (VerifyVlanStatus, "success"): { + "eos_data": [ + { + "vlans": { + "1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}}, + "4092": {"name": "VLAN4092", "dynamic": True, "status": "active", "interfaces": {}}, + "4094": {"name": "VLAN4094", "dynamic": True, "status": "active", "interfaces": {}}, + }, + "sourceDetail": "", + } + ], + "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "active"}]}, + "expected": {"result": AntaTestStatus.SUCCESS}, + }, + (VerifyVlanStatus, "failure-vlan-not-conifgured"): { + "eos_data": [{"vlans": {"1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}}}, "sourceDetail": ""}], + "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "active"}]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["VLAN: Vlan4092 - Not configured", "VLAN: Vlan4094 - Not configured"]}, + }, + (VerifyVlanStatus, "failure-incorrect-status"): { + "eos_data": [ + { + "vlans": { + "1": {"name": "default", "dynamic": False, "status": "active", "interfaces": {}}, + "4092": {"name": "VLAN4092", "dynamic": True, "status": "suspended", "interfaces": {}}, + "4094": {"name": "VLAN4094", "dynamic": True, "status": "active", "interfaces": {}}, + }, + "sourceDetail": "", + } + ], + "inputs": {"vlans": [{"vlan_id": 4092, "status": "active"}, {"vlan_id": 4094, "status": "suspended"}]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": [ + "VLAN: Vlan4092 - Incorrect administrative status - Expected: active Actual: suspended", + "VLAN: Vlan4094 - Incorrect administrative status - Expected: suspended Actual: active", + ], + }, + }, +} diff --git a/tests/units/anta_tests/test_vxlan.py b/tests/units/anta_tests/test_vxlan.py index ef8a294..e99c812 100644 --- a/tests/units/anta_tests/test_vxlan.py +++ b/tests/units/anta_tests/test_vxlan.py @@ -5,50 +5,47 @@ from __future__ import annotations -from typing import Any +import sys +from typing import TYPE_CHECKING, Any +from anta.models import AntaTest +from anta.result_manager.models import AntaTestStatus from anta.tests.vxlan import VerifyVxlan1ConnSettings, VerifyVxlan1Interface, VerifyVxlanConfigSanity, VerifyVxlanVniBinding, VerifyVxlanVtep -from tests.units.anta_tests import test +from tests.units.anta_tests import AntaUnitTest, test -DATA: list[dict[str, Any]] = [ - { - "name": "success", - "test": VerifyVxlan1Interface, +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + TypeAlias = type + + +AntaUnitTestDataDict: TypeAlias = dict[tuple[type[AntaTest], str], AntaUnitTest] + +DATA: AntaUnitTestDataDict = { + (VerifyVxlan1Interface, "success"): { "eos_data": [{"interfaceDescriptions": {"Vxlan1": {"lineProtocolStatus": "up", "interfaceStatus": "up"}}}], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped", - "test": VerifyVxlan1Interface, + (VerifyVxlan1Interface, "skipped"): { "eos_data": [{"interfaceDescriptions": {"Loopback0": {"lineProtocolStatus": "up", "interfaceStatus": "up"}}}], - "inputs": None, - "expected": {"result": "skipped", "messages": ["Interface: Vxlan1 - Not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Interface: Vxlan1 - Not configured"]}, }, - { - "name": "failure-down-up", - "test": VerifyVxlan1Interface, + (VerifyVxlan1Interface, "failure-down-up"): { "eos_data": [{"interfaceDescriptions": {"Vxlan1": {"lineProtocolStatus": "down", "interfaceStatus": "up"}}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: down/up"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: down/up"]}, }, - { - "name": "failure-up-down", - "test": VerifyVxlan1Interface, + (VerifyVxlan1Interface, "failure-up-down"): { "eos_data": [{"interfaceDescriptions": {"Vxlan1": {"lineProtocolStatus": "up", "interfaceStatus": "down"}}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: up/down"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: up/down"]}, }, - { - "name": "failure-down-down", - "test": VerifyVxlan1Interface, + (VerifyVxlan1Interface, "failure-down-down"): { "eos_data": [{"interfaceDescriptions": {"Vxlan1": {"lineProtocolStatus": "down", "interfaceStatus": "down"}}}], - "inputs": None, - "expected": {"result": "failure", "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: down/down"]}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Interface: Vxlan1 - Incorrect Line protocol status/Status - Expected: up/up Actual: down/down"], + }, }, - { - "name": "success", - "test": VerifyVxlanConfigSanity, + (VerifyVxlanConfigSanity, "success"): { "eos_data": [ { "categories": { @@ -106,14 +103,11 @@ DATA: list[dict[str, Any]] = [ }, }, "warnings": [], - }, + } ], - "inputs": None, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure", - "test": VerifyVxlanConfigSanity, + (VerifyVxlanConfigSanity, "failure"): { "eos_data": [ { "categories": { @@ -171,186 +165,181 @@ DATA: list[dict[str, Any]] = [ }, }, "warnings": ["Your configuration contains warnings. This does not mean misconfigurations. But you may wish to re-check your configurations."], - }, + } ], - "inputs": None, - "expected": { - "result": "failure", - "messages": ["Vxlan Category: localVtep - Config sanity check is not passing"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Vxlan Category: localVtep - Config sanity check is not passing"]}, }, - { - "name": "skipped", - "test": VerifyVxlanConfigSanity, + (VerifyVxlanConfigSanity, "skipped"): { "eos_data": [{"categories": {}}], - "inputs": None, - "expected": {"result": "skipped", "messages": ["VXLAN is not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["VXLAN is not configured"]}, }, - { - "name": "success", - "test": VerifyVxlanVniBinding, + (VerifyVxlanVniBinding, "success"): { "eos_data": [ { "vxlanIntfs": { "Vxlan1": { "vniBindings": { - "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}, + "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} }, - "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}}, - }, - }, - }, + "vniBindingsToVrf": {"500": {"vrfName": "TEST", "vlan": 1199, "source": "evpn"}, "600": {"vrfName": "PROD", "vlan": 1198, "source": "evpn"}}, + } + } + } ], - "inputs": {"bindings": {10020: 20, 500: 1199}}, - "expected": {"result": "success"}, + "inputs": {"bindings": {10020: 20, 500: 1199, 600: "PROD"}}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-no-binding", - "test": VerifyVxlanVniBinding, + (VerifyVxlanVniBinding, "failure-no-binding"): { "eos_data": [ { "vxlanIntfs": { "Vxlan1": { "vniBindings": { - "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}, + "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} }, "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}}, - }, - }, - }, + } + } + } ], "inputs": {"bindings": {10010: 10, 10020: 20, 500: 1199}}, - "expected": {"result": "failure", "messages": ["Interface: Vxlan1 VNI: 10010 - Binding not found"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 VNI: 10010 - Binding not found"]}, }, - { - "name": "failure-wrong-binding", - "test": VerifyVxlanVniBinding, + (VerifyVxlanVniBinding, "failure-vrf-wrong-binding"): { "eos_data": [ { "vxlanIntfs": { "Vxlan1": { "vniBindings": { - "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}, + "10020": {"vlan": 20, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} + }, + "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}, "600": {"vrfName": "TEST", "vlan": 1199, "source": "evpn"}}, + } + } + } + ], + "inputs": {"bindings": {10020: 20, 500: 1199, 600: "PROD"}}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 VNI: 600 - Wrong VRF binding - Expected: PROD Actual: TEST"]}, + }, + (VerifyVxlanVniBinding, "failure-wrong-binding"): { + "eos_data": [ + { + "vxlanIntfs": { + "Vxlan1": { + "vniBindings": { + "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} }, "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}}, - }, - }, - }, + } + } + } ], "inputs": {"bindings": {10020: 20, 500: 1199}}, - "expected": {"result": "failure", "messages": ["Interface: Vxlan1 VNI: 10020 VLAN: 20 - Wrong VLAN binding - Actual: 30"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 VNI: 10020 - Wrong VLAN binding - Expected: 20 Actual: 30"]}, }, - { - "name": "failure-no-and-wrong-binding", - "test": VerifyVxlanVniBinding, + (VerifyVxlanVniBinding, "failure-no-and-wrong-binding"): { "eos_data": [ { "vxlanIntfs": { "Vxlan1": { "vniBindings": { - "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}}, + "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} }, "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}}, - }, - }, - }, + } + } + } ], "inputs": {"bindings": {10010: 10, 10020: 20, 500: 1199}}, "expected": { - "result": "failure", - "messages": ["Interface: Vxlan1 VNI: 10010 - Binding not found", "Interface: Vxlan1 VNI: 10020 VLAN: 20 - Wrong VLAN binding - Actual: 30"], + "result": AntaTestStatus.FAILURE, + "messages": ["Interface: Vxlan1 VNI: 10010 - Binding not found", "Interface: Vxlan1 VNI: 10020 - Wrong VLAN binding - Expected: 20 Actual: 30"], }, }, - { - "name": "skipped", - "test": VerifyVxlanVniBinding, + (VerifyVxlanVniBinding, "failure-wrong-vni-vrf-binding"): { + "eos_data": [ + { + "vxlanIntfs": { + "Vxlan1": { + "vniBindings": { + "10020": {"vlan": 30, "dynamicVlan": False, "source": "static", "interfaces": {"Ethernet31": {"dot1q": 0}, "Vxlan1": {"dot1q": 20}}} + }, + "vniBindingsToVrf": {"500": {"vrfName": "PROD", "vlan": 1199, "source": "evpn"}}, + } + } + } + ], + "inputs": {"bindings": {10020: "PROD", 500: 30}}, + "expected": { + "result": AntaTestStatus.FAILURE, + "messages": ["Interface: Vxlan1 VNI: 10020 - Binding not found", "Interface: Vxlan1 VNI: 500 - Wrong VLAN binding - Expected: 30 Actual: 1199"], + }, + }, + (VerifyVxlanVniBinding, "skipped"): { "eos_data": [{"vxlanIntfs": {}}], "inputs": {"bindings": {10020: 20, 500: 1199}}, - "expected": {"result": "skipped", "messages": ["Vxlan1 interface is not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Interface: Vxlan1 - Not configured"]}, }, - { - "name": "success", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "success"): { "eos_data": [{"vteps": {}, "interfaces": {"Vxlan1": {"vteps": ["10.1.1.5", "10.1.1.6"]}}}], "inputs": {"vteps": ["10.1.1.5", "10.1.1.6"]}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "failure-missing-vtep", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "failure-missing-vtep"): { "eos_data": [{"vteps": {}, "interfaces": {"Vxlan1": {"vteps": ["10.1.1.5", "10.1.1.6"]}}}], "inputs": {"vteps": ["10.1.1.5", "10.1.1.6", "10.1.1.7"]}, - "expected": {"result": "failure", "messages": ["The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.7"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.7"]}, }, - { - "name": "failure-no-vtep", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "failure-no-vtep"): { "eos_data": [{"vteps": {}, "interfaces": {"Vxlan1": {"vteps": []}}}], "inputs": {"vteps": ["10.1.1.5", "10.1.1.6"]}, - "expected": {"result": "failure", "messages": ["The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6"]}, }, - { - "name": "failure-no-input-vtep", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "failure-no-input-vtep"): { "eos_data": [{"vteps": {}, "interfaces": {"Vxlan1": {"vteps": ["10.1.1.5"]}}}], "inputs": {"vteps": []}, - "expected": {"result": "failure", "messages": ["Unexpected VTEP peer(s) on Vxlan1 interface: 10.1.1.5"]}, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Unexpected VTEP peer(s) on Vxlan1 interface: 10.1.1.5"]}, }, - { - "name": "failure-missmatch", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "failure-missmatch"): { "eos_data": [{"vteps": {}, "interfaces": {"Vxlan1": {"vteps": ["10.1.1.6", "10.1.1.7", "10.1.1.8"]}}}], "inputs": {"vteps": ["10.1.1.5", "10.1.1.6"]}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5", "Unexpected VTEP peer(s) on Vxlan1 interface: 10.1.1.7, 10.1.1.8", ], }, }, - { - "name": "skipped", - "test": VerifyVxlanVtep, + (VerifyVxlanVtep, "skipped"): { "eos_data": [{"vteps": {}, "interfaces": {}}], "inputs": {"vteps": ["10.1.1.5", "10.1.1.6", "10.1.1.7"]}, - "expected": {"result": "skipped", "messages": ["Vxlan1 interface is not configured"]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Interface: Vxlan1 - Not configured"]}, }, - { - "name": "success", - "test": VerifyVxlan1ConnSettings, + (VerifyVxlan1ConnSettings, "success"): { "eos_data": [{"interfaces": {"Vxlan1": {"srcIpIntf": "Loopback1", "udpPort": 4789}}}], "inputs": {"source_interface": "Loopback1", "udp_port": 4789}, - "expected": {"result": "success"}, + "expected": {"result": AntaTestStatus.SUCCESS}, }, - { - "name": "skipped", - "test": VerifyVxlan1ConnSettings, + (VerifyVxlan1ConnSettings, "skipped"): { "eos_data": [{"interfaces": {}}], "inputs": {"source_interface": "Loopback1", "udp_port": 4789}, - "expected": {"result": "skipped", "messages": ["Vxlan1 interface is not configured."]}, + "expected": {"result": AntaTestStatus.SKIPPED, "messages": ["Interface: Vxlan1 - Not configured"]}, }, - { - "name": "failure-wrong-interface", - "test": VerifyVxlan1ConnSettings, + (VerifyVxlan1ConnSettings, "failure-wrong-interface"): { "eos_data": [{"interfaces": {"Vxlan1": {"srcIpIntf": "Loopback10", "udpPort": 4789}}}], "inputs": {"source_interface": "lo1", "udp_port": 4789}, - "expected": { - "result": "failure", - "messages": ["Interface: Vxlan1 - Incorrect Source interface - Expected: Loopback1 Actual: Loopback10"], - }, + "expected": {"result": AntaTestStatus.FAILURE, "messages": ["Interface: Vxlan1 - Incorrect Source interface - Expected: Loopback1 Actual: Loopback10"]}, }, - { - "name": "failure-wrong-port", - "test": VerifyVxlan1ConnSettings, + (VerifyVxlan1ConnSettings, "failure-wrong-port"): { "eos_data": [{"interfaces": {"Vxlan1": {"srcIpIntf": "Loopback10", "udpPort": 4789}}}], "inputs": {"source_interface": "Lo1", "udp_port": 4780}, "expected": { - "result": "failure", + "result": AntaTestStatus.FAILURE, "messages": [ "Interface: Vxlan1 - Incorrect Source interface - Expected: Loopback1 Actual: Loopback10", "Interface: Vxlan1 - Incorrect UDP port - Expected: 4780 Actual: 4789", ], }, }, -] +} diff --git a/tests/units/cli/conftest.py b/tests/units/cli/conftest.py index 3764695..d8b308a 100644 --- a/tests/units/cli/conftest.py +++ b/tests/units/cli/conftest.py @@ -123,7 +123,7 @@ def click_runner(capsys: pytest.CaptureFixture[str], anta_env: dict[str, str]) - # Patch asynceapi methods used by AsyncEOSDevice. See tests/units/test_device.py with ( - patch("asynceapi.device.Device.check_connection", return_value=True), + patch("asynceapi.device.Device.check_api_endpoint", return_value=True), patch("asynceapi.device.Device.cli", side_effect=cli), patch("asyncssh.connect"), patch( diff --git a/tests/units/cli/get/test_commands.py b/tests/units/cli/get/test_commands.py index 775dcbf..b50812e 100644 --- a/tests/units/cli/get/test_commands.py +++ b/tests/units/cli/get/test_commands.py @@ -368,7 +368,7 @@ def test_from_ansible_overwrite( None, True, False, - "`anta get tests --module ` does not support relative imports", + "`--module ` option does not support relative imports", ExitCode.USAGE_ERROR, id="Use relative module name", ), @@ -454,3 +454,185 @@ def test_get_tests_local_module(click_runner: CliRunner) -> None: if cwd != local_module_parent_path: assert "injecting CWD in PYTHONPATH and retrying..." in result.output assert "No test found in 'local_module'" in result.output + + +@pytest.mark.parametrize( + ("module", "test_name", "catalog", "expected_output", "expected_exit_code"), + [ + pytest.param( + None, + None, + None, + "VerifyAcctConsoleMethods", + ExitCode.OK, + id="Get all commands", + ), + pytest.param( + "anta.tests.aaa", + None, + None, + "VerifyAcctConsoleMethods", + ExitCode.OK, + id="Get commands, filter on module", + ), + pytest.param( + None, + "VerifyNTPAssociations", + None, + "VerifyNTPAssociations", + ExitCode.OK, + id="Get commands, filter on exact test name", + ), + pytest.param( + None, + "VerifyNTP", + None, + "anta.tests.system", + ExitCode.OK, + id="Get commands, filter on included test name", + ), + pytest.param( + "unknown_module", + None, + None, + "Module `unknown_module` was not found!", + ExitCode.USAGE_ERROR, + id="Get commands wrong module", + ), + pytest.param( + "unknown_module.unknown", + None, + None, + "Module `unknown_module.unknown` was not found!", + ExitCode.USAGE_ERROR, + id="Get commands wrong submodule", + ), + pytest.param( + ".unknown_module", + None, + None, + "`--module ` option does not support relative imports", + ExitCode.USAGE_ERROR, + id="Use relative module name", + ), + pytest.param( + None, + "VerifySomething", + None, + "No test 'VerifySomething' found in 'anta.tests'", + ExitCode.OK, + id="Get commands wrong test name", + ), + pytest.param( + "anta.tests.aaa", + "VerifyNTP", + None, + "No test 'VerifyNTP' found in 'anta.tests.aaa'", + ExitCode.OK, + id="Get commands test exists but not in module", + ), + pytest.param( + None, + None, + DATA_DIR / "test_catalog.yml", + "VerifyEOSVersion", + ExitCode.OK, + id="Get all commands from catalog", + ), + pytest.param( + "anta.tests.aaa", + None, + DATA_DIR / "test_catalog.yml", + "No test found in 'anta.tests.aaa' for catalog '", # partial match as no test from aaa in the catalog + ExitCode.OK, + id="Get all commands from module in catalog", + ), + pytest.param( + None, + None, + DATA_DIR / "non_existing_catalog.yml", + "Invalid value for '--catalog'", + ExitCode.USAGE_ERROR, + id="Catalog does not exist", + ), + # TODO: catalog format JSON + ], +) +def test_get_commands( + click_runner: CliRunner, module: str | None, test_name: str | None, catalog: str | None, expected_output: str, expected_exit_code: str +) -> None: + """Test `anta get commands`.""" + cli_args = [ + "get", + "commands", + ] + if module is not None: + cli_args.extend(["--module", module]) + + if test_name is not None: + cli_args.extend(["--test", test_name]) + + if catalog is not None: + cli_args.extend(["--catalog", catalog]) + + # Make sure to disable any ANTA_CATALOG env that could be set and pollute the test + result = click_runner.invoke(anta, cli_args, env={"ANTA_CATALOG": None}) + + assert result.exit_code == expected_exit_code + assert expected_output in result.output + + +@pytest.mark.parametrize( + ("module", "test_name", "catalog", "expected_count"), + [ + pytest.param( + "anta.tests.aaa", + None, + None, + 4, + id="Get unique commands, filter on module", + ), + pytest.param( + None, + "VerifyNTPAssociations", + None, + 1, + id="Get unique commands, filter on exact test name", + ), + pytest.param( + None, + "VerifyNTP", + None, + 2, + id="Get unique commands, filter on included test name", + ), + pytest.param( + None, + None, + DATA_DIR / "test_catalog.yml", + 1, + id="Get all unique commands from catalog", + ), + ], +) +def test_get_commands_unique(click_runner: CliRunner, module: str | None, test_name: str | None, catalog: str | None, expected_count: int) -> None: + """Test `anta get commands`.""" + cli_args = [ + "get", + "commands", + ] + if module is not None: + cli_args.extend(["--module", module]) + + if test_name is not None: + cli_args.extend(["--test", test_name]) + + if catalog is not None: + cli_args.extend(["--catalog", catalog]) + + cli_args.extend(["--unique"]) + + # Make sure to disable any ANTA_CATALOG env that could be set and pollute the test + result = click_runner.invoke(anta, cli_args, env={"ANTA_CATALOG": None}) + + assert expected_count == len(result.output.splitlines()) diff --git a/tests/units/cli/get/test_utils.py b/tests/units/cli/get/test_utils.py index b6eebcf..28a16eb 100644 --- a/tests/units/cli/get/test_utils.py +++ b/tests/units/cli/get/test_utils.py @@ -13,7 +13,7 @@ from unittest.mock import MagicMock, patch import pytest import requests -from anta.cli.get.utils import create_inventory_from_ansible, create_inventory_from_cvp, extract_examples, find_tests_examples, get_cv_token, print_test +from anta.cli.get.utils import create_inventory_from_ansible, create_inventory_from_cvp, extract_examples, find_tests_in_module, get_cv_token, print_test from anta.inventory import AntaInventory from anta.models import AntaCommand, AntaTemplate, AntaTest @@ -219,14 +219,14 @@ class TypoExampleTest(AntaTest): self.result.is_success() -def test_find_tests_examples() -> None: +def test_find_tests_in_module() -> None: """Test find_tests_examples. Only testing the failure scenarii not tested through test_commands. TODO: expand """ with pytest.raises(ValueError, match="Error when importing"): - find_tests_examples("blah", "UnusedTestName") + find_tests_in_module("blah", "UnusedTestName") def test_print_test() -> None: diff --git a/tests/units/cli/nrfu/test_commands.py b/tests/units/cli/nrfu/test_commands.py index 1910636..783dfae 100644 --- a/tests/units/cli/nrfu/test_commands.py +++ b/tests/units/cli/nrfu/test_commands.py @@ -178,7 +178,7 @@ def test_anta_nrfu_md_report(click_runner: CliRunner, tmp_path: Path) -> None: def test_anta_nrfu_md_report_failure(click_runner: CliRunner, tmp_path: Path) -> None: """Test anta nrfu md-report failure.""" md_output = tmp_path / "test.md" - with patch("anta.reporter.md_reporter.MDReportGenerator.generate", side_effect=OSError()): + with patch("anta.reporter.md_reporter.MDReportGenerator.generate_sections", side_effect=OSError()): result = click_runner.invoke(anta, ["nrfu", "md-report", "--md-output", str(md_output)]) assert result.exit_code == ExitCode.USAGE_ERROR @@ -206,5 +206,17 @@ def test_anta_nrfu_md_report_with_hide(click_runner: CliRunner, tmp_path: Path) total_tests = int(match.group(1)) total_tests_success = int(match.group(2)) - assert total_tests == 0 - assert total_tests_success == 0 + assert total_tests == 3 + assert total_tests_success == 3 + + # Collecting the rows inside the Test Results section + row_count = 0 + lines = content.splitlines() + + idx = lines.index("## Test Results") + + for line in lines[idx + 1 :]: + if line.startswith("|") and "---" not in line: + row_count += 1 + # Reducing the row count by 1, as above conditions counts the TABLE_HEADING + assert (row_count - 1) == 0 diff --git a/tests/units/cli/test__init__.py b/tests/units/cli/test__init__.py index 308516f..2c24731 100644 --- a/tests/units/cli/test__init__.py +++ b/tests/units/cli/test__init__.py @@ -6,35 +6,21 @@ from __future__ import annotations import sys -from importlib import reload -from typing import TYPE_CHECKING, Any +from typing import Any from unittest.mock import patch import pytest -import anta.cli -if TYPE_CHECKING: - from types import ModuleType - -builtins_import = __import__ - - -# Tried to achieve this with mock -# http://materials-scientist.com/blog/2021/02/11/mocking-failing-module-import-python/ -def import_mock(name: str, *args: Any) -> ModuleType: # noqa: ANN401 - """Mock.""" - if name == "click": - msg = "No module named 'click'" - raise ModuleNotFoundError(msg) - return builtins_import(name, *args) - - -def test_cli_error_missing(capsys: pytest.CaptureFixture[Any]) -> None: +# https://github.com/python/cpython/issues/88852 +@pytest.mark.skipif(sys.version_info <= (3, 11), reason="Unreliable behavior patching sys.modules before 3.11") +def test_cli_error_missing_click(capsys: pytest.CaptureFixture[Any]) -> None: """Test ANTA errors out when anta[cli] was not installed.""" - with patch.dict(sys.modules) as sys_modules, patch("builtins.__import__", import_mock): - del sys_modules["anta.cli._main"] - reload(anta.cli) + with patch.dict(sys.modules, {"click": None}) as sys_modules: + for k in list(sys_modules.keys()): + if k.startswith("anta."): + del sys_modules[k] + import anta.cli with pytest.raises(SystemExit) as e_info: anta.cli.cli() @@ -53,3 +39,18 @@ def test_cli_error_missing(capsys: pytest.CaptureFixture[Any]) -> None: assert "Make sure you've installed everything with: pip install 'anta[cli]'" in captured.out assert "The caught exception was:" in captured.out assert e_info.value.code == 1 + + +# https://github.com/python/cpython/issues/88852 +@pytest.mark.skipif(sys.version_info <= (3, 11), reason="Unreliable behavior patching sys.modules before 3.11") +def test_cli_error_missing_other() -> None: + """Test ANTA errors out when anta[cli] was not installed.""" + with patch.dict(sys.modules, {"httpx": None}) as sys_modules: + # Need to clean up from previous runs a path that will trigger reimporting httpx + for k in list(sys_modules.keys()): + if k.startswith("anta."): + del sys_modules[k] + import anta.cli + + with pytest.raises(ImportError, match="httpx"): + anta.cli.cli() diff --git a/tests/units/conftest.py b/tests/units/conftest.py index 49c786f..3987722 100644 --- a/tests/units/conftest.py +++ b/tests/units/conftest.py @@ -5,8 +5,10 @@ from __future__ import annotations +import os from pathlib import Path from typing import TYPE_CHECKING, Any +from unittest import mock from unittest.mock import patch import pytest @@ -15,7 +17,7 @@ import yaml from anta.device import AntaDevice, AsyncEOSDevice if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Generator, Iterator from anta.models import AntaCommand @@ -83,3 +85,10 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path: content: dict[str, Any] = request.param file.write_text(yaml.dump(content, allow_unicode=True)) return file + + +@pytest.fixture +def setenvvar(monkeypatch: pytest.MonkeyPatch) -> Generator[pytest.MonkeyPatch, None, None]: + """Fixture to set environment variables for testing.""" + with mock.patch.dict(os.environ, clear=True): + yield monkeypatch diff --git a/tests/units/input_models/routing/test_bgp.py b/tests/units/input_models/routing/test_bgp.py index ee94111..ea454a1 100644 --- a/tests/units/input_models/routing/test_bgp.py +++ b/tests/units/input_models/routing/test_bgp.py @@ -28,7 +28,9 @@ from anta.tests.routing.bgp import ( ) if TYPE_CHECKING: - from anta.custom_types import Afi, RedistributedAfiSafi, RedistributedProtocol, Safi + from ipaddress import IPv4Address, IPv6Address + + from anta.custom_types import Afi, Interface, RedistributedAfiSafi, RedistributedProtocol, Safi class TestBgpAddressFamily: @@ -60,6 +62,33 @@ class TestBgpAddressFamily: BgpAddressFamily(afi=afi, safi=safi, vrf=vrf) +class TestBgpPeer: + """Test anta.input_models.routing.bgp.BgpPeer.""" + + @pytest.mark.parametrize( + ("peer_address", "interface"), + [ + pytest.param("10.1.1.0", None, id="peer"), + pytest.param(None, "Et1", id="interface"), + ], + ) + def test_valid(self, peer_address: IPv4Address | IPv6Address, interface: Interface) -> None: + """Test BgpPeer valid inputs.""" + BgpPeer(peer_address=peer_address, interface=interface) + + @pytest.mark.parametrize( + ("peer_address", "interface"), + [ + pytest.param("10.1.1.0", "Et1", id="both"), + pytest.param(None, "None", id="both-None"), + ], + ) + def test_invalid(self, peer_address: IPv4Address | IPv6Address, interface: Interface) -> None: + """Test BgpPeer invalid inputs.""" + with pytest.raises(ValidationError): + BgpPeer(peer_address=peer_address, interface=interface) + + class TestVerifyBGPPeerCountInput: """Test anta.tests.routing.bgp.VerifyBGPPeerCount.Input.""" diff --git a/tests/units/inventory/test__init__.py b/tests/units/inventory/test__init__.py index 461f037..45cdb27 100644 --- a/tests/units/inventory/test__init__.py +++ b/tests/units/inventory/test__init__.py @@ -19,6 +19,9 @@ if TYPE_CHECKING: from _pytest.mark.structures import ParameterSet + from anta.device import AntaDevice, AsyncEOSDevice + + INIT_VALID_PARAMS: list[ParameterSet] = [ pytest.param( {"anta_inventory": {"hosts": [{"host": "192.168.0.17"}, {"host": "192.168.0.2"}, {"host": "my.awesome.host.com"}]}}, @@ -87,3 +90,22 @@ class TestAntaInventory: with pytest.raises(OSError, match="No such file or directory"): _ = AntaInventory.parse(filename="dummy.yml", username="arista", password="arista123") assert "Unable to parse ANTA Device Inventory file" in caplog.records[0].message + + @pytest.mark.parametrize(("inventory"), [{"count": 3}], indirect=True) + def test_max_potential_connections(self, inventory: AntaInventory) -> None: + """Test max_potential_connections property with regular AsyncEOSDevice objects in the inventory.""" + # Each AsyncEOSDevice has a max_connections of 100 + assert inventory.max_potential_connections == 300 + + @pytest.mark.parametrize(("device"), [{"name": "anta_device"}], indirect=True) + def test_get_potential_connections_custom_anta_device(self, caplog: pytest.LogCaptureFixture, async_device: AsyncEOSDevice, device: AntaDevice) -> None: + """Test max_potential_connections property with an AntaDevice with no max_connections in the inventory.""" + caplog.set_level(logging.DEBUG) + + inventory = AntaInventory() + inventory.add_device(async_device) + inventory.add_device(device) + + assert len(inventory) == 2 + assert inventory.max_potential_connections is None + assert "Device anta_device 'max_connections' is not available" in caplog.messages diff --git a/tests/units/reporter/test_md_reporter.py b/tests/units/reporter/test_md_reporter.py index f5d2423..8bb1068 100644 --- a/tests/units/reporter/test_md_reporter.py +++ b/tests/units/reporter/test_md_reporter.py @@ -7,15 +7,48 @@ from __future__ import annotations from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING, ClassVar import pytest from anta.reporter.md_reporter import MDReportBase, MDReportGenerator from anta.result_manager import ResultManager +from anta.result_manager.models import AntaTestStatus +from anta.tools import convert_categories + +if TYPE_CHECKING: + from collections.abc import Generator DATA_DIR: Path = Path(__file__).parent.parent.parent.resolve() / "data" +class FailedTestResultsSummary(MDReportBase): + """Test-only class used for simulating behavior in unit tests. + + Generates the `## Failed Test Results Summary` section of the markdown report. + """ + + TABLE_HEADING: ClassVar[list[str]] = [ + "| Device Under Test | Categories | Test | Description | Custom Field | Result | Messages |", + "| ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- |", + ] + + def generate_rows(self) -> Generator[str, None, None]: + """Generate the rows of the all test results table.""" + for result in self.results.results: + messages = self.safe_markdown(result.messages[0]) if len(result.messages) == 1 else self.safe_markdown("
".join(result.messages)) + categories = ", ".join(sorted(convert_categories(result.categories))) + yield ( + f"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} " + f"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\n" + ) + + def generate_section(self) -> None: + """Generate the `## Failed Test Results Summary` section of the markdown report.""" + self.write_heading(heading_level=2) + self.write_table(table_heading=self.TABLE_HEADING) + + def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> None: """Test the MDReportGenerator class.""" md_filename = tmp_path / "test.md" @@ -35,6 +68,31 @@ def test_md_report_generate(tmp_path: Path, result_manager: ResultManager) -> No assert content == expected_content +def test_md_report_generate_sections(tmp_path: Path, result_manager: ResultManager) -> None: + """Test the MDReportGenerator class.""" + md_filename = tmp_path / "test.md" + expected_report = "test_md_report_custom_sections.md" + rm = result_manager.sort(sort_by=["name", "categories", "test"]) + + sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS] + # Adding custom section + failed_section = (FailedTestResultsSummary, rm.filter({AntaTestStatus.SUCCESS, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED, AntaTestStatus.UNSET})) + sections.insert(-1, failed_section) + + # Generate the Markdown report + MDReportGenerator.generate_sections(sections, md_filename) + assert md_filename.exists() + + # Load the existing Markdown report to compare with the generated one + with (DATA_DIR / expected_report).open("r", encoding="utf-8") as f: + expected_content = f.read() + + # Check the content of the Markdown file + content = md_filename.read_text(encoding="utf-8") + + assert content == expected_content + + def test_md_report_base() -> None: """Test the MDReportBase class.""" @@ -52,3 +110,17 @@ def test_md_report_base() -> None: with pytest.raises(NotImplementedError, match="Subclasses should implement this method"): report.generate_rows() + + +def test_md_report_error(result_manager: ResultManager) -> None: + """Test the MDReportGenerator class to OSError to be raised.""" + md_filename = Path("non_existent_directory/non_existent_file.md") + rm = result_manager.sort(sort_by=["name", "categories", "test"]) + + sections = [(section, rm) for section in MDReportGenerator.DEFAULT_SECTIONS] + + with pytest.raises(OSError, match="No such file or directory"): + MDReportGenerator.generate_sections(sections, md_filename) + + with pytest.raises(OSError, match="No such file or directory"): + MDReportGenerator.generate(result_manager, md_filename) diff --git a/tests/units/result_manager/test__init__.py b/tests/units/result_manager/test__init__.py index e70dbf9..5b20506 100644 --- a/tests/units/result_manager/test__init__.py +++ b/tests/units/result_manager/test__init__.py @@ -172,11 +172,11 @@ class TestResultManager: assert "results_by_status" not in result_manager.__dict__ # Access the cache - assert result_manager.get_total_results() == 30 + assert result_manager.get_total_results() == 181 # Check the cache is filled with the correct results count assert "results_by_status" in result_manager.__dict__ - assert sum(len(v) for v in result_manager.__dict__["results_by_status"].values()) == 30 + assert sum(len(v) for v in result_manager.__dict__["results_by_status"].values()) == 181 # Add a new test result_manager.add(result=test_result_factory()) @@ -185,46 +185,51 @@ class TestResultManager: assert "results_by_status" not in result_manager.__dict__ # Access the cache again - assert result_manager.get_total_results() == 31 + assert result_manager.get_total_results() == 182 # Check the cache is filled again with the correct results count assert "results_by_status" in result_manager.__dict__ - assert sum(len(v) for v in result_manager.__dict__["results_by_status"].values()) == 31 + assert sum(len(v) for v in result_manager.__dict__["results_by_status"].values()) == 182 def test_get_results(self, result_manager: ResultManager) -> None: """Test ResultManager.get_results.""" # Check for single status success_results = result_manager.get_results(status={AntaTestStatus.SUCCESS}) - assert len(success_results) == 4 + assert len(success_results) == 43 assert all(r.result == "success" for r in success_results) # Check for multiple statuses failure_results = result_manager.get_results(status={AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) - assert len(failure_results) == 17 + assert len(failure_results) == 104 assert all(r.result in {"failure", "error"} for r in failure_results) # Check all results all_results = result_manager.get_results() - assert len(all_results) == 30 + assert len(all_results) == 181 def test_get_results_sort_by(self, result_manager: ResultManager) -> None: """Test ResultManager.get_results with sort_by.""" # Check all results with sort_by result all_results = result_manager.get_results(sort_by=["result"]) - assert len(all_results) == 30 - assert [r.result for r in all_results] == ["error"] * 2 + ["failure"] * 15 + ["skipped"] * 9 + ["success"] * 4 + assert len(all_results) == 181 + assert [r.result for r in all_results] == ["error"] * 1 + ["failure"] * 103 + ["skipped"] * 34 + ["success"] * 43 # Check all results with sort_by device (name) all_results = result_manager.get_results(sort_by=["name"]) - assert len(all_results) == 30 + assert len(all_results) == 181 assert all_results[0].name == "s1-spine1" # Check multiple statuses with sort_by categories success_skipped_results = result_manager.get_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.SKIPPED}, sort_by=["categories"]) - assert len(success_skipped_results) == 13 + assert len(success_skipped_results) == 77 assert success_skipped_results[0].categories == ["avt"] assert success_skipped_results[-1].categories == ["vxlan"] + # Check multiple statuses with sort_by custom_field + success_skipped_results = result_manager.get_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.SKIPPED}, sort_by=["custom_field"]) + assert len(success_skipped_results) == 77 + assert success_skipped_results[-1].test == "VerifyReloadCause" + # Check all results with bad sort_by with pytest.raises( ValueError, @@ -237,18 +242,18 @@ class TestResultManager: def test_get_total_results(self, result_manager: ResultManager) -> None: """Test ResultManager.get_total_results.""" # Test all results - assert result_manager.get_total_results() == 30 + assert result_manager.get_total_results() == 181 # Test single status - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS}) == 4 - assert result_manager.get_total_results(status={AntaTestStatus.FAILURE}) == 15 - assert result_manager.get_total_results(status={AntaTestStatus.ERROR}) == 2 - assert result_manager.get_total_results(status={AntaTestStatus.SKIPPED}) == 9 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS}) == 43 + assert result_manager.get_total_results(status={AntaTestStatus.FAILURE}) == 103 + assert result_manager.get_total_results(status={AntaTestStatus.ERROR}) == 1 + assert result_manager.get_total_results(status={AntaTestStatus.SKIPPED}) == 34 # Test multiple statuses - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE}) == 19 - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) == 21 - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED}) == 30 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE}) == 146 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) == 147 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED}) == 181 @pytest.mark.parametrize( ("status", "error_status", "ignore_error", "expected_status"), @@ -546,6 +551,32 @@ class TestResultManager: assert results[2].result == "failure" assert results[2].test == "Test2" + def test_sort_fields_as_none(self, test_result_factory: Callable[[], TestResult]) -> None: + """Test sorting by multiple fields.""" + result_manager = ResultManager() + test1 = test_result_factory() + test1.result = AntaTestStatus.ERROR + test1.test = "Test3" + test1.custom_field = "custom" + test2 = test_result_factory() + test2.result = AntaTestStatus.ERROR + test2.test = "Test1" + test3 = test_result_factory() + test3.result = AntaTestStatus.FAILURE + test3.test = "Test2" + + result_manager.results = [test1, test2, test3] + sorted_manager = result_manager.sort(["custom_field"]) + results = sorted_manager.results + + assert results[0].result == "error" + assert results[0].test == "Test1" + assert results[1].result == "failure" + assert results[1].test == "Test2" + assert results[2].result == "error" + assert results[2].test == "Test3" + assert results[2].custom_field == "custom" + def test_sort_invalid_field(self) -> None: """Test that sort method raises ValueError for invalid sort_by fields.""" result_manager = ResultManager() @@ -561,3 +592,34 @@ class TestResultManager: """Test that the sort method is chainable.""" result_manager = ResultManager() assert isinstance(result_manager.sort(["name"]), ResultManager) + + def test_merge_result_manager(self) -> None: + """Test the merge_results function.""" + result = ResultManager() + final_merged_results = ResultManager.merge_results([result]) + assert isinstance(final_merged_results, ResultManager) + + def test_merge_two_result_managers(self, test_result_factory: Callable[[], TestResult]) -> None: + """Test merging two non-empty ResultManager instances.""" + rm1 = ResultManager() + test1_rm1 = test_result_factory() + test1_rm1.name = "device1" + rm1.add(test1_rm1) + test2_rm1 = test_result_factory() + test2_rm1.name = "device2" + rm1.add(test2_rm1) + + rm2 = ResultManager() + test1_rm2 = test_result_factory() + test1_rm2.name = "device3" + rm2.add(test1_rm2) + + merged_rm = ResultManager.merge_results([rm1, rm2]) + assert len(merged_rm) == 3 + assert {r.name for r in merged_rm.results} == {"device1", "device2", "device3"} + + def test_merge_empty_list(self) -> None: + """Test merging an empty list of ResultManager instances.""" + merged_rm = ResultManager.merge_results([]) + assert isinstance(merged_rm, ResultManager) + assert len(merged_rm) == 0 diff --git a/tests/units/result_manager/test_files/test_md_report_results.json b/tests/units/result_manager/test_files/test_md_report_results.json index ab932dc..0ece91e 100644 --- a/tests/units/result_manager/test_files/test_md_report_results.json +++ b/tests/units/result_manager/test_files/test_md_report_results.json @@ -1,389 +1,2356 @@ [ { - "name": "s1-spine1", - "test": "VerifyMlagDualPrimary", - "categories": [ - "mlag" - ], - "description": "Verifies the MLAG dual-primary detection parameters.", - "result": "failure", - "messages": [ - "Dual-primary detection is disabled" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyInterfacesStatus", + "categories": [ + "interfaces" + ], + "description": "Verifies the operational states of specified interfaces to ensure they match expected configurations.", + "result": "failure", + "messages": [ + "Port-Channel100 - Not configured", + "Ethernet49/1 - Not configured" + ], + "custom_field": "interface" }, { - "name": "s1-spine1", - "test": "VerifyHostname", - "categories": [ - "services" - ], - "description": "Verifies the hostname of a device.", - "result": "failure", - "messages": [ - "Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyVlanStatus", + "categories": [ + "vlan" + ], + "description": "Verifies the administrative status of specified VLANs.", + "result": "failure", + "messages": [ + "VLAN: Vlan10 - Incorrect administrative status - Expected: suspended Actual: active" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyBGPAdvCommunities", - "categories": [ - "bgp" - ], - "description": "Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s).", - "result": "error", - "messages": [ - "show bgp neighbors vrf all has failed: The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyReloadCause", + "categories": [ + "system" + ], + "description": "Verifies the last reload cause of the device.", + "result": "success", + "messages": [], + "custom_field": "reload-cause" }, { - "name": "s1-spine1", - "test": "VerifyStunClient", - "categories": [ - "stun" - ], - "description": "(Deprecated) Verifies the translation for a source address on a STUN client.", - "result": "failure", - "messages": [ - "Client 172.18.3.2 Port: 4500 - STUN client translation not found." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyVxlanVniBinding", + "categories": [ + "vxlan" + ], + "description": "Verifies the VNI-VLAN, VNI-VRF bindings of the Vxlan1 interface.", + "result": "failure", + "messages": [ + "Interface: Vxlan1 VNI: 500 - Binding not found" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyBannerLogin", - "categories": [ - "security" - ], - "description": "Verifies the login banner of a device.", - "result": "failure", - "messages": [ - "Expected `# Copyright (c) 2023-2024 Arista Networks, Inc.\n# Use of this source code is governed by the Apache License 2.0\n# that can be found in the LICENSE file.\n` as the login banner, but found `` instead." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyISISNeighborState", + "categories": [ + "isis" + ], + "description": "Verifies the health of IS-IS neighbors.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyISISNeighborState", - "categories": [ - "isis" - ], - "description": "Verifies the health of IS-IS neighbors.", - "result": "skipped", - "messages": [ - "IS-IS not configured" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyMlagStatus", + "categories": [ + "mlag" + ], + "description": "Verifies the health status of the MLAG configuration.", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyEOSVersion", - "categories": [ - "software" - ], - "description": "Verifies the EOS version of the device.", - "result": "failure", - "messages": [ - "EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyTemperature", + "categories": [ + "hardware" + ], + "description": "Verifies if the device temperature is within acceptable limits.", + "result": "skipped", + "messages": [ + "VerifyTemperature test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyTcamProfile", - "categories": [ - "profiles" - ], - "description": "Verifies the device TCAM profile.", - "result": "skipped", - "messages": [ - "VerifyTcamProfile test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyPtpModeStatus", + "categories": [ + "ptp" + ], + "description": "Verifies that the device is configured as a PTP Boundary Clock.", + "result": "skipped", + "messages": [ + "VerifyPtpModeStatus test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyPathsHealth", - "categories": [ - "path-selection" - ], - "description": "Verifies the path and telemetry state of all paths under router path-selection.", - "result": "skipped", - "messages": [ - "VerifyPathsHealth test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyAcctConsoleMethods", + "categories": [ + "aaa" + ], + "description": "Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).", + "result": "failure", + "messages": [ + "AAA console accounting is not configured for commands, exec, system, dot1x" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyBannerMotd", - "categories": [ - "security" - ], - "description": "Verifies the motd banner of a device.", - "result": "failure", - "messages": [ - "Expected `# Copyright (c) 2023-2024 Arista Networks, Inc.\n# Use of this source code is governed by the Apache License 2.0\n# that can be found in the LICENSE file.\n` as the motd banner, but found `` instead." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyL3MTU", + "categories": [ + "interfaces" + ], + "description": "Verifies the global L3 MTU of all L3 interfaces.", + "result": "failure", + "messages": [ + "Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 9214", + "Interface: Ethernet1 - Incorrect MTU - Expected: 2500 Actual: 9214", + "Interface: Loopback0 - Incorrect MTU - Expected: 1500 Actual: 65535", + "Interface: Loopback1 - Incorrect MTU - Expected: 1500 Actual: 65535", + "Interface: Vlan1006 - Incorrect MTU - Expected: 1500 Actual: 9164", + "Interface: Vlan1199 - Incorrect MTU - Expected: 1500 Actual: 9164", + "Interface: Vlan4093 - Incorrect MTU - Expected: 1500 Actual: 9214", + "Interface: Vlan4094 - Incorrect MTU - Expected: 1500 Actual: 9214", + "Interface: Vlan3019 - Incorrect MTU - Expected: 1500 Actual: 9214", + "Interface: Vlan3009 - Incorrect MTU - Expected: 1500 Actual: 9214" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyFieldNotice44Resolution", - "categories": [ - "field notices" - ], - "description": "Verifies that the device is using the correct Aboot version per FN0044.", - "result": "skipped", - "messages": [ - "VerifyFieldNotice44Resolution test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyDNSLookup", + "categories": [ + "services" + ], + "description": "Verifies the DNS name to IP address resolution.", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyLoggingHosts", - "categories": [ - "logging" - ], - "description": "Verifies logging hosts (syslog servers) for a specified VRF.", - "result": "failure", - "messages": [ - "Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyBGPPeerRouteRefreshCap", + "categories": [ + "bgp" + ], + "description": "Verifies the route refresh capabilities of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyAVTPathHealth", - "categories": [ - "avt" - ], - "description": "Verifies the status of all AVT paths for all VRFs.", - "result": "skipped", - "messages": [ - "VerifyAVTPathHealth test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyUptime", + "categories": [ + "system" + ], + "description": "Verifies the device uptime.", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyTemperature", - "categories": [ - "hardware" - ], - "description": "Verifies if the device temperature is within acceptable limits.", - "result": "skipped", - "messages": [ - "VerifyTemperature test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyAuthenMethods", + "categories": [ + "aaa" + ], + "description": "Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).", + "result": "failure", + "messages": [ + "AAA authentication methods are not configured for login console" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyNTPAssociations", - "categories": [ - "system" - ], - "description": "Verifies the Network Time Protocol (NTP) associations.", - "result": "failure", - "messages": [ - "NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured", - "NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured", - "NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyHardwareFlowTrackerStatus", + "categories": [ + "flow tracking" + ], + "description": "Verifies the hardware flow tracking state.", + "result": "skipped", + "messages": [ + "VerifyHardwareFlowTrackerStatus test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyDynamicVlanSource", - "categories": [ - "vlan" - ], - "description": "Verifies dynamic VLAN allocation for specified VLAN sources.", - "result": "failure", - "messages": [ - "Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyTransceiversManufacturers", + "categories": [ + "hardware" + ], + "description": "Verifies if all the transceivers come from approved manufacturers.", + "result": "skipped", + "messages": [ + "VerifyTransceiversManufacturers test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyActiveCVXConnections", - "categories": [ - "cvx" - ], - "description": "Verifies the number of active CVX Connections.", - "result": "error", - "messages": [ - "show cvx connections brief has failed: Unavailable command (controller not ready) (at token 2: 'connections')" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyPtpPortModeStatus", + "categories": [ + "ptp" + ], + "description": "Verifies the PTP interfaces state.", + "result": "skipped", + "messages": [ + "VerifyPtpPortModeStatus test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyIPv4RouteNextHops", - "categories": [ - "routing" - ], - "description": "Verifies the next-hops of the IPv4 prefixes.", - "result": "success", - "messages": [], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyTacacsServerGroups", + "categories": [ + "aaa" + ], + "description": "Verifies if the provided TACACS server group(s) are configured.", + "result": "failure", + "messages": [ + "No TACACS server group(s) are configured" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyVxlan1ConnSettings", - "categories": [ - "vxlan" - ], - "description": "Verifies the interface vxlan1 source interface and UDP port.", - "result": "success", - "messages": [], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyISISSegmentRoutingDataplane", + "categories": [ + "isis", + "segment-routing" + ], + "description": "Verifies IS-IS segment routing data-plane configuration.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyStunClientTranslation", - "categories": [ - "stun" - ], - "description": "Verifies the translation for a source address on a STUN client.", - "result": "failure", - "messages": [ - "Client 172.18.3.2 Port: 4500 - STUN client translation not found.", - "Client 100.64.3.2 Port: 4500 - STUN client translation not found." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyReachability", + "categories": [ + "connectivity" + ], + "description": "Test network reachability to one or many destination IP(s).", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyPtpGMStatus", - "categories": [ - "ptp" - ], - "description": "Verifies that the device is locked to a valid PTP Grandmaster.", - "result": "skipped", - "messages": [ - "VerifyPtpGMStatus test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyBGPPeerGroup", + "categories": [ + "bgp" + ], + "description": "Verifies BGP peer group of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyRunningConfigDiffs", - "categories": [ - "configuration" - ], - "description": "Verifies there is no difference between the running-config and the startup-config.", - "result": "success", - "messages": [], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyErrdisableRecovery", + "categories": [ + "services" + ], + "description": "Verifies the error disable recovery functionality.", + "result": "failure", + "messages": [ + "Reason: acl Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300", + "Reason: bpduguard Status: Enabled Interval: 30 - Incorrect configuration - Status: Disabled Interval: 300" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyBFDPeersHealth", - "categories": [ - "bfd" - ], - "description": "Verifies the health of IPv4 BFD peers across all VRFs.", - "result": "failure", - "messages": [ - "No IPv4 BFD peers are configured for any VRF." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyAcctDefaultMethods", + "categories": [ + "aaa" + ], + "description": "Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).", + "result": "failure", + "messages": [ + "AAA default accounting is not configured for commands, exec, system, dot1x" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyIPProxyARP", - "categories": [ - "interfaces" - ], - "description": "Verifies if Proxy ARP is enabled.", - "result": "failure", - "messages": [ - "Interface: Ethernet1 - Proxy-ARP disabled", - "Interface: Ethernet2 - Proxy-ARP disabled" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyTacacsServers", + "categories": [ + "aaa" + ], + "description": "Verifies TACACS servers are configured for a specified VRF.", + "result": "failure", + "messages": [ + "No TACACS servers are configured" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifySnmpContact", - "categories": [ - "snmp" - ], - "description": "Verifies the SNMP contact of a device.", - "result": "failure", - "messages": [ - "SNMP contact is not configured." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifySpecificPath", + "categories": [ + "path-selection" + ], + "description": "Verifies the DPS path and telemetry state of an IPv4 peer.", + "result": "skipped", + "messages": [ + "VerifySpecificPath test is not supported on cEOSLab" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyLLDPNeighbors", - "categories": [ - "connectivity" - ], - "description": "Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.", - "result": "failure", - "messages": [ - "Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3", - "Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyInterfaceUtilization", + "categories": [ + "interfaces" + ], + "description": "Verifies that the utilization of interfaces is below a certain threshold.", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyAcctConsoleMethods", - "categories": [ - "aaa" - ], - "description": "Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).", - "result": "failure", - "messages": [ - "AAA console accounting is not configured for commands, exec, system, dot1x" - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyL2MTU", + "categories": [ + "interfaces" + ], + "description": "Verifies the global L2 MTU of all L2 interfaces.", + "result": "failure", + "messages": [ + "Interface: Ethernet3 - Incorrect MTU configured - Expected: 1500 Actual: 9214", + "Interface: Port-Channel5 - Incorrect MTU configured - Expected: 1500 Actual: 9214" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyOSPFMaxLSA", - "categories": [ - "ospf" - ], - "description": "Verifies all OSPF instances did not cross the maximum LSA threshold.", - "result": "skipped", - "messages": [ - "No OSPF instance found." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyCoredump", + "categories": [ + "system" + ], + "description": "Verifies there are no core dump files.", + "result": "success", + "messages": [], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifySTPBlockedPorts", - "categories": [ - "stp" - ], - "description": "Verifies there is no STP blocked ports.", - "result": "success", - "messages": [], - "custom_field": null + "name": "s1-spine1", + "test": "VerifyISISGracefulRestart", + "categories": [ + "isis" + ], + "description": "Verifies the IS-IS graceful restart feature.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null }, { - "name": "s1-spine1", - "test": "VerifyLANZ", - "categories": [ - "lanz" - ], - "description": "Verifies if LANZ is enabled.", - "result": "skipped", - "messages": [ - "VerifyLANZ test is not supported on cEOSLab." - ], - "custom_field": null + "name": "s1-spine1", + "test": "VerifySSHStatus", + "categories": [ + "security" + ], + "description": "Verifies if the SSHD agent is disabled in the default VRF.", + "result": "failure", + "messages": [ + "SSHD status for Default VRF is enabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTacacsSourceIntf", + "categories": [ + "aaa" + ], + "description": "Verifies TACACS source-interface for a specified VRF.", + "result": "failure", + "messages": [ + "VRF: MGMT Source Interface: Management0 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMcsClientMounts", + "categories": [ + "cvx" + ], + "description": "Verify if all MCS client mounts are in mountStateMountComplete.", + "result": "failure", + "messages": [ + "MCS Client mount states are not present" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIGMPSnoopingGlobal", + "categories": [ + "multicast" + ], + "description": "Verifies the IGMP snooping global status.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyISISNeighborCount", + "categories": [ + "isis" + ], + "description": "Verifies the number of IS-IS neighbors per interface and level.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyGreenTCounters", + "categories": [ + "greent" + ], + "description": "Verifies if the GreenT counters are incremented.", + "result": "failure", + "messages": [ + "GreenT counters are not incremented" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBFDSpecificPeers", + "categories": [ + "bfd" + ], + "description": "Verifies the state of IPv4 BFD peer sessions.", + "result": "failure", + "messages": [ + "Peer: 192.0.255.8 VRF: default - Not found", + "Peer: 192.0.255.7 VRF: default - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIllegalLACP", + "categories": [ + "interfaces" + ], + "description": "Verifies there are no illegal LACP packets in all port channels.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyCVXClusterStatus", + "categories": [ + "cvx" + ], + "description": "Verifies the CVX Server Cluster status.", + "result": "failure", + "messages": [ + "CVX Server status is not enabled", + "CVX Server is not a cluster" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeersHealthRibd", + "categories": [ + "bgp" + ], + "description": "Verifies the health of all the BGP peers.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyOSPFNeighborState", + "categories": [ + "ospf" + ], + "description": "Verifies all OSPF neighbors are in FULL state.", + "result": "skipped", + "messages": [ + "OSPF not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEOSExtensions", + "categories": [ + "software" + ], + "description": "Verifies that all EOS extensions installed on the device are enabled for boot persistence.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingTimestamp", + "categories": [ + "logging" + ], + "description": "Verifies if logs are generated with the appropriate timestamp.", + "result": "failure", + "messages": [ + "Logs are not generated with the appropriate timestamp format" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpNotificationHost", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP notification host(s) (SNMP manager) configurations.", + "result": "failure", + "messages": [ + "No SNMP host is configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPathsHealth", + "categories": [ + "path-selection" + ], + "description": "Verifies the path and telemetry state of all paths under router path-selection.", + "result": "skipped", + "messages": [ + "VerifyPathsHealth test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAPIHttpsSSL", + "categories": [ + "security" + ], + "description": "Verifies if the eAPI has a valid SSL profile.", + "result": "failure", + "messages": [ + "eAPI HTTPS server SSL profile default is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMlagDualPrimary", + "categories": [ + "mlag" + ], + "description": "Verifies the MLAG dual-primary detection parameters.", + "result": "failure", + "messages": [ + "Dual-primary detection is disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEVPNType5Routes", + "categories": [ + "bgp" + ], + "description": "Verifies EVPN Type-5 routes for given IP prefixes and VNIs.", + "result": "failure", + "messages": [ + "Prefix: 192.168.10.0/24 VNI: 10 - No EVPN Type-5 routes found", + "Prefix: 192.168.20.0/24 VNI: 20 - No EVPN Type-5 routes found", + "Prefix: 192.168.30.0/24 VNI: 30 - No EVPN Type-5 routes found", + "Prefix: 192.168.40.0/24 VNI: 40 - No EVPN Type-5 routes found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPCounters", + "categories": [ + "stp" + ], + "description": "Verifies there is no errors in STP BPDU packets.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMlagConfigSanity", + "categories": [ + "mlag" + ], + "description": "Verifies there are no MLAG config-sanity inconsistencies.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySVI", + "categories": [ + "interfaces" + ], + "description": "Verifies the status of all SVIs.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAVTPathHealth", + "categories": [ + "avt" + ], + "description": "Verifies the status of all AVT paths for all VRFs.", + "result": "skipped", + "messages": [ + "VerifyAVTPathHealth test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPExchangedRoutes", + "categories": [ + "bgp" + ], + "description": "Verifies the advertised and received routes of BGP IPv4 peer(s).", + "result": "failure", + "messages": [ + "Peer: 172.30.255.5 VRF: default Advertised route: 192.0.254.5/32 - Not found", + "Peer: 172.30.255.5 VRF: default Received route: 192.0.255.4/32 - Not found", + "Peer: 172.30.255.1 VRF: default Advertised route: 192.0.255.1/32 - Not found", + "Peer: 172.30.255.1 VRF: default Advertised route: 192.0.254.5/32 - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPForwardingPorts", + "categories": [ + "stp" + ], + "description": "Verifies that all interfaces are forwarding for a provided list of VLAN(s).", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyInterfaceErrors", + "categories": [ + "interfaces" + ], + "description": "Verifies that the interfaces error counters are equal to zero.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyISISInterfaceMode", + "categories": [ + "isis" + ], + "description": "Verifies IS-IS interfaces are running in the correct mode.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEnvironmentCooling", + "categories": [ + "hardware" + ], + "description": "Verifies the status of power supply fans and all fan trays.", + "result": "skipped", + "messages": [ + "VerifyEnvironmentCooling test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpGroup", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP group configurations for specified version(s).", + "result": "failure", + "messages": [ + "Group: Group1 Version: v1 - Not configured", + "Group: Group2 Version: v3 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBFDPeersHealth", + "categories": [ + "bfd" + ], + "description": "Verifies the health of IPv4 BFD peers across all VRFs.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLLDPNeighbors", + "categories": [ + "connectivity" + ], + "description": "Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.", + "result": "failure", + "messages": [ + "Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3", + "Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyFieldNotice44Resolution", + "categories": [ + "field notices" + ], + "description": "Verifies that the device is using the correct Aboot version per FN0044.", + "result": "skipped", + "messages": [ + "VerifyFieldNotice44Resolution test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpErrorCounters", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP error counters.", + "result": "failure", + "messages": [ + "SNMP counters not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPRouteECMP", + "categories": [ + "bgp" + ], + "description": "Verifies BGP IPv4 route ECMP paths.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyUnifiedForwardingTableMode", + "categories": [ + "profiles" + ], + "description": "Verifies the device is using the expected UFT mode.", + "result": "skipped", + "messages": [ + "VerifyUnifiedForwardingTableMode test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpContact", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP contact of a device.", + "result": "failure", + "messages": [ + "SNMP contact is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpLocation", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP location of a device.", + "result": "failure", + "messages": [ + "SNMP location is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBannerLogin", + "categories": [ + "security" + ], + "description": "Verifies the login banner of a device.", + "result": "failure", + "messages": [ + "Login banner is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVxlanConfigSanity", + "categories": [ + "vxlan" + ], + "description": "Verifies there are no VXLAN config-sanity inconsistencies.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerCount", + "categories": [ + "bgp" + ], + "description": "Verifies the count of BGP peers for given address families.", + "result": "failure", + "messages": [ + "AFI: ipv4 SAFI: unicast VRF: PROD - VRF not configured", + "AFI: ipv4 SAFI: multicast VRF: DEV - VRF not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeersHealth", + "categories": [ + "bgp" + ], + "description": "Verifies the health of BGP peers for given address families.", + "result": "failure", + "messages": [ + "AFI: ipv6 SAFI: unicast VRF: DEV - VRF not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRunningConfigDiffs", + "categories": [ + "configuration" + ], + "description": "Verifies there is no difference between the running-config and the startup-config.", + "result": "failure", + "messages": [ + "--- flash:/startup-config\n+++ system:/running-config\n@@ -18,6 +18,7 @@\n hostname leaf1-dc1\n ip name-server vrf MGMT 10.14.0.1\n dns domain fun.aristanetworks.com\n+ip host vitthal 192.168.66.220\n !\n platform tfa\n personality arfa\n" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyNTP", + "categories": [ + "system" + ], + "description": "Verifies if NTP is synchronised.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEnvironmentPower", + "categories": [ + "hardware" + ], + "description": "Verifies the power supplies status.", + "result": "skipped", + "messages": [ + "VerifyEnvironmentPower test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAVTSpecificPath", + "categories": [ + "avt" + ], + "description": "Verifies the Adaptive Virtual Topology (AVT) path.", + "result": "skipped", + "messages": [ + "VerifyAVTSpecificPath test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAuthzMethods", + "categories": [ + "aaa" + ], + "description": "Verifies the AAA authorization method lists for different authorization types (commands, exec).", + "result": "failure", + "messages": [ + "AAA authorization methods local, none, logging are not matching for commands, exec" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpHostLogging", + "categories": [ + "snmp" + ], + "description": "Verifies SNMP logging configurations.", + "result": "failure", + "messages": [ + "SNMP logging is disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingLogsGeneration", + "categories": [ + "logging" + ], + "description": "Verifies if logs are generated.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyDNSServers", + "categories": [ + "services" + ], + "description": "Verifies if the DNS (Domain Name Service) servers are correctly configured.", + "result": "failure", + "messages": [ + "Server 10.14.0.1 VRF: default Priority: 1 - Not configured", + "Server 10.14.0.11 VRF: MGMT Priority: 0 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingHostname", + "categories": [ + "logging" + ], + "description": "Verifies if logs are generated with the device FQDN.", + "result": "failure", + "messages": [ + "Logs are not generated with the device FQDN" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTcamProfile", + "categories": [ + "profiles" + ], + "description": "Verifies the device TCAM profile.", + "result": "skipped", + "messages": [ + "VerifyTcamProfile test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAPIHttpStatus", + "categories": [ + "security" + ], + "description": "Verifies if eAPI HTTP server is disabled globally.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyInterfaceIPv4", + "categories": [ + "interfaces" + ], + "description": "Verifies the interface IPv4 addresses.", + "result": "failure", + "messages": [ + "Interface: Ethernet2 - IP address mismatch - Expected: 172.30.11.1/31 Actual: 10.100.0.11/31", + "Interface: Ethernet2 - Secondary IP address is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingSourceIntf", + "categories": [ + "logging" + ], + "description": "Verifies logging source-interface for a specified VRF.", + "result": "failure", + "messages": [ + "Source-interface: Management0 VRF: default - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRoutingProtocolModel", + "categories": [ + "routing" + ], + "description": "Verifies the configured routing protocol model.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPRedistribution", + "categories": [ + "bgp" + ], + "description": "Verifies BGP redistribution.", + "result": "error", + "messages": [ + "show bgp instance vrf all has failed: Invalid requested version for ModelMetaClass: no such revision 4, most current is 3" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMlagInterfaces", + "categories": [ + "mlag" + ], + "description": "Verifies there are no inactive or active-partial MLAG ports.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpSourceInterface", + "categories": [ + "snmp" + ], + "description": "Verifies SNMP source interfaces.", + "result": "failure", + "messages": [ + "SNMP source interface(s) not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEVPNType2Route", + "categories": [ + "bgp" + ], + "description": "Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.", + "result": "failure", + "messages": [ + "Address: 192.168.20.102 VNI: 10020 - No EVPN Type-2 route", + "Address: aa:c1:ab:5d:b4:1e VNI: 10010 - No EVPN Type-2 route" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpStatus", + "categories": [ + "snmp" + ], + "description": "Verifies if the SNMP agent is enabled.", + "result": "failure", + "messages": [ + "VRF: default - SNMP agent disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyActiveCVXConnections", + "categories": [ + "cvx" + ], + "description": "Verifies the number of active CVX Connections.", + "result": "failure", + "messages": [ + "'show cvx connections brief' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'connections')" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPRootPriority", + "categories": [ + "stp" + ], + "description": "Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).", + "result": "failure", + "messages": [ + "Instance: MST10 - Not configured", + "Instance: MST20 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerTtlMultiHops", + "categories": [ + "bgp" + ], + "description": "Verifies BGP TTL and max-ttl-hops count for BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: 172.30.11.2 VRF: test - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpPDUCounters", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP PDU counters.", + "result": "failure", + "messages": [ + "SNMP counters not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRunningConfigLines", + "categories": [ + "configuration" + ], + "description": "Search the Running-Config for the given RegEx patterns.", + "result": "failure", + "messages": [ + "Following patterns were not found: '^enable password.*$', 'bla bla'" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPtpGMStatus", + "categories": [ + "ptp" + ], + "description": "Verifies that the device is locked to a valid PTP Grandmaster.", + "result": "skipped", + "messages": [ + "VerifyPtpGMStatus test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIpVirtualRouterMac", + "categories": [ + "interfaces" + ], + "description": "Verifies the IP virtual router MAC address.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerASNCap", + "categories": [ + "bgp" + ], + "description": "Verifies the four octet ASN capability of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPProxyARP", + "categories": [ + "interfaces" + ], + "description": "Verifies if Proxy ARP is enabled.", + "result": "failure", + "messages": [ + "Interface: Ethernet1 - Proxy-ARP disabled", + "Interface: Ethernet2 - Proxy-ARP disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingPersistent", + "categories": [ + "logging" + ], + "description": "Verifies if logging persistent is enabled and logs are saved in flash.", + "result": "failure", + "messages": [ + "Persistent logging is disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMlagPrimaryPriority", + "categories": [ + "mlag" + ], + "description": "Verifies the configuration of the MLAG primary priority.", + "result": "failure", + "messages": [ + "MLAG primary priority mismatch - Expected: 3276 Actual: 32767" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStunServer", + "categories": [ + "stun" + ], + "description": "Verifies the STUN server status is enabled and running.", + "result": "failure", + "messages": [ + "STUN server status is disabled and not running" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyOSPFNeighborCount", + "categories": [ + "ospf" + ], + "description": "Verifies the number of OSPF neighbors in FULL state is the one we expect.", + "result": "skipped", + "messages": [ + "OSPF not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRoutingTableEntry", + "categories": [ + "routing" + ], + "description": "Verifies that the provided routes are present in the routing table of a specified VRF.", + "result": "failure", + "messages": [ + "The following route(s) are missing from the routing table of VRF default: 10.1.0.1, 10.1.0.2" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAdverseDrops", + "categories": [ + "hardware" + ], + "description": "Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches.", + "result": "skipped", + "messages": [ + "VerifyAdverseDrops test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyISISSegmentRoutingAdjacencySegments", + "categories": [ + "isis", + "segment-routing" + ], + "description": "Verifies IS-IS segment routing adjacency segments.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPv4ACL", + "categories": [ + "security" + ], + "description": "Verifies the configuration of IPv4 ACLs.", + "result": "failure", + "messages": [ + "ACL name: LabTest - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyOSPFMaxLSA", + "categories": [ + "ospf" + ], + "description": "Verifies all OSPF instances did not cross the maximum LSA threshold.", + "result": "skipped", + "messages": [ + "OSPF not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPNlriAcceptance", + "categories": [ + "bgp" + ], + "description": "Verifies that all received NLRI are accepted for all AFI/SAFI configured for BGP peers.", + "result": "failure", + "messages": [ + "Peer: 10.100.0.128 VRF: default - Not found", + "Peer: 2001:db8:1::2 VRF: default - Not found", + "Peer: fe80::2%Et1 VRF: default - Not found", + "Peer: fe80::2%Et1 VRF: default - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyCPUUtilization", + "categories": [ + "system" + ], + "description": "Verifies whether the CPU utilization is below 75%.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyDynamicVlanSource", + "categories": [ + "vlan" + ], + "description": "Verifies dynamic VLAN allocation for specified VLAN sources.", + "result": "failure", + "messages": [ + "Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEnvironmentSystemCooling", + "categories": [ + "hardware" + ], + "description": "Verifies the device's system cooling status.", + "result": "skipped", + "messages": [ + "VerifyEnvironmentSystemCooling test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyHardwareEntropy", + "categories": [ + "security" + ], + "description": "Verifies hardware entropy generation is enabled on device.", + "result": "failure", + "messages": [ + "Hardware entropy generation is disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySpecificIPSecConn", + "categories": [ + "security" + ], + "description": "Verifies the IPv4 security connections.", + "result": "failure", + "messages": [ + "Peer: 10.255.0.1 VRF: default - Not configured", + "Peer: 10.255.0.2 VRF: default - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingHosts", + "categories": [ + "logging" + ], + "description": "Verifies logging hosts (syslog servers) for a specified VRF.", + "result": "failure", + "messages": [ + "Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPTimers", + "categories": [ + "bgp" + ], + "description": "Verifies the timers of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: 172.30.11.5 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLANZ", + "categories": [ + "lanz" + ], + "description": "Verifies if LANZ is enabled.", + "result": "skipped", + "messages": [ + "VerifyLANZ test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPMode", + "categories": [ + "stp" + ], + "description": "Verifies the configured STP mode for a provided list of VLAN(s).", + "result": "failure", + "messages": [ + "VLAN 10 - Incorrect STP mode - Expected: rapidPvst Actual: mstp", + "VLAN 20 - Incorrect STP mode - Expected: rapidPvst Actual: mstp" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPtpOffset", + "categories": [ + "ptp" + ], + "description": "Verifies that the PTP timing offset is within +/- 1000ns from the master clock.", + "result": "skipped", + "messages": [ + "VerifyPtpOffset test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpUser", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP user configurations.", + "result": "failure", + "messages": [ + "User: test Group: test_group Version: v3 - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAVTRole", + "categories": [ + "avt" + ], + "description": "Verifies the AVT role of a device.", + "result": "skipped", + "messages": [ + "VerifyAVTRole test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVlanInternalPolicy", + "categories": [ + "vlan" + ], + "description": "Verifies the VLAN internal allocation policy and the range of VLANs.", + "result": "failure", + "messages": [ + "VLAN internal allocation policy: ascending - Incorrect end VLAN id configured - Expected: 4094 Actual: 1199" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTelnetStatus", + "categories": [ + "security" + ], + "description": "Verifies if Telnet is disabled in the default VRF.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyHostname", + "categories": [ + "services" + ], + "description": "Verifies the hostname of a device.", + "result": "failure", + "messages": [ + "Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerRouteLimit", + "categories": [ + "bgp" + ], + "description": "Verifies maximum routes and warning limit for BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVxlan1Interface", + "categories": [ + "vxlan" + ], + "description": "Verifies the Vxlan1 interface status.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyZeroTouch", + "categories": [ + "configuration" + ], + "description": "Verifies ZeroTouch is disabled.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerMPCaps", + "categories": [ + "bgp" + ], + "description": "Verifies the multiprotocol capabilities of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: default - ipv4MplsLabels not found", + "Interface: Ethernet1 VRF: default - ipv4MplsVpn not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBFDPeersIntervals", + "categories": [ + "bfd" + ], + "description": "Verifies the timers of IPv4 BFD peer sessions.", + "result": "failure", + "messages": [ + "Peer: 192.0.255.8 VRF: default - Not found", + "Peer: 192.0.255.7 VRF: default - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVxlan1ConnSettings", + "categories": [ + "vxlan" + ], + "description": "Verifies Vxlan1 source interface and UDP port.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRoutingStatus", + "categories": [ + "routing" + ], + "description": "Verifies the routing status for IPv4/IPv6 unicast, multicast, and IPv6 interfaces (RFC5549).", + "result": "failure", + "messages": [ + "IPv6 unicast routing enabled status mismatch - Expected: True Actual: False" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMlagReloadDelay", + "categories": [ + "mlag" + ], + "description": "Verifies the reload-delay parameters of the MLAG configuration.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPv4RouteNextHops", + "categories": [ + "routing" + ], + "description": "Verifies the next-hops of the IPv4 prefixes.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerMD5Auth", + "categories": [ + "bgp" + ], + "description": "Verifies the MD5 authentication and state of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: 172.30.11.5 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: default - Session does not have MD5 authentication enabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySSHIPv6Acl", + "categories": [ + "security" + ], + "description": "Verifies if the SSHD agent has IPv6 ACL(s) configured.", + "result": "failure", + "messages": [ + "VRF: default - SSH IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPBlockedPorts", + "categories": [ + "stp" + ], + "description": "Verifies there is no STP blocked ports.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingErrors", + "categories": [ + "logging" + ], + "description": "Verifies there are no syslog messages with a severity of ERRORS or higher.", + "result": "failure", + "messages": [ + "Device has reported syslog messages with a severity of ERRORS or higher:\nApr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF data AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes \n Apr 29 08:01:27 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: sent to neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes \n Apr 29 08:01:29 leaf1-dc1 Bgp: %BGP-3-NOTIFICATION: received from neighbor 10.100.4.5 (VRF guest AS 65102) 6/7 (Cease/connection collision resolution) 0 bytes \n \n" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyInterfacesSpeed", + "categories": [ + "interfaces" + ], + "description": "Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.", + "result": "failure", + "messages": [ + "Interface: Ethernet2 - Bandwidth mismatch - Expected: 10.0Gbps Actual: 1Gbps", + "Interface: Ethernet3 - Bandwidth mismatch - Expected: 100.0Gbps Actual: 1Gbps", + "Interface: Ethernet3 - Auto-negotiation mismatch - Expected: success Actual: unknown", + "Interface: Ethernet3 - Data lanes count mismatch - Expected: 1 Actual: 0", + "Interface: Ethernet2 - Bandwidth mismatch - Expected: 2.5Gbps Actual: 1Gbps" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyInterfaceErrDisabled", + "categories": [ + "interfaces" + ], + "description": "Verifies there are no interfaces in the errdisabled state.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPortChannels", + "categories": [ + "interfaces" + ], + "description": "Verifies there are no inactive ports in all port channels.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoopbackCount", + "categories": [ + "interfaces" + ], + "description": "Verifies the number of loopback interfaces and their status.", + "result": "failure", + "messages": [ + "Loopback interface(s) count mismatch: Expected 3 Actual: 2" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerSession", + "categories": [ + "bgp" + ], + "description": "Verifies the session state of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 10.1.0.1 VRF: default - Not found", + "Peer: 10.1.0.2 VRF: default - Not found", + "Peer: 10.1.255.2 VRF: DEV - Not found", + "Peer: 10.1.255.4 VRF: DEV - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Vlan3499 VRF: PROD - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyISISSegmentRoutingTunnels", + "categories": [ + "isis", + "segment-routing" + ], + "description": "Verify ISIS-SR tunnels computed by device.", + "result": "skipped", + "messages": [ + "IS-IS-SR not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerSessionRibd", + "categories": [ + "bgp" + ], + "description": "Verifies the session state of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 10.1.0.1 VRF: default - Not found", + "Peer: 10.1.255.4 VRF: DEV - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpIPv6Acl", + "categories": [ + "snmp" + ], + "description": "Verifies if the SNMP agent has IPv6 ACL(s) configured.", + "result": "failure", + "messages": [ + "VRF: default - Incorrect SNMP IPv6 ACL(s) - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingEntries", + "categories": [ + "logging" + ], + "description": "Verifies that the expected log string is present in the last specified log messages.", + "result": "failure", + "messages": [ + "Pattern: `.*ACCOUNTING-5-EXEC: cvpadmin ssh.*` - Not found in last 30 alerts log entries", + "Pattern: `.*SPANTREE-6-INTERFACE_ADD:.*` - Not found in last 10 critical log entries" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyManagementCVX", + "categories": [ + "cvx" + ], + "description": "Verifies the management CVX global status.", + "result": "failure", + "messages": [ + "Management CVX status is not valid: Expected: enabled Actual: disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStormControlDrops", + "categories": [ + "interfaces" + ], + "description": "Verifies there are no interface storm-control drop counters.", + "result": "skipped", + "messages": [ + "VerifyStormControlDrops test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPv4RouteType", + "categories": [ + "routing" + ], + "description": "Verifies the route-type of the IPv4 prefixes.", + "result": "failure", + "messages": [ + "Prefix: 10.100.0.12/31 VRF: default - Route not found", + "Prefix: 10.100.1.5/32 VRF: default - Incorrect route type - Expected: iBGP Actual: connected" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVxlanVtep", + "categories": [ + "vxlan" + ], + "description": "Verifies Vxlan1 VTEP peers.", + "result": "failure", + "messages": [ + "The following VTEP peer(s) are missing from the Vxlan1 interface: 10.1.1.5, 10.1.1.6", + "Unexpected VTEP peer(s) on Vxlan1 interface: 10.100.2.3" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStunClientTranslation", + "categories": [ + "stun" + ], + "description": "Verifies the translation for a source address on a STUN client.", + "result": "failure", + "messages": [ + "Client 172.18.3.2 Port: 4500 - STUN client translation not found", + "Client 100.64.3.2 Port: 4500 - STUN client translation not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAPISSLCertificate", + "categories": [ + "security" + ], + "description": "Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStunClient", + "categories": [ + "stun" + ], + "description": "(Deprecated) Verifies the translation for a source address on a STUN client.", + "result": "failure", + "messages": [ + "Client 172.18.3.2 Port: 4500 - STUN client translation not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTransceiversTemperature", + "categories": [ + "hardware" + ], + "description": "Verifies if all the transceivers are operating at an acceptable temperature.", + "result": "skipped", + "messages": [ + "VerifyTransceiversTemperature test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPtpLockStatus", + "categories": [ + "ptp" + ], + "description": "Verifies that the device was locked to the upstream PTP GM in the last minute.", + "result": "skipped", + "messages": [ + "VerifyPtpLockStatus test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStpTopologyChanges", + "categories": [ + "stp" + ], + "description": "Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEOSVersion", + "categories": [ + "software" + ], + "description": "Verifies the EOS version of the device.", + "result": "failure", + "messages": [ + "EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBannerMotd", + "categories": [ + "security" + ], + "description": "Verifies the motd banner of a device.", + "result": "failure", + "messages": [ + "MOTD banner is not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySSHIPv4Acl", + "categories": [ + "security" + ], + "description": "Verifies if the SSHD agent has IPv4 ACL(s) configured.", + "result": "failure", + "messages": [ + "VRF: default - SSH IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLACPInterfacesStatus", + "categories": [ + "interfaces" + ], + "description": "Verifies the Link Aggregation Control Protocol (LACP) status of the interface.", + "result": "failure", + "messages": [ + "Interface: Ethernet1 Port-Channel: Port-Channel100 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIGMPSnoopingVlans", + "categories": [ + "multicast" + ], + "description": "Verifies the IGMP snooping status for the provided VLANs.", + "result": "failure", + "messages": [ + "VLAN10 - Incorrect IGMP state - Expected: disabled Actual: enabled", + "Supplied vlan 12 is not present on the device" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMaintenance", + "categories": [ + "system" + ], + "description": "Verifies that the device is not currently under or entering maintenance.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyFieldNotice72Resolution", + "categories": [ + "field notices" + ], + "description": "Verifies if the device is exposed to FN0072, and if the issue has been mitigated.", + "result": "skipped", + "messages": [ + "VerifyFieldNotice72Resolution test is not supported on cEOSLab" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRoutingTableSize", + "categories": [ + "routing" + ], + "description": "Verifies the size of the IP routing table of the default VRF.", + "result": "failure", + "messages": [ + "Routing table routes are outside the routes range - Expected: 2 <= to >= 20 Actual: 35" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPSpecificPeers", + "categories": [ + "bgp" + ], + "description": "Verifies the health of specific BGP peer(s) for given address families.", + "result": "failure", + "messages": [ + "AFI: evpn Peer: 10.1.0.1 - Not configured", + "AFI: evpn Peer: 10.1.0.2 - Not configured", + "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.254.1 - Not configured", + "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.0 - Not configured", + "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.2 - Not configured", + "AFI: ipv4 SAFI: unicast VRF: default Peer: 10.1.255.4 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpIPv4Acl", + "categories": [ + "snmp" + ], + "description": "Verifies if the SNMP agent has IPv4 ACL(s) configured.", + "result": "failure", + "messages": [ + "VRF: default - Incorrect SNMP IPv4 ACL(s) - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerDropStats", + "categories": [ + "bgp" + ], + "description": "Verifies BGP NLRI drop statistics of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBFDPeersRegProtocols", + "categories": [ + "bfd" + ], + "description": "Verifies the registered routing protocol of IPv4 BFD peer sessions.", + "result": "failure", + "messages": [ + "Peer: 192.0.255.7 VRF: default - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPSecConnHealth", + "categories": [ + "security" + ], + "description": "Verifies all IPv4 security connections.", + "result": "failure", + "messages": [ + "No IPv4 security connection configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPPeerUpdateErrors", + "categories": [ + "bgp" + ], + "description": "Verifies BGP update error counters of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyFileSystemUtilization", + "categories": [ + "system" + ], + "description": "Verifies that no partition is utilizing more than 75% of its disk space.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAPIIPv6Acl", + "categories": [ + "security" + ], + "description": "Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.", + "result": "failure", + "messages": [ + "VRF: default - eAPI IPv6 ACL(s) count mismatch - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingAccounting", + "categories": [ + "logging" + ], + "description": "Verifies if AAA accounting logs are generated.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyMcsServerMounts", + "categories": [ + "cvx" + ], + "description": "Verify if all MCS server mounts are in a MountComplete state.", + "result": "failure", + "messages": [ + "'show cvx mounts' failed on s1-spine1: Unavailable command (controller not ready) (at token 2: 'mounts')" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyGreenT", + "categories": [ + "greent" + ], + "description": "Verifies if a GreenT policy other than the default is created.", + "result": "failure", + "messages": [ + "No GreenT policy is created" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBgpRouteMaps", + "categories": [ + "bgp" + ], + "description": "Verifies BGP inbound and outbound route-maps of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.1 VRF: default - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found", + "Interface: Ethernet1 VRF: MGMT - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAPIIPv4Acl", + "categories": [ + "security" + ], + "description": "Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.", + "result": "failure", + "messages": [ + "VRF: default - eAPI IPv4 ACL(s) count mismatch - Expected: 3 Actual: 0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPRoutePaths", + "categories": [ + "bgp" + ], + "description": "Verifies BGP IPv4 route paths.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPAdvCommunities", + "categories": [ + "bgp" + ], + "description": "Verifies the advertised communities of BGP peers.", + "result": "failure", + "messages": [ + "Peer: 172.30.11.17 VRF: default - Not found", + "Peer: 172.30.11.21 VRF: MGMT - Not found", + "Peer: fd00:dc:1::1 VRF: default - Not found" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTerminAttrVersion", + "categories": [ + "software" + ], + "description": "Verifies the TerminAttr version of the device.", + "result": "failure", + "messages": [ + "TerminAttr version mismatch - Actual: v1.29.0 not in Expected: v1.13.6, v1.8.0" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPDisabledVlans", + "categories": [ + "stp" + ], + "description": "Verifies the STP disabled VLAN(s).", + "result": "failure", + "messages": [ + "VLAN: 6 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySyslogLogging", + "categories": [ + "logging" + ], + "description": "Verifies if syslog logging is enabled.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAgentLogs", + "categories": [ + "system" + ], + "description": "Verifies there are no agent crash reports.", + "result": "success", + "messages": [], + "custom_field": null } ] diff --git a/tests/units/test__runner.py b/tests/units/test__runner.py new file mode 100644 index 0000000..46c63ed --- /dev/null +++ b/tests/units/test__runner.py @@ -0,0 +1,414 @@ +# 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. +"""Test anta._runner.py.""" + +from __future__ import annotations + +import logging +import os +from collections import defaultdict +from pathlib import Path +from typing import ClassVar + +import pytest +import respx +from pydantic import ValidationError + +from anta._runner import AntaRunContext, AntaRunFilters, AntaRunner +from anta.catalog import AntaCatalog, AntaTestDefinition +from anta.inventory import AntaInventory +from anta.models import AntaCommand, AntaTemplate, AntaTest +from anta.result_manager import ResultManager +from anta.result_manager.models import TestResult as AntaTestResult +from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings +from anta.tests.routing.generic import VerifyRoutingTableEntry + +DATA_DIR: Path = Path(__file__).parent.parent.resolve() / "data" + + +class TestAntaRunner: + """Test AntaRunner class.""" + + def test_init_with_default_settings(self, caplog: pytest.LogCaptureFixture) -> None: + """Test initialization with default settings.""" + caplog.set_level(logging.DEBUG) + default_settings = {"nofile": DEFAULT_NOFILE, "max_concurrency": DEFAULT_MAX_CONCURRENCY} + + runner = AntaRunner() + + assert f"AntaRunner initialized with settings: {default_settings}" in caplog.messages + assert runner._settings + + def test_init_with_custom_env_settings(self, caplog: pytest.LogCaptureFixture, setenvvar: pytest.MonkeyPatch) -> None: + """Test initialization with custom env settings.""" + caplog.set_level(logging.DEBUG) + desired_settings = {"nofile": 1048576, "max_concurrency": 10000} + setenvvar.setenv("ANTA_NOFILE", str(desired_settings["nofile"])) + setenvvar.setenv("ANTA_MAX_CONCURRENCY", str(desired_settings["max_concurrency"])) + + runner = AntaRunner() + + assert f"AntaRunner initialized with settings: {desired_settings}" in caplog.messages + assert runner._settings + + def test_init_with_provided_settings(self, caplog: pytest.LogCaptureFixture) -> None: + """Test initialization with provided settings.""" + caplog.set_level(logging.DEBUG) + desired_settings = AntaRunnerSettings(nofile=1048576, max_concurrency=10000) + + runner = AntaRunner(settings=desired_settings) + + assert f"AntaRunner initialized with settings: {desired_settings.model_dump()}" in caplog.messages + assert runner._settings + + async def test_dry_run(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner.run() in dry-run.""" + caplog.set_level(logging.INFO) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner = AntaRunner() + ctx = await runner.run(inventory, catalog, dry_run=True) + + # Validate the final context attributes + assert ctx.selected_inventory == ctx.inventory == inventory + assert len(ctx.manager) > 0 + assert ctx.manager.status == "unset" + assert ctx.total_tests_scheduled > 0 + assert ctx.total_devices_filtered_by_tags == 0 + assert ctx.total_devices_unreachable == 0 + assert ctx.total_devices_selected_for_testing == ctx.total_devices_in_inventory == len(inventory) + assert ctx.duration is not None + + assert "Dry-run mode, exiting before running the tests." in caplog.messages + + @pytest.mark.parametrize( + ("filters", "expected_devices", "expected_tests"), + [ + pytest.param( + AntaRunFilters(devices=None, tests=None, tags=None), + 3, + 27, + id="all-tests", + ), + pytest.param( + AntaRunFilters(devices=None, tests=None, tags={"leaf"}), + 2, + 6, + id="1-tag", + ), + pytest.param( + AntaRunFilters(devices=None, tests=None, tags={"leaf", "spine"}), + 3, + 9, + id="2-tags", + ), + pytest.param( + AntaRunFilters(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None), + 3, + 5, + id="filtered-tests", + ), + pytest.param( + AntaRunFilters(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}), + 2, + 4, + id="1-tag-filtered-tests", + ), + pytest.param( + AntaRunFilters(devices={"leaf1"}, tests=None, tags=None), + 1, + 9, + id="filtered-devices", + ), + pytest.param( + AntaRunFilters(devices=None, tests=None, tags={"invalid"}), + 0, + 0, + id="invalid-tag", + ), + pytest.param( + AntaRunFilters(devices={"invalid"}, tests=None, tags=None), + 0, + 0, + id="invalid-device", + ), + pytest.param( + AntaRunFilters(devices=None, tests={"invalid"}, tags=None), + 3, + 0, + id="invalid-test", + ), + pytest.param( + AntaRunFilters(devices=None, tests=None, tags={"dc1"}), + 1, + 0, + id="device-tag-no-tests", + ), + ], + ) + async def test_run_filters(self, caplog: pytest.LogCaptureFixture, filters: AntaRunFilters, expected_devices: int, expected_tests: int) -> None: + """Test AntaRunner.run() with different filters.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner = AntaRunner() + ctx = await runner.run(inventory, catalog, filters=filters, dry_run=True) + + # Gather the warning message + warning_msg = None + if expected_devices == 0: + msg = "The inventory is empty after filtering by tags/devices. " + if filters.devices: + msg += f"Devices filter: {', '.join(sorted(filters.devices))}. " + if filters.tags: + msg += f"Tags filter: {', '.join(sorted(filters.tags))}. " + msg += "Exiting ..." + elif expected_tests == 0: + msg = "No tests scheduled to run after filtering by tags/tests. " + if filters.tests: + msg += f"Tests filter: {', '.join(sorted(filters.tests))}. " + if filters.tags: + msg += f"Tags filter: {', '.join(sorted(filters.tags))}. " + msg += "Exiting ..." + + if warning_msg is not None: + assert msg in ctx.warnings_at_setup + assert msg in caplog.messages + + assert ctx.total_tests_scheduled == expected_tests + assert ctx.total_devices_selected_for_testing == expected_devices + + async def test_run_invalid_filters(self) -> None: + """Test AntaRunner.run() with invalid filters.""" + inventory = AntaInventory() + catalog = AntaCatalog() + runner = AntaRunner() + + with pytest.raises(ValidationError, match="1 validation error for AntaRunFilters"): + await runner.run(inventory, catalog, filters=AntaRunFilters(devices="invalid"), dry_run=True) # type: ignore[arg-type] + + async def test_run_provided_manager(self) -> None: + """Test AntaRunner.run() with a provided ResultManager instance.""" + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + manager = ResultManager() + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog, manager, dry_run=True) + assert isinstance(ctx.manager, ResultManager) + assert ctx.manager is manager + assert len(manager) == 27 + + async def test_run_provided_manager_not_empty(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner.run() with a provided non-empty ResultManager instance.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + manager = ResultManager() + test = AntaTestResult(name="DC1-LEAF1A", test="VerifyNTP", categories=["system"], description="NTP Test") + runner = AntaRunner() + manager.add(test) + + ctx = await runner.run(inventory, catalog, manager, dry_run=True) + assert isinstance(ctx.manager, ResultManager) + assert ctx.manager is manager + assert len(manager) == 28 + assert len(manager.device_stats) == ctx.total_devices_selected_for_testing + 1 + + warning_msg = ( + "Appending new results to the provided ResultManager which already holds 1 results. Statistics in this run context are for the current execution only." + ) + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + async def test_run_empty_catalog(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner.run() with an empty AntaCatalog.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog() + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog) + + warning_msg = "The list of tests is empty. Exiting ..." + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + async def test_run_empty_inventory(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner.run() with an empty AntaInventory.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory() + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog) + + warning_msg = "The initial inventory is empty. Exiting ..." + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + @pytest.mark.parametrize("inventory", [{"reachable": False}], indirect=True) + async def test_run_no_reachable_devices(self, caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: + """Test AntaRunner.run() with an empty AntaInventory.""" + caplog.set_level(logging.WARNING) + + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog) + assert ctx.total_devices_unreachable == ctx.total_devices_in_inventory + assert "device-0" in ctx.devices_unreachable_at_setup + + warning_msg = "No reachable devices found for testing after connectivity checks. Exiting ..." + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + async def test_run_invalid_anta_test(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner.run() with a provided non-empty ResultManager instance.""" + caplog.set_level(logging.CRITICAL) + + class InvalidTest(AntaTest): + """ANTA test that raises an exception when test is called.""" + + categories: ClassVar[list[str]] = [] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [] + + def test(self) -> None: # type: ignore[override] + """Test function.""" + msg = "Test not implemented" + raise NotImplementedError(msg) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + test_definition = AntaTestDefinition(test=InvalidTest, inputs=None) + catalog = AntaCatalog(tests=[test_definition]) + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog, dry_run=True) + assert len(ctx.manager) == 0 + + error_msg = ( + f"There is an error when creating test {__name__}.InvalidTest.\n" + "If this is not a custom test implementation: " + "Please reach out to the maintainer team or open an issue on Github: https://github.com/aristanetworks/anta.\n" + "NotImplementedError: Test not implemented" + ) + assert error_msg in caplog.messages + + async def test_log_run_information_default(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner._log_run_information with default settings.""" + caplog.set_level(logging.INFO) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner = AntaRunner() + await runner.run(inventory, catalog, dry_run=True) + + expected_output = [ + "ANTA NRFU Dry Run Information", + "Devices:", + " Total in initial inventory: 3", + " Selected for testing: 3", + "Total number of selected tests: 27", + ] + for line in expected_output: + assert line in caplog.text + + async def test_log_run_information_concurrency_limit(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner._log_run_information with higher tests count than concurrency limit.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner_settings = AntaRunnerSettings(max_concurrency=20) + runner = AntaRunner(settings=runner_settings) + + ctx = await runner.run(inventory, catalog, dry_run=True) + + warning_msg = "Tests count (27) exceeds concurrent limit (20). Tests will be throttled. Please consult the ANTA FAQ." + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + @pytest.mark.skipif(os.name != "posix", reason="Very unlikely to happen on non-POSIX systems due to sys.maxsize") + async def test_log_run_information_file_descriptor_limit(self, caplog: pytest.LogCaptureFixture) -> None: + """Test AntaRunner._log_run_information with higher connections count than file descriptor limit.""" + caplog.set_level(logging.WARNING) + + inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta") + catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml") + runner_settings = AntaRunnerSettings(nofile=128) + runner = AntaRunner(settings=runner_settings) + + ctx = await runner.run(inventory, catalog, dry_run=True) + + warning_msg = "Potential connections (300) exceeds file descriptor limit (128). Connection errors may occur. Please consult the ANTA FAQ." + assert warning_msg in ctx.warnings_at_setup + assert warning_msg in caplog.messages + + @pytest.mark.parametrize(("inventory"), [{"count": 3}], indirect=True) + @respx.mock + async def test_run(self, inventory: AntaInventory) -> None: + """Test AntaRunner.run().""" + # Mock the eAPI requests + respx.post(path="/command-api", headers={"Content-Type": "application/json-rpc"}, json__params__cmds__0__cmd="show ip route vrf default").respond( + json={"result": [{"vrfs": {"default": {"routes": {}}}}]} + ) + tests = [AntaTestDefinition(test=VerifyRoutingTableEntry, inputs={"routes": [f"10.1.0.{i}"], "collect": "all"}) for i in range(5)] + catalog = AntaCatalog(tests=tests) + runner = AntaRunner() + + ctx = await runner.run(inventory, catalog) + + assert ctx.total_devices_selected_for_testing == 3 + assert ctx.total_tests_scheduled == 15 + assert len(ctx.warnings_at_setup) == 0 + assert len(ctx.manager) == 15 + for result in ctx.manager.results: + assert result.result == "failure" + + +# pylint: disable=too-few-public-methods +class TestAntaRunContext: + """Test AntaRunContext class.""" + + def test_init(self) -> None: + """Test initialization.""" + inventory = AntaInventory() + catalog = AntaCatalog() + manager = ResultManager() + filters = AntaRunFilters() + + ctx = AntaRunContext(inventory, catalog, manager, filters) + + # Test initialized attributes + assert ctx.inventory is inventory + assert ctx.catalog is catalog + assert ctx.manager is manager + assert ctx.filters is filters + assert not ctx.dry_run + + assert isinstance(ctx.selected_inventory, AntaInventory) + assert len(ctx.selected_inventory) == 0 + assert isinstance(ctx.selected_tests, defaultdict) + assert len(ctx.selected_tests) == 0 + assert isinstance(ctx.devices_filtered_at_setup, list) + assert len(ctx.devices_filtered_at_setup) == 0 + assert isinstance(ctx.devices_unreachable_at_setup, list) + assert len(ctx.devices_unreachable_at_setup) == 0 + assert isinstance(ctx.warnings_at_setup, list) + assert len(ctx.warnings_at_setup) == 0 + assert ctx.start_time is None + assert ctx.end_time is None + + # Test properties + assert ctx.total_devices_in_inventory == 0 + assert ctx.total_devices_filtered_by_tags == 0 + assert ctx.total_devices_unreachable == 0 + assert ctx.total_devices_selected_for_testing == 0 + assert ctx.total_tests_scheduled == 0 + assert ctx.duration is None diff --git a/tests/units/test_custom_types.py b/tests/units/test_custom_types.py index c0f3f3a..8c1121d 100644 --- a/tests/units/test_custom_types.py +++ b/tests/units/test_custom_types.py @@ -23,6 +23,7 @@ from anta.custom_types import ( REGEXP_TYPE_VXLAN_SRC_INTERFACE, aaa_group_prefix, bgp_multiprotocol_capabilities_abbreviations, + convert_reload_cause, interface_autocomplete, interface_case_sensitivity, snmp_v3_prefix, @@ -267,3 +268,28 @@ def test_snmp_v3_prefix_valid_input() -> None: assert snmp_v3_prefix("auth") == "v3Auth" assert snmp_v3_prefix("noauth") == "v3NoAuth" assert snmp_v3_prefix("priv") == "v3Priv" + + +@pytest.mark.parametrize( + ("str_input", "expected_output"), + [ + pytest.param("Ztp", "System reloaded due to Zero Touch Provisioning", id="valid"), + pytest.param("USER", "Reload requested by the user.", id="valid"), + pytest.param("fpga", "Reload requested after FPGA upgrade", id="valid"), + ], +) +def test_convert_reload_cause(str_input: str, expected_output: str) -> None: + """Test convert_reload_cause.""" + assert convert_reload_cause(str_input) == expected_output + + +@pytest.mark.parametrize( + ("str_input"), + [ + pytest.param("ztp2", id="invalid"), + ], +) +def test_invalid_convert_reload_cause(str_input: str) -> None: + """Test invalid convert_reload_cause.""" + with pytest.raises(ValueError, match=r"Invalid reload cause: 'ztp2' - expected causes are \['ZTP', 'USER', 'FPGA'\]"): + convert_reload_cause(str_input) diff --git a/tests/units/test_device.py b/tests/units/test_device.py index e65eeb2..ee3e538 100644 --- a/tests/units/test_device.py +++ b/tests/units/test_device.py @@ -429,29 +429,8 @@ REFRESH_PARAMS: list[ParameterSet] = [ pytest.param( {}, ( - {"return_value": False}, - { - "return_value": { - "mfgName": "Arista", - "modelName": "DCS-7280CR3-32P4-F", - "hardwareRevision": "11.00", - "serialNumber": "JPE19500066", - "systemMacAddress": "fc:bd:67:3d:13:c5", - "hwMacAddress": "fc:bd:67:3d:13:c5", - "configMacAddress": "00:00:00:00:00:00", - "version": "4.31.1F-34361447.fraserrel (engineering build)", - "architecture": "x86_64", - "internalVersion": "4.31.1F-34361447.fraserrel", - "internalBuildId": "4940d112-a2fc-4970-8b5a-a16cd03fd08c", - "imageFormatVersion": "3.0", - "imageOptimization": "Default", - "bootupTimestamp": 1700729434.5892005, - "uptime": 20666.78, - "memTotal": 8099732, - "memFree": 4989568, - "isIntlVersion": False, - } - }, + {"side_effect": HTTPError(message="Unauthorized")}, + {}, ), {"is_online": False, "established": False, "hw_model": None}, id="is not online", @@ -613,6 +592,10 @@ class TestAntaDevice: """ assert device.cache_statistics == expected + def test_max_connections(self, device: AntaDevice) -> None: + """Test max_connections property.""" + assert device.max_connections is None + class TestAsyncEOSDevice: """Test for anta.device.AsyncEOSDevice.""" @@ -646,6 +629,16 @@ class TestAsyncEOSDevice: else: assert dev1 != dev2 + def test_max_connections(self, async_device: AsyncEOSDevice) -> None: + """Test max_connections property.""" + # HTTPX uses a max_connections of 100 by default + assert async_device.max_connections == 100 + + def test_max_connections_none(self, async_device: AsyncEOSDevice) -> None: + """Test max_connections property when not available in the session object.""" + with patch.object(async_device, "_session", None): + assert async_device.max_connections is None + @pytest.mark.parametrize( ("async_device", "patch_kwargs", "expected"), REFRESH_PARAMS, @@ -653,9 +646,9 @@ class TestAsyncEOSDevice: ) async def test_refresh(self, async_device: AsyncEOSDevice, patch_kwargs: list[dict[str, Any]], expected: dict[str, Any]) -> None: """Test AsyncEOSDevice.refresh().""" - with patch.object(async_device._session, "check_connection", **patch_kwargs[0]), patch.object(async_device._session, "cli", **patch_kwargs[1]): + with patch.object(async_device._session, "check_api_endpoint", **patch_kwargs[0]), patch.object(async_device._session, "cli", **patch_kwargs[1]): await async_device.refresh() - async_device._session.check_connection.assert_called_once() # type: ignore[attr-defined] # asynceapi.Device.check_connection is patched + async_device._session.check_api_endpoint.assert_called_once() # type: ignore[attr-defined] # asynceapi.Device.check_api_endpoint is patched if expected["is_online"]: async_device._session.cli.assert_called_once() # type: ignore[attr-defined] # asynceapi.Device.cli is patched assert async_device.is_online == expected["is_online"] diff --git a/tests/units/test_models.py b/tests/units/test_models.py index e7b1db9..47733f0 100644 --- a/tests/units/test_models.py +++ b/tests/units/test_models.py @@ -298,82 +298,30 @@ class FakeTestWithMissingTest(AntaTest): commands: ClassVar[list[AntaCommand | AntaTemplate]] = [] -ANTATEST_DATA: list[dict[str, Any]] = [ - { - "name": "no input", - "test": FakeTest, - "inputs": None, - "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}, - }, - { - "name": "extra input", - "test": FakeTest, +ANTATEST_DATA: dict[tuple[type[AntaTest], str], Any] = { + (FakeTest, "no input"): {"inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}}, + (FakeTest, "extra input"): { "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"}, - "expected": { - "__init__": { - "result": "error", - "messages": ["Extra inputs are not permitted"], - }, - "test": {"result": "error"}, - }, + "expected": {"__init__": {"result": "error", "messages": ["Extra inputs are not permitted"]}, "test": {"result": "error"}}, }, - { - "name": "no input", - "test": FakeTestWithInput, - "inputs": None, - "expected": { - "__init__": {"result": "error", "messages": ["Field required"]}, - "test": {"result": "error"}, - }, - }, - { - "name": "wrong input type", - "test": FakeTestWithInput, + (FakeTestWithInput, "no input"): {"inputs": None, "expected": {"__init__": {"result": "error", "messages": ["Field required"]}, "test": {"result": "error"}}}, + (FakeTestWithInput, "wrong input type"): { "inputs": {"string": 1}, - "expected": { - "__init__": { - "result": "error", - "messages": ["Input should be a valid string"], - }, - "test": {"result": "error"}, - }, + "expected": {"__init__": {"result": "error", "messages": ["Input should be a valid string"]}, "test": {"result": "error"}}, }, - { - "name": "good input", - "test": FakeTestWithInput, + (FakeTestWithInput, "good input"): { "inputs": {"string": "culpa! veniam quas quas veniam molestias, esse"}, - "expected": { - "__init__": {"result": "unset"}, - "test": { - "result": "success", - "messages": ["culpa! veniam quas quas veniam molestias, esse"], - }, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "success", "messages": ["culpa! veniam quas quas veniam molestias, esse"]}}, }, - { - "name": "good input", - "test": FakeTestWithTemplate, + (FakeTestWithTemplate, "good input"): { "inputs": {"interface": "Ethernet1"}, - "expected": { - "__init__": {"result": "unset"}, - "test": {"result": "success", "messages": ["show interface Ethernet1"]}, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "success", "messages": ["show interface Ethernet1"]}}, }, - { - "name": "wrong input type", - "test": FakeTestWithTemplate, + (FakeTestWithTemplate, "wrong input type"): { "inputs": {"interface": 1}, - "expected": { - "__init__": { - "result": "error", - "messages": ["Input should be a valid string"], - }, - "test": {"result": "error"}, - }, + "expected": {"__init__": {"result": "error", "messages": ["Input should be a valid string"]}, "test": {"result": "error"}}, }, - { - "name": "wrong render definition", - "test": FakeTestWithTemplateNoRender, + (FakeTestWithTemplateNoRender, "wrong render definition"): { "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { @@ -383,9 +331,7 @@ ANTATEST_DATA: list[dict[str, Any]] = [ "test": {"result": "error"}, }, }, - { - "name": "AntaTemplateRenderError", - "test": FakeTestWithTemplateBadRender1, + (FakeTestWithTemplateBadRender1, "AntaTemplateRenderError"): { "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { @@ -395,125 +341,57 @@ ANTATEST_DATA: list[dict[str, Any]] = [ "test": {"result": "error"}, }, }, - { - "name": "RuntimeError in render()", - "test": FakeTestWithTemplateBadRender2, + (FakeTestWithTemplateBadRender2, "RuntimeError in render()"): { "inputs": {"interface": "Ethernet1"}, "expected": { - "__init__": { - "result": "error", - "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): RuntimeError"], - }, + "__init__": {"result": "error", "messages": ["Exception in tests.units.test_models.FakeTestWithTemplateBadRender2.render(): RuntimeError"]}, "test": {"result": "error"}, }, }, - { - "name": "Extra template parameters in render()", - "test": FakeTestWithTemplateBadRender3, + (FakeTestWithTemplateBadRender3, "Extra template parameters in render()"): { "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": { "result": "error", "messages": [ - "Exception in tests.units.test_models.FakeTestWithTemplateBadRender3.render(): ValidationError: 1 validation error for AntaParams\n" - "extra\n" + "Exception in tests.units.test_models.FakeTestWithTemplateBadRender3.render(): ValidationError: 1 validation error for AntaParams\nextra\n" " Extra inputs are not permitted [type=extra_forbidden, input_value='blah', input_type=str]\n" ], }, "test": {"result": "error"}, }, }, - { - "name": "Access undefined template param in test()", - "test": FakeTestWithTemplateBadTest, + (FakeTestWithTemplateBadTest, "Access undefined template param in test()"): { "inputs": {"interface": "Ethernet1"}, "expected": { "__init__": {"result": "unset"}, "test": {"result": "error", "messages": ["AttributeError: 'AntaParams' object has no attribute 'wrong_template_param'"]}, }, }, - { - "name": "unskip on platforms", - "test": UnSkipOnPlatformTest, + (UnSkipOnPlatformTest, "unskip on platforms"): {"inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}}, + (SkipOnPlatformTest, "skip on platforms, unset"): {"inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "skipped"}}}, + (SkipOnPlatformTestWithInput, "skip on platforms, not unset"): { "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": {"result": "success"}, - }, + "expected": {"__init__": {"result": "error", "messages": ["Field required"]}, "test": {"result": "error"}}, }, - { - "name": "skip on platforms, unset", - "test": SkipOnPlatformTest, + (DeprecatedTestWithoutNewTest, "deprecate test without new test"): { "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": {"result": "skipped"}, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}, }, - { - "name": "skip on platforms, not unset", - "test": SkipOnPlatformTestWithInput, + (DeprecatedTestWithNewTest, "deprecate test with new test"): {"inputs": None, "expected": {"__init__": {"result": "unset"}, "test": {"result": "success"}}}, + (FakeTestWithFailedCommand, "failed command"): { "inputs": None, - "expected": { - "__init__": {"result": "error", "messages": ["Field required"]}, - "test": {"result": "error"}, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "error", "messages": ["show version has failed: failed command"]}}, }, - { - "name": "deprecate test without new test", - "test": DeprecatedTestWithoutNewTest, + (FakeTestWithUnsupportedCommand, "unsupported command"): { "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": {"result": "success"}, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "skipped", "messages": ["'show hardware counter drop' is not supported on pytest"]}}, }, - { - "name": "deprecate test with new test", - "test": DeprecatedTestWithNewTest, + (FakeTestWithKnownEOSError, "known EOS error command"): { "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": {"result": "success"}, - }, + "expected": {"__init__": {"result": "unset"}, "test": {"result": "failure", "messages": ["BGP inactive"]}}, }, - { - "name": "failed command", - "test": FakeTestWithFailedCommand, - "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": { - "result": "error", - "messages": ["show version has failed: failed command"], - }, - }, - }, - { - "name": "unsupported command", - "test": FakeTestWithUnsupportedCommand, - "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": { - "result": "skipped", - "messages": ["'show hardware counter drop' is not supported on pytest"], - }, - }, - }, - { - "name": "known EOS error command", - "test": FakeTestWithKnownEOSError, - "inputs": None, - "expected": { - "__init__": {"result": "unset"}, - "test": { - "result": "failure", - "messages": ["BGP inactive"], - }, - }, - }, -] +} BLACKLIST_COMMANDS_PARAMS = ["reload", "reload now", "reload --force", "write", "wr mem", "write memory", "conf t", "configure terminal", "configure session"] @@ -597,18 +475,20 @@ class TestAntaTest: for result_msg, expected_msg in zip(test.result.messages, expected["messages"]): # NOTE: zip(strict=True) has been added in Python 3.10 assert expected_msg in result_msg - @pytest.mark.parametrize("data", ANTATEST_DATA, ids=build_test_id) - def test__init__(self, device: AntaDevice, data: dict[str, Any]) -> None: + @pytest.mark.parametrize("data", ANTATEST_DATA.items(), ids=build_test_id) + def test__init__(self, device: AntaDevice, data: tuple[tuple[type[AntaTest], str], Any]) -> None: """Test the AntaTest constructor.""" - expected = data["expected"]["__init__"] - test = data["test"](device, inputs=data["inputs"]) + (anta_test, test_data) = data + expected = test_data["expected"]["__init__"] + test = anta_test[0](device, inputs=test_data["inputs"]) self._assert_test(test, expected) - @pytest.mark.parametrize("data", ANTATEST_DATA, ids=build_test_id) - def test_test(self, device: AntaDevice, data: dict[str, Any]) -> None: + @pytest.mark.parametrize("data", ANTATEST_DATA.items(), ids=build_test_id) + def test_test(self, device: AntaDevice, data: tuple[tuple[type[AntaTest], str], Any]) -> None: """Test the AntaTest.test method.""" - expected = data["expected"]["test"] - test = data["test"](device, inputs=data["inputs"]) + (anta_test, test_data) = data + expected = test_data["expected"]["test"] + test = anta_test[0](device, inputs=test_data["inputs"]) asyncio.run(test.test()) self._assert_test(test, expected) diff --git a/tests/units/test_runner.py b/tests/units/test_runner.py index 1b9c40c..dc18d23 100644 --- a/tests/units/test_runner.py +++ b/tests/units/test_runner.py @@ -7,18 +7,19 @@ from __future__ import annotations import logging import os -import sys from pathlib import Path +from typing import TYPE_CHECKING from unittest.mock import patch import pytest from anta.catalog import AntaCatalog -from anta.inventory import AntaInventory -from anta.result_manager import ResultManager -from anta.runner import main, prepare_tests +from anta.runner import prepare_tests -from .test_models import FakeTest, FakeTestWithMissingTest +from .test_models import FakeTest + +if TYPE_CHECKING: + from anta.inventory import AntaInventory if os.name == "posix": # The function is not defined on non-POSIX system @@ -30,47 +31,8 @@ DATA_DIR: Path = Path(__file__).parent.parent.resolve() / "data" FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)]) -async def test_empty_tests(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: - """Test that when the list of tests is empty, a log is raised.""" - caplog.set_level(logging.INFO) - manager = ResultManager() - await main(manager, inventory, AntaCatalog()) - - assert len(caplog.record_tuples) == 1 - assert "The list of tests is empty, exiting" in caplog.records[0].message - - -async def test_empty_inventory(caplog: pytest.LogCaptureFixture) -> None: - """Test that when the Inventory is empty, a log is raised.""" - caplog.set_level(logging.INFO) - manager = ResultManager() - await main(manager, AntaInventory(), FAKE_CATALOG) - assert len(caplog.record_tuples) == 3 - assert "The inventory is empty, exiting" in caplog.records[1].message - - -@pytest.mark.parametrize( - ("inventory", "tags", "devices"), - [ - pytest.param({"count": 1, "reachable": False}, None, None, id="not-reachable"), - pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"leaf"}, None, id="not-reachable-with-tag"), - pytest.param({"count": 1, "reachable": True}, {"invalid-tag"}, None, id="reachable-with-invalid-tag"), - pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": True}, None, {"invalid-device"}, id="reachable-with-invalid-device"), - pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, None, {"leaf1"}, id="not-reachable-with-device"), - pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"leaf"}, {"leaf1"}, id="not-reachable-with-device-and-tag"), - pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"invalid"}, {"invalid-device"}, id="reachable-with-invalid-tag-and-device"), - ], - indirect=["inventory"], -) -async def test_no_selected_device(caplog: pytest.LogCaptureFixture, inventory: AntaInventory, tags: set[str], devices: set[str]) -> None: - """Test that when the list of established devices is empty a log is raised.""" - caplog.set_level(logging.WARNING) - manager = ResultManager() - await main(manager, inventory, FAKE_CATALOG, tags=tags, devices=devices) - msg = f"No reachable device {f'matching the tags {tags} ' if tags else ''}was found.{f' Selected devices: {devices} ' if devices is not None else ''}" - assert msg in caplog.messages - - +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows") def test_adjust_rlimit_nofile_valid_env(caplog: pytest.LogCaptureFixture) -> None: """Test adjust_rlimit_nofile with valid environment variables.""" @@ -104,9 +66,11 @@ def test_adjust_rlimit_nofile_valid_env(caplog: pytest.LogCaptureFixture) -> Non setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (20480, 1048576)) +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows") def test_adjust_rlimit_nofile_invalid_env(caplog: pytest.LogCaptureFixture) -> None: - """Test adjust_rlimit_nofile with valid environment variables.""" + """Test adjust_rlimit_nofile with invalid environment variables.""" with ( caplog.at_level(logging.DEBUG), patch.dict("os.environ", {"ANTA_NOFILE": "invalid"}), @@ -138,31 +102,42 @@ def test_adjust_rlimit_nofile_invalid_env(caplog: pytest.LogCaptureFixture) -> N setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (16384, 1048576)) -@pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only") -async def test_check_runner_log_for_windows(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: - """Test log output for Windows host regarding rlimit.""" - caplog.set_level(logging.INFO) - manager = ResultManager() - # Using dry-run to shorten the test - await main(manager, inventory, FAKE_CATALOG, dry_run=True) - assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[-3].message - - -# We could instead merge multiple coverage report together but that requires more work than just this. -@pytest.mark.skipif(os.name != "posix", reason="Fake non-posix for coverage") -async def test_check_runner_log_for_windows_fake(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: - """Test log output for Windows host regarding rlimit.""" - with patch("os.name", new="win32"): - del sys.modules["anta.runner"] - from anta.runner import main # pylint: disable=W0621 - - caplog.set_level(logging.INFO) - manager = ResultManager() - # Using dry-run to shorten the test - await main(manager, inventory, FAKE_CATALOG, dry_run=True) - assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[-3].message +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows") +def test_adjust_rlimit_nofile_value_error(caplog: pytest.LogCaptureFixture) -> None: + """Test adjust_rlimit_nofile with invalid environment variables.""" + with ( + caplog.at_level(logging.DEBUG), + patch.dict("os.environ", {"ANTA_NOFILE": "666"}), + patch("anta.runner.resource.getrlimit") as getrlimit_mock, + patch("anta.runner.resource.setrlimit") as setrlimit_mock, + ): + # Simulate the default system limits + system_limits = (8192, 1048576) + + # Setup getrlimit mock return value + getrlimit_mock.return_value = system_limits + + # Simulate setrlimit behavior raising ValueError + def side_effect_setrlimit(_resource_id: int, _limits: tuple[int, int]) -> None: + msg = "not allowed to raise maximum limit" + raise ValueError(msg) + + setrlimit_mock.side_effect = side_effect_setrlimit + + result = adjust_rlimit_nofile() + + # Assert the limits were *NOT* updated as expected + assert result == system_limits + assert "Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: 8192 | Hard Limit: 1048576" in caplog.text + assert caplog.records[-1].levelname == "WARNING" + assert "Failed to set soft limit for open file descriptors for the current ANTA process" in caplog.records[-1].getMessage() + + setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (666, 1048576)) +# TODO: Remove this in ANTA v2.0.0 +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.parametrize( ("inventory", "tags", "tests", "devices_count", "tests_count"), [ @@ -190,29 +165,3 @@ async def test_prepare_tests( assert selected_tests is not None assert len(selected_tests) == devices_count assert sum(len(tests) for tests in selected_tests.values()) == tests_count - - -async def test_dry_run(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: - """Test that when dry_run is True, no tests are run.""" - caplog.set_level(logging.INFO) - manager = ResultManager() - await main(manager, inventory, FAKE_CATALOG, dry_run=True) - assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message - - -async def test_cannot_create_test(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None: - """Test that when an Exception is raised during test instantiation, it is caught and a log is raised.""" - caplog.set_level(logging.CRITICAL) - manager = ResultManager() - catalog = AntaCatalog.from_list([(FakeTestWithMissingTest, None)]) # type: ignore[type-abstract] - await main(manager, inventory, catalog) - msg = ( - "There is an error when creating test tests.units.test_models.FakeTestWithMissingTest.\nIf this is not a custom test implementation: " - "Please reach out to the maintainer team or open an issue on Github: https://github.com/aristanetworks/anta.\nTypeError: " - ) - msg += ( - "Can't instantiate abstract class FakeTestWithMissingTest without an implementation for abstract method 'test'" - if sys.version_info >= (3, 12) - else "Can't instantiate abstract class FakeTestWithMissingTest with abstract method test" - ) - assert msg in caplog.messages diff --git a/tests/units/test_settings.py b/tests/units/test_settings.py new file mode 100644 index 0000000..43d3ddc --- /dev/null +++ b/tests/units/test_settings.py @@ -0,0 +1,126 @@ +# 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. +"""Unit tests for the anta.settings module.""" + +from __future__ import annotations + +import logging +import os +import sys +from unittest.mock import patch + +import pytest +from pydantic import ValidationError + +from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings + +if os.name == "posix": + # The function is not defined on non-POSIX system + import resource + + +class TestAntaRunnerSettings: + """Tests for the AntaRunnerSettings class.""" + + def test_defaults(self, setenvvar: pytest.MonkeyPatch) -> None: + """Test defaults for ANTA runner settings.""" + settings = AntaRunnerSettings() + assert settings.nofile == DEFAULT_NOFILE + assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY + + def test_env_var(self, setenvvar: pytest.MonkeyPatch) -> None: + """Test setting different ANTA runner settings.""" + setenvvar.setenv("ANTA_NOFILE", "20480") + settings = AntaRunnerSettings() + assert settings.nofile == 20480 + assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY + + def test_validation(self, setenvvar: pytest.MonkeyPatch) -> None: + """Test validation of ANTA runner settings.""" + setenvvar.setenv("ANTA_NOFILE", "-1") + with pytest.raises(ValidationError): + AntaRunnerSettings() + + setenvvar.setenv("ANTA_MAX_CONCURRENCY", "0") + with pytest.raises(ValidationError): + AntaRunnerSettings() + + @pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only") + def test_file_descriptor_limit_windows(self, caplog: pytest.LogCaptureFixture) -> None: + """Test file_descriptor_limit on Windows.""" + caplog.set_level(logging.INFO) + settings = AntaRunnerSettings() + assert settings.file_descriptor_limit == sys.maxsize + assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.text + + @pytest.mark.skipif(os.name != "posix", reason="Fake non-posix for coverage") + async def test_file_descriptor_limit_fake_windows(self, caplog: pytest.LogCaptureFixture) -> None: + """Test file_descriptor_limit on fake Windows.""" + caplog.set_level(logging.INFO) + with patch("os.name", new="win32"): + settings = AntaRunnerSettings() + assert settings.file_descriptor_limit == sys.maxsize + assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[0].message + + @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows") + def test_file_descriptor_limit_posix(self, caplog: pytest.LogCaptureFixture) -> None: + """Test file_descriptor_limit on POSIX systems.""" + with ( + caplog.at_level(logging.DEBUG), + patch.dict("os.environ", {"ANTA_NOFILE": "20480"}), + patch("resource.getrlimit") as getrlimit_mock, + patch("resource.setrlimit") as setrlimit_mock, + ): + # Simulate the default system limits + system_limits = (8192, 1048576) + + # Setup getrlimit mock return value + getrlimit_mock.return_value = system_limits + + # Simulate setrlimit behavior + def side_effect_setrlimit(_resource_id: int, limits: tuple[int, int]) -> None: + getrlimit_mock.return_value = (limits[0], limits[1]) + + setrlimit_mock.side_effect = side_effect_setrlimit + + settings = AntaRunnerSettings() + + # Assert the limits were updated as expected + assert settings.file_descriptor_limit == 20480 + assert "Initial file descriptor limits for the current ANTA process: Soft Limit: 8192 | Hard Limit: 1048576" in caplog.text + assert "Setting file descriptor soft limit to 20480" in caplog.text + + setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (20480, 1048576)) # pylint: disable=possibly-used-before-assignment + + @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows") + def test_file_descriptor_limit_value_error(self, caplog: pytest.LogCaptureFixture) -> None: + """Test file_descriptor_limit with invalid environment variables.""" + with ( + caplog.at_level(logging.DEBUG), + patch.dict("os.environ", {"ANTA_NOFILE": "666"}), + patch("resource.getrlimit") as getrlimit_mock, + patch("resource.setrlimit") as setrlimit_mock, + ): + # Simulate the default system limits + system_limits = (32768, 131072) + + # Setup getrlimit mock return value + getrlimit_mock.return_value = system_limits + + # Simulate setrlimit behavior raising ValueError + def side_effect_setrlimit(_resource_id: int, _limits: tuple[int, int]) -> None: + msg = "not allowed to raise maximum limit" + raise ValueError(msg) + + setrlimit_mock.side_effect = side_effect_setrlimit + + settings = AntaRunnerSettings() + + # Assert the limits were *NOT* updated as expected + assert settings.file_descriptor_limit == 32768 + assert "Initial file descriptor limits for the current ANTA process: Soft Limit: 32768 | Hard Limit: 131072" in caplog.text + assert caplog.records[-1].levelname == "WARNING" + assert "Failed to set file descriptor soft limit for the current ANTA process" in caplog.records[-1].getMessage() + + setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (666, 131072))