From 0b75d57c154bff202970bb8c2f4caea525372b87 Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Thu, 10 Aug 2023 15:50:48 +0200 Subject: [PATCH 01/36] ci: Update release workflow to link jobs (#359) --- .github/workflows/on-demand.yml | 48 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 41 +++++++++++++++------------- 2 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/on-demand.yml diff --git a/.github/workflows/on-demand.yml b/.github/workflows/on-demand.yml new file mode 100644 index 000000000..f3872090b --- /dev/null +++ b/.github/workflows/on-demand.yml @@ -0,0 +1,48 @@ +on: + workflow_run: + inputs: + tag: + description: 'docker container tag' + required: true + type: string + default: 'dev' + +jobs: + docker: + name: Docker Image Build + runs-on: ubuntu-latest + strategy: + matrix: + platform: + - linux/amd64 + - linux/arm64 + - linux/arm/v7 + - linux/arm/v8 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker meta for TAG + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=${{ inputs.tag }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ab4bf447..6bbc1a8dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,9 +6,29 @@ on: - published jobs: + pypi: + name: Publish version to Pypi servers + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel build + - name: Build package + run: | + python -m build + - name: Publish package to Pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + release-coverage: name: Updated ANTA release coverage badge runs-on: ubuntu-20.04 + needs: [pypi] steps: - uses: actions/checkout@v3 - name: Setup Python @@ -29,6 +49,7 @@ jobs: release-doc: name: "Publish documentation for release ${{github.ref_name}}" runs-on: ubuntu-latest + needs: [release-coverage] steps: - uses: actions/checkout@v3 with: @@ -46,25 +67,6 @@ jobs: run: | pip install .[doc] mike deploy --update-alias --push ${{github.ref_name}} stable - pypi: - name: Publish version to Pypi servers - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel build - - name: Build package - run: | - python -m build - - name: Publish package to Pypi - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} docker: name: Docker Image Build @@ -76,6 +78,7 @@ jobs: - linux/amd64 - linux/arm64 - linux/arm/v7 + - linux/arm/v8 steps: - name: Checkout uses: actions/checkout@v3 From ca8c9449147de47b95869558306b7f92118642e5 Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Thu, 10 Aug 2023 15:57:00 +0200 Subject: [PATCH 02/36] ci: Update on-demand workflow (#360) --- .github/workflows/on-demand.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/on-demand.yml b/.github/workflows/on-demand.yml index f3872090b..db9b2f186 100644 --- a/.github/workflows/on-demand.yml +++ b/.github/workflows/on-demand.yml @@ -1,5 +1,6 @@ +name: 'Build docker on-demand' on: - workflow_run: + workflow_dispatch: inputs: tag: description: 'docker container tag' From 473c079ab1a29b641dd206a0d80119daa8fd6624 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:41:29 +0200 Subject: [PATCH 03/36] chore: bump bumpver from 2023.1125 to 2023.1126 (#361) Bumps [bumpver](https://github.com/mbarkhau/bumpver) from 2023.1125 to 2023.1126. - [Changelog](https://github.com/mbarkhau/bumpver/blob/master/CHANGELOG.md) - [Commits](https://github.com/mbarkhau/bumpver/compare/2023.1125...2023.1126) --- updated-dependencies: - dependency-name: bumpver dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 466d648e7..ba76ebe5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ requires-python = ">=3.8" [project.optional-dependencies] dev = [ - "bumpver==2023.1125", + "bumpver==2023.1126", "black==23.7.0", "flake8==6.1.0", "isort==5.12.0", From 7712312ef4f09be0df843202d531526b16b1e13b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:45:46 +0200 Subject: [PATCH 04/36] ci: bump GrantBirki/comment from 2.0.5 to 2.0.6 (#362) Bumps [GrantBirki/comment](https://github.com/grantbirki/comment) from 2.0.5 to 2.0.6. - [Release notes](https://github.com/grantbirki/comment/releases) - [Commits](https://github.com/grantbirki/comment/compare/v2.0.5...v2.0.6) --- updated-dependencies: - dependency-name: GrantBirki/comment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/code-testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-testing.yml b/.github/workflows/code-testing.yml index 965d8e51d..82537edd1 100644 --- a/.github/workflows/code-testing.yml +++ b/.github/workflows/code-testing.yml @@ -64,7 +64,7 @@ jobs: if: needs.file-changes.outputs.cli == 'true' && needs.file-changes.outputs.docs == 'false' steps: - name: Documentation is missing - uses: GrantBirki/comment@v2.0.5 + uses: GrantBirki/comment@v2.0.6 with: body: | Please consider that documentation is missing under `docs/` folder. From 5c8476fa6beacf3fa9d044c59cc5676591863895 Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Tue, 15 Aug 2023 10:38:54 +0200 Subject: [PATCH 05/36] doc: Add demo link to README (#364) --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index f80232f77..b191d4a6d 100755 --- a/docs/README.md +++ b/docs/README.md @@ -76,7 +76,7 @@ Commands: ## Documentation -The documentation is published on [ANTA package website](https://www.anta.ninja) +The documentation is published on [ANTA package website](https://www.anta.ninja). Also, a [demo repository](https://github.com/titom73/atd-anta-demo) is available to facilitate your journey with ANTA. ## Contribution guide From 874124bfc2a6a1abdf3601d847dab6066f3ef924 Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Wed, 16 Aug 2023 08:50:49 +0200 Subject: [PATCH 06/36] doc: Add badge for repo activity (#365) * doc: Add badge for repo activity * Update README.md --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index b191d4a6d..8c0378c47 100755 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,9 @@ [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/arista-netdevops-community/anta/blob/main/LICENSE) [![Linting and Testing Anta](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml/badge.svg)](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/arista-netdevops-community/anta) [![github release](https://img.shields.io/github/release/arista-netdevops-community/anta.svg)](https://github.com/arista-netdevops-community/anta/releases/) -![PyPI - Downloads/month](https://img.shields.io/pypi/dm/eos-downloader) +![PyPI - Downloads](https://img.shields.io/pypi/dm/anta) ![coverage](https://raw.githubusercontent.com/arista-netdevops-community/anta/coverage-badge/latest-release-coverage.svg) # Arista Network Test Automation (ANTA) Framework From 084d42e26d9adc425e53f280d3862bf574bed809 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:27:15 +0200 Subject: [PATCH 07/36] chore: bump tox from 4.7.0 to 4.9.0 (#366) Bumps [tox](https://github.com/tox-dev/tox) from 4.7.0 to 4.9.0. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.7.0...4.9.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba76ebe5c..486864e92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dev = [ "pytest-html>=3.2.0", "pytest-metadata>=3.0.0", "pylint-pydantic>=0.2.4", - "tox==4.7.0", + "tox==4.9.0", "types-PyYAML", "types-paramiko", "types-requests", From 0065d6ad9e3cbed39f0bf86491191943e7ddf599 Mon Sep 17 00:00:00 2001 From: Carl Buchmann Date: Fri, 18 Aug 2023 09:56:23 -0400 Subject: [PATCH 08/36] doc: Insert Arista license header in all source files (#367) * Doc: Insert license header in all source files * + add docs to files scope * move to license-short to .github * remove insert-license for j2 files --- .github/license-short.txt | 3 ++ .pre-commit-config.yaml | 30 ++++++++++++++++++- anta/__init__.py | 3 ++ anta/cli/__init__.py | 3 ++ anta/cli/console.py | 3 ++ anta/cli/debug/__init__.py | 3 ++ anta/cli/debug/commands.py | 3 ++ anta/cli/exec/__init__.py | 3 ++ anta/cli/exec/commands.py | 3 ++ anta/cli/exec/utils.py | 3 ++ anta/cli/get/__init__.py | 3 ++ anta/cli/get/commands.py | 3 ++ anta/cli/get/utils.py | 3 ++ anta/cli/nrfu/__init__.py | 3 ++ anta/cli/nrfu/commands.py | 3 ++ anta/cli/nrfu/utils.py | 3 ++ anta/cli/utils.py | 3 ++ anta/decorators.py | 3 ++ anta/device.py | 3 ++ anta/inventory/__init__.py | 3 ++ anta/inventory/exceptions.py | 3 ++ anta/inventory/models.py | 3 ++ anta/loader.py | 3 ++ anta/models.py | 3 ++ anta/reporter/__init__.py | 3 ++ anta/reporter/models.py | 3 ++ anta/result_manager/__init__.py | 3 ++ anta/result_manager/models.py | 3 ++ anta/runner.py | 3 ++ anta/tests/__init__.py | 3 ++ anta/tests/aaa.py | 3 ++ anta/tests/configuration.py | 3 ++ anta/tests/connectivity.py | 3 ++ anta/tests/field_notices.py | 3 ++ anta/tests/hardware.py | 3 ++ anta/tests/interfaces.py | 3 ++ anta/tests/logging.py | 3 ++ anta/tests/mlag.py | 3 ++ anta/tests/multicast.py | 3 ++ anta/tests/profiles.py | 3 ++ anta/tests/routing/__init__.py | 3 ++ anta/tests/routing/bgp.py | 3 ++ anta/tests/routing/generic.py | 3 ++ anta/tests/routing/ospf.py | 3 ++ anta/tests/security.py | 3 ++ anta/tests/snmp.py | 3 ++ anta/tests/software.py | 3 ++ anta/tests/stp.py | 3 ++ anta/tests/system.py | 3 ++ anta/tests/vxlan.py | 3 ++ anta/tools/__init__.py | 3 ++ anta/tools/get_value.py | 3 ++ anta/tools/misc.py | 3 ++ anta/tools/pydantic.py | 3 ++ docs/README.md | 6 ++++ docs/advanced_usages/as-python-lib.md | 10 +++++-- docs/advanced_usages/custom-tests.md | 6 ++++ docs/api/device.md | 6 ++++ docs/api/inventory.md | 6 ++++ docs/api/inventory.models.input.md | 6 ++++ docs/api/models.md | 6 ++++ docs/api/report_manager.md | 6 ++++ docs/api/report_manager_models.md | 6 ++++ docs/api/result_manager.md | 6 ++++ docs/api/result_manager_models.md | 6 ++++ docs/api/tests.aaa.md | 6 ++++ docs/api/tests.configuration.md | 6 ++++ docs/api/tests.connectivity.md | 6 ++++ docs/api/tests.field_notices.md | 6 ++++ docs/api/tests.hardware.md | 6 ++++ docs/api/tests.interfaces.md | 6 ++++ docs/api/tests.logging.md | 6 ++++ docs/api/tests.md | 8 ++++- docs/api/tests.mlag.md | 6 ++++ docs/api/tests.multicast.md | 6 ++++ docs/api/tests.profiles.md | 6 ++++ docs/api/tests.routing.bgp.md | 6 ++++ docs/api/tests.routing.generic.md | 6 ++++ docs/api/tests.routing.ospf.md | 6 ++++ docs/api/tests.security.md | 6 ++++ docs/api/tests.snmp.md | 6 ++++ docs/api/tests.software.md | 6 ++++ docs/api/tests.stp.md | 6 ++++ docs/api/tests.system.md | 7 ++++- docs/api/tests.vxlan.md | 6 ++++ docs/cli/debug.md | 8 ++++- docs/cli/exec.md | 6 ++++ docs/cli/get-inventory-information.md | 14 ++++++--- docs/cli/inv-from-ansible.md | 6 ++++ docs/cli/inv-from-cvp.md | 6 ++++ docs/cli/nrfu.md | 6 ++++ docs/cli/overview.md | 6 ++++ docs/contribution.md | 6 ++++ docs/faq.md | 8 ++++- docs/getting-started.md | 8 ++++- docs/imgs/animated-svg.md | 8 ++++- docs/imgs/anta-nrfu.cast | 1 - docs/imgs/anta-nrfu.svg | 2 +- docs/requirements-and-installation.md | 6 ++++ docs/snippets/anta_help.txt | 2 +- docs/usage-inventory-catalog.md | 6 ++++ tests/README.md | 6 ++++ tests/__init__.py | 3 ++ tests/conftest.py | 3 ++ tests/data/__init__.py | 3 ++ tests/data/json_data.py | 3 ++ tests/lib/__init__.py | 3 ++ tests/lib/fixture.py | 3 ++ tests/lib/parametrize.py | 3 ++ tests/lib/utils.py | 3 ++ tests/units/__init__.py | 3 ++ tests/units/anta_tests/__init__.py | 3 ++ tests/units/anta_tests/aaa/__init__.py | 3 ++ tests/units/anta_tests/aaa/data.py | 3 ++ tests/units/anta_tests/aaa/test_exc.py | 3 ++ .../anta_tests/configuration/__init__.py | 3 ++ tests/units/anta_tests/configuration/data.py | 3 ++ .../anta_tests/configuration/test_exc.py | 3 ++ .../units/anta_tests/connectivity/__init__.py | 3 ++ tests/units/anta_tests/connectivity/data.py | 3 ++ .../units/anta_tests/connectivity/test_exc.py | 3 ++ .../anta_tests/field_notices/__init__.py | 3 ++ tests/units/anta_tests/field_notices/data.py | 3 ++ .../anta_tests/field_notices/test_exc.py | 3 ++ tests/units/anta_tests/hardware/__init__.py | 3 ++ tests/units/anta_tests/hardware/data.py | 3 ++ tests/units/anta_tests/hardware/test_exc.py | 3 ++ tests/units/anta_tests/interfaces/__init__.py | 3 ++ tests/units/anta_tests/interfaces/data.py | 3 ++ tests/units/anta_tests/interfaces/test_exc.py | 3 ++ tests/units/anta_tests/logging/__init__.py | 3 ++ tests/units/anta_tests/logging/data.py | 3 ++ tests/units/anta_tests/logging/test_exc.py | 3 ++ tests/units/anta_tests/mlag/__init__.py | 3 ++ tests/units/anta_tests/mlag/data.py | 3 ++ tests/units/anta_tests/mlag/test_exc.py | 3 ++ tests/units/anta_tests/multicast/__init__.py | 3 ++ tests/units/anta_tests/multicast/data.py | 3 ++ tests/units/anta_tests/multicast/test_exc.py | 3 ++ tests/units/anta_tests/profiles/__init__.py | 3 ++ tests/units/anta_tests/profiles/data.py | 3 ++ tests/units/anta_tests/profiles/test_exc.py | 3 ++ tests/units/anta_tests/routing/__init__.py | 3 ++ .../units/anta_tests/routing/bgp/__init__.py | 3 ++ tests/units/anta_tests/routing/bgp/data.py | 3 ++ .../units/anta_tests/routing/bgp/test_exc.py | 3 ++ .../anta_tests/routing/generic/__init__.py | 3 ++ .../units/anta_tests/routing/generic/data.py | 3 ++ .../anta_tests/routing/generic/test_exc.py | 3 ++ .../units/anta_tests/routing/ospf/__init__.py | 3 ++ tests/units/anta_tests/routing/ospf/data.py | 3 ++ .../units/anta_tests/routing/ospf/test_exc.py | 3 ++ tests/units/anta_tests/security/__init__.py | 3 ++ tests/units/anta_tests/security/data.py | 3 ++ tests/units/anta_tests/security/test_exc.py | 3 ++ tests/units/anta_tests/snmp/__init__.py | 3 ++ tests/units/anta_tests/snmp/data.py | 3 ++ tests/units/anta_tests/snmp/test_exc.py | 3 ++ tests/units/anta_tests/software/__init__.py | 3 ++ tests/units/anta_tests/software/data.py | 3 ++ tests/units/anta_tests/software/test_exc.py | 3 ++ tests/units/anta_tests/stp/__init__.py | 3 ++ tests/units/anta_tests/stp/data.py | 3 ++ tests/units/anta_tests/stp/test_exc.py | 3 ++ tests/units/anta_tests/system/__init__.py | 3 ++ tests/units/anta_tests/system/data.py | 3 ++ tests/units/anta_tests/system/test_exc.py | 3 ++ tests/units/anta_tests/vxlan/__init__.py | 3 ++ tests/units/anta_tests/vxlan/data.py | 3 ++ tests/units/anta_tests/vxlan/test_exc.py | 3 ++ tests/units/cli/__init__.py | 3 ++ tests/units/cli/debug/__init__.py | 3 ++ tests/units/cli/debug/test_commands.py | 3 ++ tests/units/cli/exec/__init__.py | 3 ++ tests/units/cli/exec/test_commands.py | 3 ++ tests/units/cli/exec/test_utils.py | 3 ++ tests/units/cli/get/__init__.py | 3 ++ tests/units/cli/get/test_commands.py | 3 ++ tests/units/cli/test__init__.py | 3 ++ tests/units/inventory/__init__.py | 3 ++ tests/units/inventory/test_inventory.py | 3 ++ tests/units/inventory/test_models.py | 3 ++ tests/units/reporter/__init__.py | 3 ++ tests/units/reporter/test__init__.py | 3 ++ tests/units/result_manager/__init__.py | 3 ++ tests/units/result_manager/test__init__.py | 3 ++ tests/units/result_manager/test_models.py | 3 ++ tests/units/test_device.py | 3 ++ tests/units/tools/__init__.py | 3 ++ tests/units/tools/test_get_value.py | 3 ++ tests/units/tools/test_misc.py | 3 ++ tests/units/tools/test_pydantic.py | 3 ++ 192 files changed, 741 insertions(+), 16 deletions(-) create mode 100644 .github/license-short.txt diff --git a/.github/license-short.txt b/.github/license-short.txt new file mode 100644 index 000000000..787e7ab9d --- /dev/null +++ b/.github/license-short.txt @@ -0,0 +1,3 @@ +Copyright (c) 2023 Arista Networks, Inc. +Use of this source code is governed by the Apache License 2.0 +that can be found in the LICENSE file. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dd21db7c..414937f62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -files: ^(anta|scripts|tests)/ +files: ^(anta|docs|scripts|tests)/ repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -12,6 +12,34 @@ repos: - id: check-added-large-files - id: check-merge-conflict + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.4 + hooks: + - name: Check and insert license on Python files + id: insert-license + # exclude: + files: .*\.py$ + args: + - --license-filepath + - .github/license-short.txt + - --use-current-year + - --allow-past-years + - --fuzzy-match-generates-todo + - --no-extra-eol + + - name: Check and insert license on Markdown files + id: insert-license + files: .*\.md$ + # exclude: + args: + - --license-filepath + - .github/license-short.txt + - --use-current-year + - --allow-past-years + - --fuzzy-match-generates-todo + - --comment-style + - '' + - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: diff --git a/anta/__init__.py b/anta/__init__.py index bfbceb054..85d311cf9 100644 --- a/anta/__init__.py +++ b/anta/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Arista Network Test Automation (ANTA) Framework.""" import importlib.metadata import os diff --git a/anta/cli/__init__.py b/anta/cli/__init__.py index 5affe6a19..20b2cd353 100644 --- a/anta/cli/__init__.py +++ b/anta/cli/__init__.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ ANTA CLI diff --git a/anta/cli/console.py b/anta/cli/console.py index e7544487e..90285f1d4 100644 --- a/anta/cli/console.py +++ b/anta/cli/console.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ ANTA Top-level Console diff --git a/anta/cli/debug/__init__.py b/anta/cli/debug/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/cli/debug/__init__.py +++ b/anta/cli/debug/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/cli/debug/commands.py b/anta/cli/debug/commands.py index eb13697d0..63f6d0195 100644 --- a/anta/cli/debug/commands.py +++ b/anta/cli/debug/commands.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- # pylint: disable = redefined-outer-name diff --git a/anta/cli/exec/__init__.py b/anta/cli/exec/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/cli/exec/__init__.py +++ b/anta/cli/exec/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/cli/exec/commands.py b/anta/cli/exec/commands.py index bb9f4588b..d2a089c40 100644 --- a/anta/cli/exec/commands.py +++ b/anta/cli/exec/commands.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ diff --git a/anta/cli/exec/utils.py b/anta/cli/exec/utils.py index 83d6f73f3..3c9363966 100644 --- a/anta/cli/exec/utils.py +++ b/anta/cli/exec/utils.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ diff --git a/anta/cli/get/__init__.py b/anta/cli/get/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/cli/get/__init__.py +++ b/anta/cli/get/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/cli/get/commands.py b/anta/cli/get/commands.py index 81578c210..7541e8026 100644 --- a/anta/cli/get/commands.py +++ b/anta/cli/get/commands.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- # pylint: disable = redefined-outer-name diff --git a/anta/cli/get/utils.py b/anta/cli/get/utils.py index 51fbf160f..fe2767003 100644 --- a/anta/cli/get/utils.py +++ b/anta/cli/get/utils.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ diff --git a/anta/cli/nrfu/__init__.py b/anta/cli/nrfu/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/cli/nrfu/__init__.py +++ b/anta/cli/nrfu/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/cli/nrfu/commands.py b/anta/cli/nrfu/commands.py index 9a7f01de8..073c8af85 100644 --- a/anta/cli/nrfu/commands.py +++ b/anta/cli/nrfu/commands.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ Commands for Anta CLI to run nrfu commands. diff --git a/anta/cli/nrfu/utils.py b/anta/cli/nrfu/utils.py index 43568e347..b860404f7 100644 --- a/anta/cli/nrfu/utils.py +++ b/anta/cli/nrfu/utils.py @@ -1,4 +1,7 @@ #!/usr/bin/env python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ diff --git a/anta/cli/utils.py b/anta/cli/utils.py index 4bfecad5c..399ab46e7 100644 --- a/anta/cli/utils.py +++ b/anta/cli/utils.py @@ -1,4 +1,7 @@ #!/usr/bin/python +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # coding: utf-8 -*- """ Utils functions to use with anta.cli module. diff --git a/anta/decorators.py b/anta/decorators.py index e0651d68d..6fadd35e9 100644 --- a/anta/decorators.py +++ b/anta/decorators.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """decorators for tests.""" from __future__ import annotations diff --git a/anta/device.py b/anta/device.py index 0cf38084e..93e69548f 100644 --- a/anta/device.py +++ b/anta/device.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 Device Abstraction Module """ diff --git a/anta/inventory/__init__.py b/anta/inventory/__init__.py index 8294f2fc0..e8918e491 100644 --- a/anta/inventory/__init__.py +++ b/anta/inventory/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Inventory Module for ANTA. """ diff --git a/anta/inventory/exceptions.py b/anta/inventory/exceptions.py index e901b39fc..469d6a676 100644 --- a/anta/inventory/exceptions.py +++ b/anta/inventory/exceptions.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Manage Exception in Inventory module.""" diff --git a/anta/inventory/models.py b/anta/inventory/models.py index a25fe67f1..6d8afe94f 100644 --- a/anta/inventory/models.py +++ b/anta/inventory/models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Models related to inventory management.""" from __future__ import annotations diff --git a/anta/loader.py b/anta/loader.py index 9c60f85e6..69791ab34 100644 --- a/anta/loader.py +++ b/anta/loader.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Loader that parses a YAML test catalog and imports corresponding Python functions """ diff --git a/anta/models.py b/anta/models.py index 8d9913b5b..c328bf2f0 100644 --- a/anta/models.py +++ b/anta/models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Models to define a TestStructure """ diff --git a/anta/reporter/__init__.py b/anta/reporter/__init__.py index 3ba9f4eb7..5d10ea547 100644 --- a/anta/reporter/__init__.py +++ b/anta/reporter/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Report management for ANTA. """ diff --git a/anta/reporter/models.py b/anta/reporter/models.py index 1a6db1063..200c2d49c 100644 --- a/anta/reporter/models.py +++ b/anta/reporter/models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Models related to anta.result_manager module.""" from pydantic import BaseModel, validator diff --git a/anta/result_manager/__init__.py b/anta/result_manager/__init__.py index 99c805f9f..03c9a3a66 100644 --- a/anta/result_manager/__init__.py +++ b/anta/result_manager/__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Result Manager Module for ANTA. """ diff --git a/anta/result_manager/models.py b/anta/result_manager/models.py index b0c5e9172..46b9aecb8 100644 --- a/anta/result_manager/models.py +++ b/anta/result_manager/models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Models related to anta.result_manager module.""" from typing import Iterator, List, Optional diff --git a/anta/runner.py b/anta/runner.py index 88020e44c..ad892b017 100644 --- a/anta/runner.py +++ b/anta/runner.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 function """ diff --git a/anta/tests/__init__.py b/anta/tests/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/tests/__init__.py +++ b/anta/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/tests/aaa.py b/anta/tests/aaa.py index 9e142df19..fba4fd083 100644 --- a/anta/tests/aaa.py +++ b/anta/tests/aaa.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the EOS various AAA settings """ diff --git a/anta/tests/configuration.py b/anta/tests/configuration.py index 53cf96d0f..2ee5fb0ad 100644 --- a/anta/tests/configuration.py +++ b/anta/tests/configuration.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the device configuration """ diff --git a/anta/tests/connectivity.py b/anta/tests/connectivity.py index a0b40e689..7f9f54098 100644 --- a/anta/tests/connectivity.py +++ b/anta/tests/connectivity.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to various connectivity checks """ diff --git a/anta/tests/field_notices.py b/anta/tests/field_notices.py index a483bd2b5..06ce1821e 100644 --- a/anta/tests/field_notices.py +++ b/anta/tests/field_notices.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions to flag field notices """ diff --git a/anta/tests/hardware.py b/anta/tests/hardware.py index 54b122e6b..23a47f44b 100644 --- a/anta/tests/hardware.py +++ b/anta/tests/hardware.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the hardware or environment """ diff --git a/anta/tests/interfaces.py b/anta/tests/interfaces.py index 2eb2cfcd0..09f65dc0f 100644 --- a/anta/tests/interfaces.py +++ b/anta/tests/interfaces.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the device interfaces """ diff --git a/anta/tests/logging.py b/anta/tests/logging.py index fd23d3ee7..2b5f74e7c 100644 --- a/anta/tests/logging.py +++ b/anta/tests/logging.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the EOS various logging settings diff --git a/anta/tests/mlag.py b/anta/tests/mlag.py index f0fc8ec19..444aa1e38 100644 --- a/anta/tests/mlag.py +++ b/anta/tests/mlag.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to Multi-chassis Link Aggregation (MLAG) """ diff --git a/anta/tests/multicast.py b/anta/tests/multicast.py index 14e8b68e8..967538e0c 100644 --- a/anta/tests/multicast.py +++ b/anta/tests/multicast.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to multicast """ diff --git a/anta/tests/profiles.py b/anta/tests/profiles.py index 7b346df30..5ec0ea409 100644 --- a/anta/tests/profiles.py +++ b/anta/tests/profiles.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to ASIC profiles """ diff --git a/anta/tests/routing/__init__.py b/anta/tests/routing/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/tests/routing/__init__.py +++ b/anta/tests/routing/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index 99b00eafe..1527c1318 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ BGP test functions """ diff --git a/anta/tests/routing/generic.py b/anta/tests/routing/generic.py index a4242ff97..34bcaab2a 100644 --- a/anta/tests/routing/generic.py +++ b/anta/tests/routing/generic.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Generic routing test functions """ diff --git a/anta/tests/routing/ospf.py b/anta/tests/routing/ospf.py index 0819e463c..c69206b37 100644 --- a/anta/tests/routing/ospf.py +++ b/anta/tests/routing/ospf.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ OSPF test functions """ diff --git a/anta/tests/security.py b/anta/tests/security.py index 5b718ca88..b45cd63ad 100644 --- a/anta/tests/security.py +++ b/anta/tests/security.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the EOS various security settings """ diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 0d20965a6..2d25feb34 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the EOS various SNMP settings """ diff --git a/anta/tests/software.py b/anta/tests/software.py index 06b18ea57..6923485ea 100644 --- a/anta/tests/software.py +++ b/anta/tests/software.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to the EOS software """ diff --git a/anta/tests/stp.py b/anta/tests/stp.py index 37cf0448d..64b68538d 100644 --- a/anta/tests/stp.py +++ b/anta/tests/stp.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to various Spanning Tree Protocol (STP) settings """ diff --git a/anta/tests/system.py b/anta/tests/system.py index 5283d002f..930c49cde 100644 --- a/anta/tests/system.py +++ b/anta/tests/system.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to system-level features and protocols """ diff --git a/anta/tests/vxlan.py b/anta/tests/vxlan.py index 72003a66a..8841ea53d 100644 --- a/anta/tests/vxlan.py +++ b/anta/tests/vxlan.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 functions related to VXLAN """ diff --git a/anta/tools/__init__.py b/anta/tools/__init__.py index e69de29bb..c460d5493 100644 --- a/anta/tools/__init__.py +++ b/anta/tools/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/anta/tools/get_value.py b/anta/tools/get_value.py index a28f10eed..c4e63729f 100644 --- a/anta/tools/get_value.py +++ b/anta/tools/get_value.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Get a value from a dictionary or nested dictionaries. """ diff --git a/anta/tools/misc.py b/anta/tools/misc.py index 6324debb6..a47ef3f00 100644 --- a/anta/tools/misc.py +++ b/anta/tools/misc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Toolkit for ANTA. """ diff --git a/anta/tools/pydantic.py b/anta/tools/pydantic.py index 0474da9ac..94ba4cb0a 100644 --- a/anta/tools/pydantic.py +++ b/anta/tools/pydantic.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ Toolkit for ANTA to play with Pydantic. """ diff --git a/docs/README.md b/docs/README.md index 8c0378c47..629585948 100755 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,9 @@ + + [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/arista-netdevops-community/anta/blob/main/LICENSE) [![Linting and Testing Anta](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml/badge.svg)](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/docs/advanced_usages/as-python-lib.md b/docs/advanced_usages/as-python-lib.md index 2555f2a33..14e810762 100644 --- a/docs/advanced_usages/as-python-lib.md +++ b/docs/advanced_usages/as-python-lib.md @@ -1,3 +1,9 @@ + + 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 @@ -270,8 +276,8 @@ cmd2 = AntaCommand(command="show running-config diffs", ofmt="text") * A __revision takes precedence over a version__ (e.g. if a command is run with version="latest" and revision=1, the first revision of the model is returned) * By default eAPI returns the first revision of each model to ensure that when upgrading, intergation with existing tools is not broken. This is done by using by default `version=1` in eAPI calls. - ANTA uses by default `version="latest"` in AntaCommand. For some commands, you may want to run them with a different revision or version. - + ANTA uses by default `version="latest"` in AntaCommand. For some commands, you may want to run them with a different revision or version. + For instance the `VerifyRoutingTableSize` test leverages the first revision of `show bfd peers`: ``` diff --git a/docs/advanced_usages/custom-tests.md b/docs/advanced_usages/custom-tests.md index 77e69f3c3..8cd5b3b58 100644 --- a/docs/advanced_usages/custom-tests.md +++ b/docs/advanced_usages/custom-tests.md @@ -1,3 +1,9 @@ + + # Create your own custom tests !!! info "" diff --git a/docs/api/device.md b/docs/api/device.md index 44411e95a..b27e29af6 100644 --- a/docs/api/device.md +++ b/docs/api/device.md @@ -1,3 +1,9 @@ + + # AntaDevice base class ## UML representation diff --git a/docs/api/inventory.md b/docs/api/inventory.md index b388b7501..41649dde5 100644 --- a/docs/api/inventory.md +++ b/docs/api/inventory.md @@ -1,3 +1,9 @@ + + ### ::: anta.inventory.AntaInventory options: filters: ["!^_[^_]", "!__str__"] diff --git a/docs/api/inventory.models.input.md b/docs/api/inventory.models.input.md index 2d215e623..f965a78f6 100644 --- a/docs/api/inventory.models.input.md +++ b/docs/api/inventory.models.input.md @@ -1,3 +1,9 @@ + + ### ::: anta.inventory.models.AntaInventoryInput ### ::: anta.inventory.models.AntaInventoryHost diff --git a/docs/api/models.md b/docs/api/models.md index be7dae6d7..0c82d32bc 100644 --- a/docs/api/models.md +++ b/docs/api/models.md @@ -1,3 +1,9 @@ + + # Anta Test definition ## UML Diagram diff --git a/docs/api/report_manager.md b/docs/api/report_manager.md index 9709bd682..a494bd704 100644 --- a/docs/api/report_manager.md +++ b/docs/api/report_manager.md @@ -1 +1,7 @@ + + ### ::: anta.reporter.ReportTable diff --git a/docs/api/report_manager_models.md b/docs/api/report_manager_models.md index 7821ced7b..704faab7f 100644 --- a/docs/api/report_manager_models.md +++ b/docs/api/report_manager_models.md @@ -1 +1,7 @@ + + ### ::: anta.reporter.models.ColorManager diff --git a/docs/api/result_manager.md b/docs/api/result_manager.md index d0ad92e40..50e9c3b3b 100644 --- a/docs/api/result_manager.md +++ b/docs/api/result_manager.md @@ -1,3 +1,9 @@ + + # Result Manager definition ## UML Diagram diff --git a/docs/api/result_manager_models.md b/docs/api/result_manager_models.md index 4924192c2..26c9dc236 100644 --- a/docs/api/result_manager_models.md +++ b/docs/api/result_manager_models.md @@ -1,3 +1,9 @@ + + # Test Result model ## UML Diagram diff --git a/docs/api/tests.aaa.md b/docs/api/tests.aaa.md index 4110ca3ed..7fa19d727 100644 --- a/docs/api/tests.aaa.md +++ b/docs/api/tests.aaa.md @@ -1,3 +1,9 @@ + + # ANTA catalog for interfaces tests ::: anta.tests.aaa diff --git a/docs/api/tests.configuration.md b/docs/api/tests.configuration.md index 8f5c9c0ad..0071a796d 100644 --- a/docs/api/tests.configuration.md +++ b/docs/api/tests.configuration.md @@ -1,3 +1,9 @@ + + # ANTA catalog for configuration tests ::: anta.tests.configuration diff --git a/docs/api/tests.connectivity.md b/docs/api/tests.connectivity.md index bcaa876a4..741bbcd0c 100644 --- a/docs/api/tests.connectivity.md +++ b/docs/api/tests.connectivity.md @@ -1,3 +1,9 @@ + + # ANTA catalog for connectivity tests ::: anta.tests.connectivity diff --git a/docs/api/tests.field_notices.md b/docs/api/tests.field_notices.md index a1845e529..c33aa9f0e 100644 --- a/docs/api/tests.field_notices.md +++ b/docs/api/tests.field_notices.md @@ -1,3 +1,9 @@ + + # ANTA catalog for Field Notices tests ::: anta.tests.field_notices diff --git a/docs/api/tests.hardware.md b/docs/api/tests.hardware.md index 7ceafba5e..b5671c89b 100644 --- a/docs/api/tests.hardware.md +++ b/docs/api/tests.hardware.md @@ -1,3 +1,9 @@ + + # ANTA catalog for hardware tests ::: anta.tests.hardware diff --git a/docs/api/tests.interfaces.md b/docs/api/tests.interfaces.md index a68b53ef4..9dd977094 100644 --- a/docs/api/tests.interfaces.md +++ b/docs/api/tests.interfaces.md @@ -1,3 +1,9 @@ + + # ANTA catalog for interfaces tests ::: anta.tests.interfaces diff --git a/docs/api/tests.logging.md b/docs/api/tests.logging.md index 51a523675..bf7e285e3 100644 --- a/docs/api/tests.logging.md +++ b/docs/api/tests.logging.md @@ -1,3 +1,9 @@ + + # ANTA catalog for logging tests ::: anta.tests.logging diff --git a/docs/api/tests.md b/docs/api/tests.md index 2af8e421d..68c949fc4 100644 --- a/docs/api/tests.md +++ b/docs/api/tests.md @@ -1,3 +1,9 @@ + + # ANTA Tests landing page This section describes all the available tests provided by ANTA package. @@ -25,4 +31,4 @@ This section describes all the available tests provided by ANTA package. -All these tests can be imported in a [catalog](../usage-inventory-catalog.md) to be used by [the anta cli](../cli/nrfu.md) or in your [own framework](../advanced_usages/as-python-lib.md) \ No newline at end of file +All these tests can be imported in a [catalog](../usage-inventory-catalog.md) to be used by [the anta cli](../cli/nrfu.md) or in your [own framework](../advanced_usages/as-python-lib.md) diff --git a/docs/api/tests.mlag.md b/docs/api/tests.mlag.md index 66629e7e3..2ee78a5a5 100644 --- a/docs/api/tests.mlag.md +++ b/docs/api/tests.mlag.md @@ -1,3 +1,9 @@ + + # ANTA catalog for mlag tests ::: anta.tests.mlag diff --git a/docs/api/tests.multicast.md b/docs/api/tests.multicast.md index 0ac67c85c..9163e5edd 100644 --- a/docs/api/tests.multicast.md +++ b/docs/api/tests.multicast.md @@ -1,3 +1,9 @@ + + # ANTA catalog for multicast tests ::: anta.tests.multicast diff --git a/docs/api/tests.profiles.md b/docs/api/tests.profiles.md index 7f5288711..86e55b8f3 100644 --- a/docs/api/tests.profiles.md +++ b/docs/api/tests.profiles.md @@ -1,3 +1,9 @@ + + # ANTA catalog for profiles tests ::: anta.tests.profiles diff --git a/docs/api/tests.routing.bgp.md b/docs/api/tests.routing.bgp.md index 14cd56589..c369fa124 100644 --- a/docs/api/tests.routing.bgp.md +++ b/docs/api/tests.routing.bgp.md @@ -1,3 +1,9 @@ + + # ANTA catalog for routing-bgp tests ::: anta.tests.routing.bgp diff --git a/docs/api/tests.routing.generic.md b/docs/api/tests.routing.generic.md index 186be0699..084c779d9 100644 --- a/docs/api/tests.routing.generic.md +++ b/docs/api/tests.routing.generic.md @@ -1,3 +1,9 @@ + + # ANTA catalog for routing-generic tests ::: anta.tests.routing.generic diff --git a/docs/api/tests.routing.ospf.md b/docs/api/tests.routing.ospf.md index 037afb98a..e67a54e59 100644 --- a/docs/api/tests.routing.ospf.md +++ b/docs/api/tests.routing.ospf.md @@ -1,3 +1,9 @@ + + # ANTA catalog for routing-ospf tests ::: anta.tests.routing.ospf diff --git a/docs/api/tests.security.md b/docs/api/tests.security.md index 35aad8dbb..b1cccf294 100644 --- a/docs/api/tests.security.md +++ b/docs/api/tests.security.md @@ -1,3 +1,9 @@ + + # ANTA catalog for security tests ::: anta.tests.security diff --git a/docs/api/tests.snmp.md b/docs/api/tests.snmp.md index c653b26e9..a5ca49dfa 100644 --- a/docs/api/tests.snmp.md +++ b/docs/api/tests.snmp.md @@ -1,3 +1,9 @@ + + # ANTA catalog for SNMP tests ::: anta.tests.snmp diff --git a/docs/api/tests.software.md b/docs/api/tests.software.md index 6874369d9..f1eabad29 100644 --- a/docs/api/tests.software.md +++ b/docs/api/tests.software.md @@ -1,3 +1,9 @@ + + # ANTA catalog for software tests ::: anta.tests.software diff --git a/docs/api/tests.stp.md b/docs/api/tests.stp.md index 4aad48b0e..eea5068da 100644 --- a/docs/api/tests.stp.md +++ b/docs/api/tests.stp.md @@ -1,3 +1,9 @@ + + # ANTA catalog for STP tests ::: anta.tests.stp diff --git a/docs/api/tests.system.md b/docs/api/tests.system.md index c6715ea79..3ef0de0d7 100644 --- a/docs/api/tests.system.md +++ b/docs/api/tests.system.md @@ -1,3 +1,9 @@ + + # ANTA catalog for system tests ::: anta.tests.system @@ -5,4 +11,3 @@ show_root_heading: false show_root_toc_entry: false merge_init_into_class: false - diff --git a/docs/api/tests.vxlan.md b/docs/api/tests.vxlan.md index 806796dc6..adb7a7593 100644 --- a/docs/api/tests.vxlan.md +++ b/docs/api/tests.vxlan.md @@ -1,3 +1,9 @@ + + # ANTA catalog for VXLAN tests ::: anta.tests.vxlan diff --git a/docs/cli/debug.md b/docs/cli/debug.md index aade7c83d..3213cc82b 100644 --- a/docs/cli/debug.md +++ b/docs/cli/debug.md @@ -1,3 +1,9 @@ + + # ANTA debug commands The ANTA CLI includes a set of debugging tools, making it easier to build and test ANTA content. This functionality is accessed via the `debug` subcommand and offers the following options: @@ -109,7 +115,7 @@ Run templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A anta --log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 --device DC1-SPINE1     > {'dst': '8.8.8.8', 'src': 'Loopback0'} -anta --log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 dst "1.1.1.1" src Loopback1 --device DC1-SPINE1           +anta --log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 dst "1.1.1.1" src Loopback1 --device DC1-SPINE1           > {'dst': '1.1.1.1', 'src': 'Loopback1'} # Notice how `src` and `dst` keep only the latest value ``` diff --git a/docs/cli/exec.md b/docs/cli/exec.md index a1fea5db9..be94b4bc5 100644 --- a/docs/cli/exec.md +++ b/docs/cli/exec.md @@ -1,3 +1,9 @@ + + # Executing Commands on Devices ANTA CLI provides a set of entrypoints to facilitate remote command execution on EOS devices. diff --git a/docs/cli/get-inventory-information.md b/docs/cli/get-inventory-information.md index acd79f9b1..0b4459b9e 100644 --- a/docs/cli/get-inventory-information.md +++ b/docs/cli/get-inventory-information.md @@ -1,3 +1,9 @@ + + # Retrieving Inventory Information The ANTA CLI offers multiple entrypoints to access data from your local inventory. @@ -13,15 +19,15 @@ anta_inventory: - host: 172.20.20.101 name: DC1-SPINE1 tags: ["SPINE", "DC1"] - + - host: 172.20.20.102 name: DC1-SPINE2 tags: ["SPINE", "DC1"] - + - host: 172.20.20.111 name: DC1-LEAF1A tags: ["LEAF", "DC1"] - + - host: 172.20.20.112 name: DC1-LEAF1B tags: ["LEAF", "DC1"] @@ -53,7 +59,7 @@ anta_inventory: - host: 172.20.20.221 name: DC2-BL1 tags: ["BL", "DC2"] - + - host: 172.20.20.222 name: DC2-BL2 tags: ["BL", "DC2"] diff --git a/docs/cli/inv-from-ansible.md b/docs/cli/inv-from-ansible.md index 8384f249c..baa01e7e2 100644 --- a/docs/cli/inv-from-ansible.md +++ b/docs/cli/inv-from-ansible.md @@ -1,3 +1,9 @@ + + # Create an Inventory from Ansible inventory In large setups, it might be beneficial to construct your inventory based on your Ansible inventory. The `from-ansible` entrypoint of the `get` command enables the user to create an ANTA inventory from Ansible. diff --git a/docs/cli/inv-from-cvp.md b/docs/cli/inv-from-cvp.md index 2ca0895a3..a09678590 100644 --- a/docs/cli/inv-from-cvp.md +++ b/docs/cli/inv-from-cvp.md @@ -1,3 +1,9 @@ + + # Create an Inventory from CloudVision In large setups, it might be beneficial to construct your inventory based on CloudVision. The `from-cvp` entrypoint of the `get` command enables the user to create an ANTA inventory from CloudVision. diff --git a/docs/cli/nrfu.md b/docs/cli/nrfu.md index 9ea29cb96..32a2b2d08 100644 --- a/docs/cli/nrfu.md +++ b/docs/cli/nrfu.md @@ -1,3 +1,9 @@ + + # Execute Network Readiness For Use (NRFU) Testing ANTA provides a set of commands for performing NRFU tests on devices. These commands are under the `anta nrfu` namespace and offer multiple output format options: diff --git a/docs/cli/overview.md b/docs/cli/overview.md index 6f2684c83..89cc24d4e 100644 --- a/docs/cli/overview.md +++ b/docs/cli/overview.md @@ -1,3 +1,9 @@ + + # Overview of ANTA's Command-Line Interface (CLI) ANTA provides a powerful Command-Line Interface (CLI) to perform a wide range of operations. This document provides a comprehensive overview of ANTA CLI usage and its commands. diff --git a/docs/contribution.md b/docs/contribution.md index 2ed3fc25b..3eef82117 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -1,3 +1,9 @@ + + # How to contribute to ANTA Contribution model is based on a fork-model. Don't push to arista-netdevops-community/anta directly. Always do a branch in your forked repository and create a PR. diff --git a/docs/faq.md b/docs/faq.md index c50935fd1..55aaed372 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,3 +1,9 @@ + + # Frequently Asked Questions (FAQ) ## Why am I seeing an `ImportError` related to `urllib3` when running ANTA? @@ -29,4 +35,4 @@ This error arises due to a compatibility issue between `urllib3` v2.0 and older --- ## Still facing issues? -If you've tried the above solutions and continue to experience problems, please report the issue in our [GitHub repository](https://github.com/arista-netdevops-community/anta). \ No newline at end of file +If you've tried the above solutions and continue to experience problems, please report the issue in our [GitHub repository](https://github.com/arista-netdevops-community/anta). diff --git a/docs/getting-started.md b/docs/getting-started.md index 0683e9ae3..efe31e9d4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,3 +1,9 @@ + + # Getting Started This section shows how to use ANTA with basic configuration. All examples are based on Arista Test Drive (ATD) topology you can access by reaching out to your prefered SE. @@ -168,7 +174,7 @@ anta \ [10:17:24] INFO Running ANTA tests... runner.py:75 • Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:02 • 0:00:00 - All tests results + All tests results ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Device IP ┃ Test Name ┃ Test Status ┃ Message(s) ┃ Test description ┃ Test category ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ diff --git a/docs/imgs/animated-svg.md b/docs/imgs/animated-svg.md index b17759afe..31c592d6f 100644 --- a/docs/imgs/animated-svg.md +++ b/docs/imgs/animated-svg.md @@ -1,2 +1,8 @@ + + Repository: https://github.com/marionebl/svg-term-cli -Command: `cat anta-nrfu.cast | svg-term --height 10 --window --out anta.svg` \ No newline at end of file +Command: `cat anta-nrfu.cast | svg-term --height 10 --window --out anta.svg` diff --git a/docs/imgs/anta-nrfu.cast b/docs/imgs/anta-nrfu.cast index 96a334cfb..dcad1ec8b 100644 --- a/docs/imgs/anta-nrfu.cast +++ b/docs/imgs/anta-nrfu.cast @@ -62,4 +62,3 @@ [8.096303, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m100%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m533/534\u001b[0m • \u001b[33m0:00:04\u001b[0m • \u001b[36m0:00:01\u001b[0m"] [8.146442, "o", "\r\u001b[2K • Running NRFU Tests...\u001b[35m100%\u001b[0m \u001b[38;5;70m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m534/534\u001b[0m • \u001b[33m0:00:04\u001b[0m • \u001b[36m0:00:00\u001b[0m\r\n\u001b[?25h"] [8.22014, "o", "\u001b[?2004h❯ "] - diff --git a/docs/imgs/anta-nrfu.svg b/docs/imgs/anta-nrfu.svg index 4d631494c..c01eabbaa 100644 --- a/docs/imgs/anta-nrfu.svg +++ b/docs/imgs/anta-nrfu.svg @@ -1 +1 @@ -antaantanrfuantanrfutable╭──────────────────────Settings──────────────────────╮RunningANTAtests:-ANTAInventorycontains6devices(AsyncEOSDevice)-Testscatalogcontains89tests╰──────────────────────────────────────────────────────╯(🐜)RunningNRFUTests...0%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━0/5340:00:00-:--:--[11:29:47]INFORunningANTAtests...runner.py:71(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01RunningNRFUTests...100%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━534/5340:00:040:00:00aanantantanantanrantanrfantanrfutantanrfutaantanrfutabantanrfutabl(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐌)RunningNRFUTests...18%━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━98/5340:00:000:00:05(🐌)RunningNRFUTests...35%━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━189/5340:00:010:00:02(🐌)RunningNRFUTests...55%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━293/5340:00:010:00:01(🐌)RunningNRFUTests...58%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━311/5340:00:010:00:01(🐌)RunningNRFUTests...63%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━335/5340:00:010:00:01(🐌)RunningNRFUTests...67%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━360/5340:00:010:00:01(🐌)RunningNRFUTests...71%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━378/5340:00:010:00:01(🐌)RunningNRFUTests...72%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━383/5340:00:010:00:01(🐌)RunningNRFUTests...72%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━385/5340:00:010:00:01(🐜)RunningNRFUTests...76%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━407/5340:00:010:00:01(🐜)RunningNRFUTests...81%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━433/5340:00:010:00:01(🐜)RunningNRFUTests...84%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━447/5340:00:020:00:01(🐜)RunningNRFUTests...85%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━456/5340:00:020:00:01(🐜)RunningNRFUTests...87%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━467/5340:00:020:00:01(🐜)RunningNRFUTests...89%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━476/5340:00:020:00:01(🐜)RunningNRFUTests...91%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━486/5340:00:020:00:01(🐜)RunningNRFUTests...95%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━505/5340:00:020:00:01(🐜)RunningNRFUTests...98%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸521/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━528/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...100%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸533/5340:00:040:00:01 \ No newline at end of file +antaantanrfuantanrfutable╭──────────────────────Settings──────────────────────╮RunningANTAtests:-ANTAInventorycontains6devices(AsyncEOSDevice)-Testscatalogcontains89tests╰──────────────────────────────────────────────────────╯(🐜)RunningNRFUTests...0%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━0/5340:00:00-:--:--[11:29:47]INFORunningANTAtests...runner.py:71(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01RunningNRFUTests...100%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━534/5340:00:040:00:00aanantantanantanrantanrfantanrfutantanrfutaantanrfutabantanrfutabl(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐜)RunningNRFUTests...15%━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━78/5340:00:000:00:01(🐌)RunningNRFUTests...18%━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━98/5340:00:000:00:05(🐌)RunningNRFUTests...35%━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━189/5340:00:010:00:02(🐌)RunningNRFUTests...55%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━293/5340:00:010:00:01(🐌)RunningNRFUTests...58%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━311/5340:00:010:00:01(🐌)RunningNRFUTests...63%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━335/5340:00:010:00:01(🐌)RunningNRFUTests...67%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━360/5340:00:010:00:01(🐌)RunningNRFUTests...71%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━378/5340:00:010:00:01(🐌)RunningNRFUTests...72%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━383/5340:00:010:00:01(🐌)RunningNRFUTests...72%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━385/5340:00:010:00:01(🐜)RunningNRFUTests...76%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━407/5340:00:010:00:01(🐜)RunningNRFUTests...81%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━433/5340:00:010:00:01(🐜)RunningNRFUTests...84%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━447/5340:00:020:00:01(🐜)RunningNRFUTests...85%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━456/5340:00:020:00:01(🐜)RunningNRFUTests...87%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━467/5340:00:020:00:01(🐜)RunningNRFUTests...89%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━476/5340:00:020:00:01(🐜)RunningNRFUTests...91%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━486/5340:00:020:00:01(🐜)RunningNRFUTests...95%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━505/5340:00:020:00:01(🐜)RunningNRFUTests...98%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸521/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━528/5340:00:020:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐌)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...99%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸531/5340:00:030:00:01(🐜)RunningNRFUTests...100%━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸533/5340:00:040:00:01 diff --git a/docs/requirements-and-installation.md b/docs/requirements-and-installation.md index 8afb03203..13709c519 100644 --- a/docs/requirements-and-installation.md +++ b/docs/requirements-and-installation.md @@ -1,3 +1,9 @@ + + # ANTA Requirements ## Python version diff --git a/docs/snippets/anta_help.txt b/docs/snippets/anta_help.txt index 2c2566007..67747975f 100644 --- a/docs/snippets/anta_help.txt +++ b/docs/snippets/anta_help.txt @@ -41,4 +41,4 @@ Commands: debug Debug commands for building ANTA exec Execute commands to inventory devices get Get data from/to ANTA - nrfu Run NRFU against inventory devices \ No newline at end of file + nrfu Run NRFU against inventory devices diff --git a/docs/usage-inventory-catalog.md b/docs/usage-inventory-catalog.md index 6be4a04db..e2c3c7297 100644 --- a/docs/usage-inventory-catalog.md +++ b/docs/usage-inventory-catalog.md @@ -1,3 +1,9 @@ + + # Inventory and Catalog definition This page describes how to create an inventory and a tests catalog. diff --git a/tests/README.md b/tests/README.md index f121b4b90..bdf222125 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,3 +1,9 @@ + + The python script [antatests_test.py](units/antatests_test.py) is used to test the functions defined in the directory [ANTA](../anta) without using actual EOS devices. It requires the installation of the package `pytest` that is indicated in the file [requirements-dev.txt](../requirements-dev.txt) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/conftest.py b/tests/conftest.py index 7d63e7f41..b57595f61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ conftest.py - used to store anta specific fixtures used for tests """ diff --git a/tests/data/__init__.py b/tests/data/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/data/__init__.py +++ b/tests/data/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/data/json_data.py b/tests/data/json_data.py index 51a09eb3d..95b6410f3 100644 --- a/tests/data/json_data.py +++ b/tests/data/json_data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. # pylint: skip-file INVENTORY_MODEL_HOST_VALID = [ diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/lib/fixture.py b/tests/lib/fixture.py index e3b742299..6bb6ca197 100644 --- a/tests/lib/fixture.py +++ b/tests/lib/fixture.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Fixture for Anta Testing""" from typing import Callable diff --git a/tests/lib/parametrize.py b/tests/lib/parametrize.py index 49ae1c9e2..964f68122 100644 --- a/tests/lib/parametrize.py +++ b/tests/lib/parametrize.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """ parametrize.py - Retrieves the mock data from the json_data file """ diff --git a/tests/lib/utils.py b/tests/lib/utils.py index 57798f796..299318f79 100644 --- a/tests/lib/utils.py +++ b/tests/lib/utils.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.lib.utils """ diff --git a/tests/units/__init__.py b/tests/units/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/__init__.py +++ b/tests/units/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/__init__.py b/tests/units/anta_tests/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/__init__.py +++ b/tests/units/anta_tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/aaa/__init__.py b/tests/units/anta_tests/aaa/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/aaa/__init__.py +++ b/tests/units/anta_tests/aaa/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/aaa/data.py b/tests/units/anta_tests/aaa/data.py index bcaa404eb..28ea64601 100644 --- a/tests/units/anta_tests/aaa/data.py +++ b/tests/units/anta_tests/aaa/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.aaa""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/aaa/test_exc.py b/tests/units/anta_tests/aaa/test_exc.py index 8e19eb067..5bc4c006b 100644 --- a/tests/units/anta_tests/aaa/test_exc.py +++ b/tests/units/anta_tests/aaa/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.aaa.py """ diff --git a/tests/units/anta_tests/configuration/__init__.py b/tests/units/anta_tests/configuration/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/configuration/__init__.py +++ b/tests/units/anta_tests/configuration/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/configuration/data.py b/tests/units/anta_tests/configuration/data.py index dc1b3c5e1..b12461ad9 100644 --- a/tests/units/anta_tests/configuration/data.py +++ b/tests/units/anta_tests/configuration/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.configuration""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/configuration/test_exc.py b/tests/units/anta_tests/configuration/test_exc.py index f48060a55..b49565c16 100644 --- a/tests/units/anta_tests/configuration/test_exc.py +++ b/tests/units/anta_tests/configuration/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.hardware.py """ diff --git a/tests/units/anta_tests/connectivity/__init__.py b/tests/units/anta_tests/connectivity/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/connectivity/__init__.py +++ b/tests/units/anta_tests/connectivity/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/connectivity/data.py b/tests/units/anta_tests/connectivity/data.py index 89adb62f9..f0cc1d339 100644 --- a/tests/units/anta_tests/connectivity/data.py +++ b/tests/units/anta_tests/connectivity/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.connectivity""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/connectivity/test_exc.py b/tests/units/anta_tests/connectivity/test_exc.py index 298a80efe..0a9abb80e 100644 --- a/tests/units/anta_tests/connectivity/test_exc.py +++ b/tests/units/anta_tests/connectivity/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.connectivity.py """ diff --git a/tests/units/anta_tests/field_notices/__init__.py b/tests/units/anta_tests/field_notices/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/field_notices/__init__.py +++ b/tests/units/anta_tests/field_notices/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/field_notices/data.py b/tests/units/anta_tests/field_notices/data.py index 4d7eb0da8..151802800 100644 --- a/tests/units/anta_tests/field_notices/data.py +++ b/tests/units/anta_tests/field_notices/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.field_notices""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/field_notices/test_exc.py b/tests/units/anta_tests/field_notices/test_exc.py index 992e6d8bd..5c90abb76 100644 --- a/tests/units/anta_tests/field_notices/test_exc.py +++ b/tests/units/anta_tests/field_notices/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.field_notices.py """ diff --git a/tests/units/anta_tests/hardware/__init__.py b/tests/units/anta_tests/hardware/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/hardware/__init__.py +++ b/tests/units/anta_tests/hardware/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/hardware/data.py b/tests/units/anta_tests/hardware/data.py index ef965e3e6..fcac13287 100644 --- a/tests/units/anta_tests/hardware/data.py +++ b/tests/units/anta_tests/hardware/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.hardware""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/hardware/test_exc.py b/tests/units/anta_tests/hardware/test_exc.py index b00b7b924..fd8889cce 100644 --- a/tests/units/anta_tests/hardware/test_exc.py +++ b/tests/units/anta_tests/hardware/test_exc.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +# Copyright (c) 2023 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.hardware.py """ diff --git a/tests/units/anta_tests/interfaces/__init__.py b/tests/units/anta_tests/interfaces/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/interfaces/__init__.py +++ b/tests/units/anta_tests/interfaces/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/interfaces/data.py b/tests/units/anta_tests/interfaces/data.py index ce937f6ef..653156356 100644 --- a/tests/units/anta_tests/interfaces/data.py +++ b/tests/units/anta_tests/interfaces/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.hardware""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/interfaces/test_exc.py b/tests/units/anta_tests/interfaces/test_exc.py index 689eaf0ed..29857bab6 100644 --- a/tests/units/anta_tests/interfaces/test_exc.py +++ b/tests/units/anta_tests/interfaces/test_exc.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +# Copyright (c) 2023 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.hardware.py """ diff --git a/tests/units/anta_tests/logging/__init__.py b/tests/units/anta_tests/logging/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/logging/__init__.py +++ b/tests/units/anta_tests/logging/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/logging/data.py b/tests/units/anta_tests/logging/data.py index 58df2e7ad..16bbc73cd 100644 --- a/tests/units/anta_tests/logging/data.py +++ b/tests/units/anta_tests/logging/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.logging""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/logging/test_exc.py b/tests/units/anta_tests/logging/test_exc.py index 607e8ebb2..395176311 100644 --- a/tests/units/anta_tests/logging/test_exc.py +++ b/tests/units/anta_tests/logging/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.security.py """ diff --git a/tests/units/anta_tests/mlag/__init__.py b/tests/units/anta_tests/mlag/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/mlag/__init__.py +++ b/tests/units/anta_tests/mlag/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/mlag/data.py b/tests/units/anta_tests/mlag/data.py index a607f2a1d..9035890eb 100644 --- a/tests/units/anta_tests/mlag/data.py +++ b/tests/units/anta_tests/mlag/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.mlag""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/mlag/test_exc.py b/tests/units/anta_tests/mlag/test_exc.py index 33445c0c3..a0d8be30f 100644 --- a/tests/units/anta_tests/mlag/test_exc.py +++ b/tests/units/anta_tests/mlag/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.mlag.py """ diff --git a/tests/units/anta_tests/multicast/__init__.py b/tests/units/anta_tests/multicast/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/multicast/__init__.py +++ b/tests/units/anta_tests/multicast/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/multicast/data.py b/tests/units/anta_tests/multicast/data.py index 307bfd097..aec791c49 100644 --- a/tests/units/anta_tests/multicast/data.py +++ b/tests/units/anta_tests/multicast/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.multicast""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/multicast/test_exc.py b/tests/units/anta_tests/multicast/test_exc.py index 11f7c0f49..110651a34 100644 --- a/tests/units/anta_tests/multicast/test_exc.py +++ b/tests/units/anta_tests/multicast/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.multicast.py """ diff --git a/tests/units/anta_tests/profiles/__init__.py b/tests/units/anta_tests/profiles/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/profiles/__init__.py +++ b/tests/units/anta_tests/profiles/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/profiles/data.py b/tests/units/anta_tests/profiles/data.py index fcedc2a5b..fb5047209 100644 --- a/tests/units/anta_tests/profiles/data.py +++ b/tests/units/anta_tests/profiles/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.profiles""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/profiles/test_exc.py b/tests/units/anta_tests/profiles/test_exc.py index 4ad35ceb6..849f0ffc1 100644 --- a/tests/units/anta_tests/profiles/test_exc.py +++ b/tests/units/anta_tests/profiles/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.profiles.py """ diff --git a/tests/units/anta_tests/routing/__init__.py b/tests/units/anta_tests/routing/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/routing/__init__.py +++ b/tests/units/anta_tests/routing/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/routing/bgp/__init__.py b/tests/units/anta_tests/routing/bgp/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/routing/bgp/__init__.py +++ b/tests/units/anta_tests/routing/bgp/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/routing/bgp/data.py b/tests/units/anta_tests/routing/bgp/data.py index 30ba2eea7..eeb59dc46 100644 --- a/tests/units/anta_tests/routing/bgp/data.py +++ b/tests/units/anta_tests/routing/bgp/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.routing.bgp""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/routing/bgp/test_exc.py b/tests/units/anta_tests/routing/bgp/test_exc.py index 291c18e46..ccfa94c6c 100644 --- a/tests/units/anta_tests/routing/bgp/test_exc.py +++ b/tests/units/anta_tests/routing/bgp/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.routing.bgp.py """ diff --git a/tests/units/anta_tests/routing/generic/__init__.py b/tests/units/anta_tests/routing/generic/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/routing/generic/__init__.py +++ b/tests/units/anta_tests/routing/generic/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/routing/generic/data.py b/tests/units/anta_tests/routing/generic/data.py index d1ccb2b5b..f78ea3c9d 100644 --- a/tests/units/anta_tests/routing/generic/data.py +++ b/tests/units/anta_tests/routing/generic/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.routing.generic""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/routing/generic/test_exc.py b/tests/units/anta_tests/routing/generic/test_exc.py index 74a871639..ee0011bf5 100644 --- a/tests/units/anta_tests/routing/generic/test_exc.py +++ b/tests/units/anta_tests/routing/generic/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.routing.generic.py """ diff --git a/tests/units/anta_tests/routing/ospf/__init__.py b/tests/units/anta_tests/routing/ospf/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/routing/ospf/__init__.py +++ b/tests/units/anta_tests/routing/ospf/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/routing/ospf/data.py b/tests/units/anta_tests/routing/ospf/data.py index 6d0590254..d98372c02 100644 --- a/tests/units/anta_tests/routing/ospf/data.py +++ b/tests/units/anta_tests/routing/ospf/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.routing.ospf""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/routing/ospf/test_exc.py b/tests/units/anta_tests/routing/ospf/test_exc.py index 7372ae49e..d7174722b 100644 --- a/tests/units/anta_tests/routing/ospf/test_exc.py +++ b/tests/units/anta_tests/routing/ospf/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.routing.ospf.py """ diff --git a/tests/units/anta_tests/security/__init__.py b/tests/units/anta_tests/security/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/security/__init__.py +++ b/tests/units/anta_tests/security/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/security/data.py b/tests/units/anta_tests/security/data.py index cd855632d..94c3a34fc 100644 --- a/tests/units/anta_tests/security/data.py +++ b/tests/units/anta_tests/security/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.security""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/security/test_exc.py b/tests/units/anta_tests/security/test_exc.py index b59c3c9de..f9366d46f 100644 --- a/tests/units/anta_tests/security/test_exc.py +++ b/tests/units/anta_tests/security/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.security.py """ diff --git a/tests/units/anta_tests/snmp/__init__.py b/tests/units/anta_tests/snmp/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/snmp/__init__.py +++ b/tests/units/anta_tests/snmp/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/snmp/data.py b/tests/units/anta_tests/snmp/data.py index d83f6a414..e299c6d25 100644 --- a/tests/units/anta_tests/snmp/data.py +++ b/tests/units/anta_tests/snmp/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.snmp""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/snmp/test_exc.py b/tests/units/anta_tests/snmp/test_exc.py index 386a7149b..4ffa23a0b 100644 --- a/tests/units/anta_tests/snmp/test_exc.py +++ b/tests/units/anta_tests/snmp/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.snmp.py """ diff --git a/tests/units/anta_tests/software/__init__.py b/tests/units/anta_tests/software/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/software/__init__.py +++ b/tests/units/anta_tests/software/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/software/data.py b/tests/units/anta_tests/software/data.py index 177db4b7a..8daa3e007 100644 --- a/tests/units/anta_tests/software/data.py +++ b/tests/units/anta_tests/software/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.hardware""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/software/test_exc.py b/tests/units/anta_tests/software/test_exc.py index bbb6e9da9..ed96cf5d6 100644 --- a/tests/units/anta_tests/software/test_exc.py +++ b/tests/units/anta_tests/software/test_exc.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +# Copyright (c) 2023 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.software.py """ diff --git a/tests/units/anta_tests/stp/__init__.py b/tests/units/anta_tests/stp/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/stp/__init__.py +++ b/tests/units/anta_tests/stp/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/stp/data.py b/tests/units/anta_tests/stp/data.py index bb53251ab..51153be47 100644 --- a/tests/units/anta_tests/stp/data.py +++ b/tests/units/anta_tests/stp/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. """Data for testing anta.tests.aaa""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/stp/test_exc.py b/tests/units/anta_tests/stp/test_exc.py index 5891c5e68..9bfb3e89f 100644 --- a/tests/units/anta_tests/stp/test_exc.py +++ b/tests/units/anta_tests/stp/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.stp.py """ diff --git a/tests/units/anta_tests/system/__init__.py b/tests/units/anta_tests/system/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/system/__init__.py +++ b/tests/units/anta_tests/system/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/system/data.py b/tests/units/anta_tests/system/data.py index a2f372ecc..258e08d73 100644 --- a/tests/units/anta_tests/system/data.py +++ b/tests/units/anta_tests/system/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.system""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/system/test_exc.py b/tests/units/anta_tests/system/test_exc.py index d37d81cea..318482166 100644 --- a/tests/units/anta_tests/system/test_exc.py +++ b/tests/units/anta_tests/system/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.system.py """ diff --git a/tests/units/anta_tests/vxlan/__init__.py b/tests/units/anta_tests/vxlan/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/anta_tests/vxlan/__init__.py +++ b/tests/units/anta_tests/vxlan/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/anta_tests/vxlan/data.py b/tests/units/anta_tests/vxlan/data.py index 2d0e67f0a..6f078ea47 100644 --- a/tests/units/anta_tests/vxlan/data.py +++ b/tests/units/anta_tests/vxlan/data.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 inputs for anta.tests.vxlan""" from typing import Any, Dict, List diff --git a/tests/units/anta_tests/vxlan/test_exc.py b/tests/units/anta_tests/vxlan/test_exc.py index fa36fb73a..e3c4dcfb2 100644 --- a/tests/units/anta_tests/vxlan/test_exc.py +++ b/tests/units/anta_tests/vxlan/test_exc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.vxlan.py """ diff --git a/tests/units/cli/__init__.py b/tests/units/cli/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/cli/__init__.py +++ b/tests/units/cli/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/cli/debug/__init__.py b/tests/units/cli/debug/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/cli/debug/__init__.py +++ b/tests/units/cli/debug/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/cli/debug/test_commands.py b/tests/units/cli/debug/test_commands.py index 78169aa1c..e69501b93 100644 --- a/tests/units/cli/debug/test_commands.py +++ b/tests/units/cli/debug/test_commands.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.cli.debug.commands """ diff --git a/tests/units/cli/exec/__init__.py b/tests/units/cli/exec/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/cli/exec/__init__.py +++ b/tests/units/cli/exec/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/cli/exec/test_commands.py b/tests/units/cli/exec/test_commands.py index dc0a82064..19fe3ac47 100644 --- a/tests/units/cli/exec/test_commands.py +++ b/tests/units/cli/exec/test_commands.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.cli.exec.commands """ diff --git a/tests/units/cli/exec/test_utils.py b/tests/units/cli/exec/test_utils.py index 178a84aae..6b050a9ed 100644 --- a/tests/units/cli/exec/test_utils.py +++ b/tests/units/cli/exec/test_utils.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.cli.exec.utils """ diff --git a/tests/units/cli/get/__init__.py b/tests/units/cli/get/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/cli/get/__init__.py +++ b/tests/units/cli/get/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/cli/get/test_commands.py b/tests/units/cli/get/test_commands.py index 7c9bfc009..78bab50d5 100644 --- a/tests/units/cli/get/test_commands.py +++ b/tests/units/cli/get/test_commands.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.cli.get.commands """ diff --git a/tests/units/cli/test__init__.py b/tests/units/cli/test__init__.py index e4ff884d2..1cbd46204 100644 --- a/tests/units/cli/test__init__.py +++ b/tests/units/cli/test__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.cli.__init__ """ diff --git a/tests/units/inventory/__init__.py b/tests/units/inventory/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/inventory/__init__.py +++ b/tests/units/inventory/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/inventory/test_inventory.py b/tests/units/inventory/test_inventory.py index 0d2ca768a..f909991e6 100644 --- a/tests/units/inventory/test_inventory.py +++ b/tests/units/inventory/test_inventory.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 Inventory unit tests.""" import logging diff --git a/tests/units/inventory/test_models.py b/tests/units/inventory/test_models.py index 21994aae0..906a5b68c 100644 --- a/tests/units/inventory/test_models.py +++ b/tests/units/inventory/test_models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 Inventory models unit tests.""" import logging diff --git a/tests/units/reporter/__init__.py b/tests/units/reporter/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/reporter/__init__.py +++ b/tests/units/reporter/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/reporter/test__init__.py b/tests/units/reporter/test__init__.py index 3d07f806b..f17310413 100644 --- a/tests/units/reporter/test__init__.py +++ b/tests/units/reporter/test__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.report.__init__.py """ diff --git a/tests/units/result_manager/__init__.py b/tests/units/result_manager/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/result_manager/__init__.py +++ b/tests/units/result_manager/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/result_manager/test__init__.py b/tests/units/result_manager/test__init__.py index acc846926..7483f7cb3 100644 --- a/tests/units/result_manager/test__init__.py +++ b/tests/units/result_manager/test__init__.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.result_manager.__init__.py """ diff --git a/tests/units/result_manager/test_models.py b/tests/units/result_manager/test_models.py index 16d3b6d3d..a041a9931 100644 --- a/tests/units/result_manager/test_models.py +++ b/tests/units/result_manager/test_models.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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 Result Manager models unit tests.""" import logging diff --git a/tests/units/test_device.py b/tests/units/test_device.py index adefa8db2..733dd43fa 100644 --- a/tests/units/test_device.py +++ b/tests/units/test_device.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.device.py """ diff --git a/tests/units/tools/__init__.py b/tests/units/tools/__init__.py index e69de29bb..c460d5493 100644 --- a/tests/units/tools/__init__.py +++ b/tests/units/tools/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2023 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. diff --git a/tests/units/tools/test_get_value.py b/tests/units/tools/test_get_value.py index e5f147408..c0257a56f 100644 --- a/tests/units/tools/test_get_value.py +++ b/tests/units/tools/test_get_value.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.tools.get_value """ diff --git a/tests/units/tools/test_misc.py b/tests/units/tools/test_misc.py index f9c0fffa6..d3d2d1f3a 100644 --- a/tests/units/tools/test_misc.py +++ b/tests/units/tools/test_misc.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.tools.misc """ diff --git a/tests/units/tools/test_pydantic.py b/tests/units/tools/test_pydantic.py index 49f5dcc6d..7bff85355 100644 --- a/tests/units/tools/test_pydantic.py +++ b/tests/units/tools/test_pydantic.py @@ -1,3 +1,6 @@ +# Copyright (c) 2023 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.tools.pydantic """ From b1ec3edd2c46f48cddfd2debdb99168a779f8947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:44:43 +0200 Subject: [PATCH 09/36] chore: bump tox from 4.9.0 to 4.10.0 (#369) Bumps [tox](https://github.com/tox-dev/tox) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.9.0...4.10.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 486864e92..3c7c93b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dev = [ "pytest-html>=3.2.0", "pytest-metadata>=3.0.0", "pylint-pydantic>=0.2.4", - "tox==4.9.0", + "tox==4.10.0", "types-PyYAML", "types-paramiko", "types-requests", From 7389ff18608feb3c61509ccf11081a6b969583f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:47:25 +0200 Subject: [PATCH 10/36] chore: update pydantic requirement from ~=2.1.1 to >=2.1.1,<2.4.0 (#370) Updates the requirements on [pydantic](https://github.com/pydantic/pydantic) to permit the latest version. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.1.1...v2.3.0) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c7c93b63..1df78e9e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "click~=8.1.6", "click-help-colors~=0.9", "cvprac~=1.3.1", - "pydantic~=2.1.1", + "pydantic>=2.1.1,<2.4.0", "PyYAML~=6.0", "requests~=2.31.0", "rich~=13.5.2", From eb73110cd8e57301dd573f71976dc3dd4d5fdbdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:12:28 +0200 Subject: [PATCH 11/36] chore: bump tox from 4.10.0 to >=4.10.0,<5.0.0 (#372) * chore: bump tox from 4.10.0 to 4.11.0 Bumps [tox](https://github.com/tox-dev/tox) from 4.10.0 to 4.11.0. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.10.0...4.11.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * deps: allow a range of version for tox --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Grimonet --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1df78e9e5..1b8e899bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dev = [ "pytest-html>=3.2.0", "pytest-metadata>=3.0.0", "pylint-pydantic>=0.2.4", - "tox==4.10.0", + "tox>=4.10.0,<5.0.0", "types-PyYAML", "types-paramiko", "types-requests", From c8fd490ba257f32c078e5ed46dbe9407d09e4082 Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Wed, 30 Aug 2023 16:51:57 +0200 Subject: [PATCH 12/36] chore: replace duplicate maintainer in pyproject (#373) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b8e899bf..4d68136f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [{ name = "Khelil Sator", email = "ksator@arista.com" }] maintainers = [ { name = "Khelil Sator", email = "ksator@arista.com" }, { name = "Matthieu Tâche", email = "mtache@arista.com" }, - { name = "Khelil Sator", email = "ksator@arista.com" }, + { name = "Thomas Grimonet", email = "tgrimonet@arista.com" }, { name = "Guillaume Mulocher", email = "gmulocher@arista.com" }, ] description = "Arista Network Test Automation (ANTA) Framework" From 5d470f917e2853261ba8fa8ae4a9e80cb92a797e Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Mon, 4 Sep 2023 12:47:45 +0200 Subject: [PATCH 13/36] feat(anta.cli): Enable CLI command auto aliases (#371) --- anta/cli/__init__.py | 8 ++++---- anta/cli/utils.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/anta/cli/__init__.py b/anta/cli/__init__.py index 20b2cd353..e0462bcd6 100644 --- a/anta/cli/__init__.py +++ b/anta/cli/__init__.py @@ -18,7 +18,7 @@ from anta.cli.exec import commands as exec_commands from anta.cli.get import commands as get_commands from anta.cli.nrfu import commands as check_commands -from anta.cli.utils import IgnoreRequiredWithHelp, parse_catalog, parse_inventory +from anta.cli.utils import AliasedGroup, IgnoreRequiredWithHelp, parse_catalog, parse_inventory from anta.loader import setup_logging from anta.result_manager import ResultManager from anta.result_manager.models import TestResult @@ -151,17 +151,17 @@ def nrfu(ctx: click.Context, catalog: List[Tuple[Callable[..., TestResult], Dict ctx.obj["result_manager"] = ResultManager() -@anta.group("exec") +@anta.group("exec", cls=AliasedGroup) def _exec() -> None: """Execute commands to inventory devices""" -@anta.group("get") +@anta.group("get", cls=AliasedGroup) def _get() -> None: """Get data from/to ANTA""" -@anta.group("debug") +@anta.group("debug", cls=AliasedGroup) def _debug() -> None: """Debug commands for building ANTA""" diff --git a/anta/cli/utils.py b/anta/cli/utils.py index 399ab46e7..bb445ba80 100644 --- a/anta/cli/utils.py +++ b/anta/cli/utils.py @@ -164,3 +164,49 @@ def parse_args(self, ctx: click.Context, args: List[str]) -> List[str]: param.required = False return super().parse_args(ctx, args) + + def get_command(self, ctx: click.Context, cmd_name: str) -> Any: + """Todo: document code""" + rv = click.Group.get_command(self, ctx, cmd_name) + if rv is not None: + return rv + matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] + if not matches: + return None + if len(matches) == 1: + return click.Group.get_command(self, ctx, matches[0]) + ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") + return None + + def resolve_command(self, ctx: click.Context, args: Any) -> Any: + """Todo: document code""" + # always return the full command name + _, cmd, args = super().resolve_command(ctx, args) + return cmd.name, cmd, args # type: ignore + + +class AliasedGroup(click.Group): + """ + Implements a subclass of Group that accepts a prefix for a command. + If there were a command called push, it would accept pus as an alias (so long as it was unique) + From Click documentation + """ + + def get_command(self, ctx: click.Context, cmd_name: str) -> Any: + """Todo: document code""" + rv = click.Group.get_command(self, ctx, cmd_name) + if rv is not None: + return rv + matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] + if not matches: + return None + if len(matches) == 1: + return click.Group.get_command(self, ctx, matches[0]) + ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") + return None + + def resolve_command(self, ctx: click.Context, args: Any) -> Any: + """Todo: document code""" + # always return the full command name + _, cmd, args = super().resolve_command(ctx, args) + return cmd.name, cmd, args # type: ignore From dd6ef856a13feb4053b699a866a9d851da2f2ace Mon Sep 17 00:00:00 2001 From: Thomas Grimonet Date: Mon, 4 Sep 2023 14:25:34 +0200 Subject: [PATCH 14/36] chore: add .env files in gitignore (#375) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e36ee5cb2..47fe74192 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist scripts/test*.py examples/tests_* .personal/* +*.env *.swp # Distribution / packaging From aab78ba0496d34633bc5a1cd05c63f120601efe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20T=C3=A2che?= Date: Wed, 6 Sep 2023 13:50:16 +0200 Subject: [PATCH 15/36] feat(anta)!: Introduce AntaTest.Input and AntaTest.render() (#315) * feat: Introduce AntaTest.Input * Fix exception logging * Implement VerifyBGPIPv4UnicastCount using AntaTest.Input * Test inputs validation error now skips the test instead of raising an error * Unit test for VerifyBGPIPv4UnicastCount * Update docstrings * VerifyBGPIPv4UnicastCount: support distinct neighbor count per VRF * VerifyReachability: update to use AntaTest.Input * Update docstring * AntaTest: catch and logs exceptions in render() * AntaTest: Input validtion leads to "error" instead of "skipped" * Use self.logger * Linting * Update documentation * Update documentation * Add exception field to TestResult Use Literal type for result field * cut: remove .old file * fix: typo in TestResult * AntaTest.render() now takes kwargs * fix: Do not allow extra fields in AntaTest.Input * Linting * test: AntaTest unit tests * test: Add more AntaTest unit tests * VerifyReachability: unit test * Refactor unit tests data * anta.tests.connectivity: update unit tests * anta.tests.routing.bgp: refactor tests * anta.tests.routing.generic: refactor tests * Refactor unit tests data * anta.tests.routing.ospf: refactor tests * Linting * anta.tests.aaa: refactor tests * refactor unit tests * anta.tests.configuration: refactor tests * anta.tests.field_notices: refactor tests * refactor unit tests data * anta.tests.hardware: refactor tests * anta.tests.interfaces: refactor tests * small fixes * linting * anta.tests.logging: refactor tests * linting * anta.tests.mlag: refactor tests * anta.tests.multicast: refactor tests * anta.tests.profiles: refactor tests * anta.tests.security: refactor tests * linting * anta.tests.snmp: refactor tests * anta.tests.softwar: refactor tests * Introduce anta.types * anta.tests.software: refactor tests * anta.tests.stp: refactor tests * anta.tests.vxlan: refactor tests * anta.tests.system: refactor tests * move test_case module * update unit tests * fix import * fix unit tests * clean unit tests * Merge with main branch * Linting * Linting * Fix unit tests * Skip tests in tests.lib * Formatting * test: missing tests/lib/test_case.py file * fix merge * test: update unit tests for Python 3.8 * linting * Rename tests.lib.test_case to tests.lib.anta * Insert licence header * Document AntaTest unit tests * Update VerifyReachability to support egress interfaces * Update examples/tests.yaml * linting * Fix pattern * Update docs * Update docs * Update docs * Simplify VerifyReachability * Update examples/tests.yaml * unused import --- .gitignore | 2 +- anta/cli/debug/commands.py | 2 +- anta/cli/utils.py | 7 +- anta/custom_types.py | 23 + anta/decorators.py | 4 +- anta/loader.py | 69 +- anta/models.py | 357 ++++-- anta/reporter/models.py | 26 +- anta/result_manager/__init__.py | 14 +- anta/result_manager/models.py | 96 +- anta/runner.py | 62 +- anta/tests/aaa.py | 315 ++--- anta/tests/configuration.py | 17 +- anta/tests/connectivity.py | 48 +- anta/tests/field_notices.py | 10 +- anta/tests/hardware.py | 83 +- anta/tests/interfaces.py | 223 +--- anta/tests/logging.py | 91 +- anta/tests/mlag.py | 102 +- anta/tests/multicast.py | 66 +- anta/tests/profiles.py | 43 +- anta/tests/routing/bgp.py | 142 +-- anta/tests/routing/generic.py | 68 +- anta/tests/routing/ospf.py | 36 +- anta/tests/security.py | 169 +-- anta/tests/snmp.py | 93 +- anta/tests/software.py | 54 +- anta/tests/stp.py | 112 +- anta/tests/system.py | 64 +- anta/tests/vxlan.py | 12 - anta/tools/misc.py | 14 +- docs/advanced_usages/as-python-lib.md | 17 +- docs/advanced_usages/custom-tests.md | 304 +++-- docs/api/models.md | 8 +- docs/api/types.md | 10 + docs/usage-inventory-catalog.md | 47 - examples/tests.yaml | 56 +- mkdocs.yml | 3 +- tests/conftest.py | 30 + tests/data/json_data.py | 2 +- tests/lib/anta.py | 30 + tests/lib/parametrize.py | 6 - tests/lib/utils.py | 13 + tests/units/anta_tests/README.md | 44 + tests/units/anta_tests/aaa/__init__.py | 3 - tests/units/anta_tests/aaa/data.py | 1114 ----------------- tests/units/anta_tests/aaa/test_exc.py | 154 --- .../anta_tests/configuration/__init__.py | 3 - tests/units/anta_tests/configuration/data.py | 41 - .../anta_tests/configuration/test_exc.py | 51 - .../units/anta_tests/connectivity/__init__.py | 3 - tests/units/anta_tests/connectivity/data.py | 111 -- .../units/anta_tests/connectivity/test_exc.py | 36 - .../anta_tests/field_notices/__init__.py | 3 - .../anta_tests/field_notices/test_exc.py | 53 - tests/units/anta_tests/hardware/__init__.py | 3 - tests/units/anta_tests/hardware/test_exc.py | 148 --- tests/units/anta_tests/interfaces/__init__.py | 3 - tests/units/anta_tests/interfaces/test_exc.py | 270 ---- tests/units/anta_tests/logging/__init__.py | 3 - tests/units/anta_tests/logging/test_exc.py | 173 --- tests/units/anta_tests/mlag/__init__.py | 3 - tests/units/anta_tests/mlag/data.py | 361 ------ tests/units/anta_tests/mlag/test_exc.py | 106 -- tests/units/anta_tests/multicast/__init__.py | 3 - tests/units/anta_tests/multicast/test_exc.py | 51 - tests/units/anta_tests/profiles/__init__.py | 3 - tests/units/anta_tests/profiles/data.py | 96 -- tests/units/anta_tests/profiles/test_exc.py | 51 - .../units/anta_tests/routing/bgp/__init__.py | 3 - .../units/anta_tests/routing/bgp/test_exc.py | 183 --- .../anta_tests/routing/generic/__init__.py | 3 - .../anta_tests/routing/generic/test_exc.py | 70 -- .../units/anta_tests/routing/ospf/__init__.py | 3 - .../units/anta_tests/routing/ospf/test_exc.py | 53 - .../routing/{bgp/data.py => test_bgp.py} | 232 ++-- .../{generic/data.py => test_generic.py} | 96 +- .../routing/{ospf/data.py => test_ospf.py} | 70 +- tests/units/anta_tests/security/__init__.py | 3 - tests/units/anta_tests/security/data.py | 517 -------- tests/units/anta_tests/security/test_exc.py | 173 --- tests/units/anta_tests/snmp/__init__.py | 3 - tests/units/anta_tests/snmp/data.py | 233 ---- tests/units/anta_tests/snmp/test_exc.py | 70 -- tests/units/anta_tests/software/__init__.py | 3 - tests/units/anta_tests/software/test_exc.py | 72 -- tests/units/anta_tests/stp/__init__.py | 3 - tests/units/anta_tests/stp/data.py | 664 ---------- tests/units/anta_tests/stp/test_exc.py | 104 -- tests/units/anta_tests/system/__init__.py | 3 - tests/units/anta_tests/system/test_exc.py | 173 --- tests/units/anta_tests/test_aaa.py | 516 ++++++++ tests/units/anta_tests/test_configuration.py | 35 + tests/units/anta_tests/test_connectivity.py | 151 +++ .../data.py => test_field_notices.py} | 113 +- .../{hardware/data.py => test_hardware.py} | 358 ++---- .../data.py => test_interfaces.py} | 645 +++------- .../{logging/data.py => test_logging.py} | 223 ++-- tests/units/anta_tests/test_mlag.py | 288 +++++ .../{multicast/data.py => test_multicast.py} | 88 +- tests/units/anta_tests/test_profiles.py | 47 + tests/units/anta_tests/test_security.py | 220 ++++ tests/units/anta_tests/test_snmp.py | 78 ++ .../{software/data.py => test_software.py} | 50 +- tests/units/anta_tests/test_stp.py | 268 ++++ .../{system/data.py => test_system.py} | 221 ++-- tests/units/anta_tests/test_vxlan.py | 198 +++ tests/units/anta_tests/vxlan/__init__.py | 3 - tests/units/anta_tests/vxlan/data.py | 413 ------ tests/units/anta_tests/vxlan/test_exc.py | 51 - tests/units/antatests_test.py.old | 90 -- tests/units/inventory/test_models.py | 20 +- tests/units/result_manager/test__init__.py | 9 +- tests/units/result_manager/test_models.py | 8 +- tests/units/test_models.py | 264 ++++ tests/units/tools/test_misc.py | 9 +- tests/units/tools/test_pydantic.py | 4 + 117 files changed, 4123 insertions(+), 8821 deletions(-) create mode 100644 anta/custom_types.py create mode 100644 docs/api/types.md create mode 100644 tests/lib/anta.py delete mode 100644 tests/lib/parametrize.py create mode 100644 tests/units/anta_tests/README.md delete mode 100644 tests/units/anta_tests/aaa/__init__.py delete mode 100644 tests/units/anta_tests/aaa/data.py delete mode 100644 tests/units/anta_tests/aaa/test_exc.py delete mode 100644 tests/units/anta_tests/configuration/__init__.py delete mode 100644 tests/units/anta_tests/configuration/data.py delete mode 100644 tests/units/anta_tests/configuration/test_exc.py delete mode 100644 tests/units/anta_tests/connectivity/__init__.py delete mode 100644 tests/units/anta_tests/connectivity/data.py delete mode 100644 tests/units/anta_tests/connectivity/test_exc.py delete mode 100644 tests/units/anta_tests/field_notices/__init__.py delete mode 100644 tests/units/anta_tests/field_notices/test_exc.py delete mode 100644 tests/units/anta_tests/hardware/__init__.py delete mode 100644 tests/units/anta_tests/hardware/test_exc.py delete mode 100644 tests/units/anta_tests/interfaces/__init__.py delete mode 100644 tests/units/anta_tests/interfaces/test_exc.py delete mode 100644 tests/units/anta_tests/logging/__init__.py delete mode 100644 tests/units/anta_tests/logging/test_exc.py delete mode 100644 tests/units/anta_tests/mlag/__init__.py delete mode 100644 tests/units/anta_tests/mlag/data.py delete mode 100644 tests/units/anta_tests/mlag/test_exc.py delete mode 100644 tests/units/anta_tests/multicast/__init__.py delete mode 100644 tests/units/anta_tests/multicast/test_exc.py delete mode 100644 tests/units/anta_tests/profiles/__init__.py delete mode 100644 tests/units/anta_tests/profiles/data.py delete mode 100644 tests/units/anta_tests/profiles/test_exc.py delete mode 100644 tests/units/anta_tests/routing/bgp/__init__.py delete mode 100644 tests/units/anta_tests/routing/bgp/test_exc.py delete mode 100644 tests/units/anta_tests/routing/generic/__init__.py delete mode 100644 tests/units/anta_tests/routing/generic/test_exc.py delete mode 100644 tests/units/anta_tests/routing/ospf/__init__.py delete mode 100644 tests/units/anta_tests/routing/ospf/test_exc.py rename tests/units/anta_tests/routing/{bgp/data.py => test_bgp.py} (84%) rename tests/units/anta_tests/routing/{generic/data.py => test_generic.py} (71%) rename tests/units/anta_tests/routing/{ospf/data.py => test_ospf.py} (87%) delete mode 100644 tests/units/anta_tests/security/__init__.py delete mode 100644 tests/units/anta_tests/security/data.py delete mode 100644 tests/units/anta_tests/security/test_exc.py delete mode 100644 tests/units/anta_tests/snmp/__init__.py delete mode 100644 tests/units/anta_tests/snmp/data.py delete mode 100644 tests/units/anta_tests/snmp/test_exc.py delete mode 100644 tests/units/anta_tests/software/__init__.py delete mode 100644 tests/units/anta_tests/software/test_exc.py delete mode 100644 tests/units/anta_tests/stp/__init__.py delete mode 100644 tests/units/anta_tests/stp/data.py delete mode 100644 tests/units/anta_tests/stp/test_exc.py delete mode 100644 tests/units/anta_tests/system/__init__.py delete mode 100644 tests/units/anta_tests/system/test_exc.py create mode 100644 tests/units/anta_tests/test_aaa.py create mode 100644 tests/units/anta_tests/test_configuration.py create mode 100644 tests/units/anta_tests/test_connectivity.py rename tests/units/anta_tests/{field_notices/data.py => test_field_notices.py} (69%) rename tests/units/anta_tests/{hardware/data.py => test_hardware.py} (74%) rename tests/units/anta_tests/{interfaces/data.py => test_interfaces.py} (60%) rename tests/units/anta_tests/{logging/data.py => test_logging.py} (50%) create mode 100644 tests/units/anta_tests/test_mlag.py rename tests/units/anta_tests/{multicast/data.py => test_multicast.py} (65%) create mode 100644 tests/units/anta_tests/test_profiles.py create mode 100644 tests/units/anta_tests/test_security.py create mode 100644 tests/units/anta_tests/test_snmp.py rename tests/units/anta_tests/{software/data.py => test_software.py} (64%) create mode 100644 tests/units/anta_tests/test_stp.py rename tests/units/anta_tests/{system/data.py => test_system.py} (56%) create mode 100644 tests/units/anta_tests/test_vxlan.py delete mode 100644 tests/units/anta_tests/vxlan/__init__.py delete mode 100644 tests/units/anta_tests/vxlan/data.py delete mode 100644 tests/units/anta_tests/vxlan/test_exc.py delete mode 100755 tests/units/antatests_test.py.old create mode 100644 tests/units/test_models.py diff --git a/.gitignore b/.gitignore index 47fe74192..ff478a4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +./lib/ lib64/ parts/ sdist/ diff --git a/anta/cli/debug/commands.py b/anta/cli/debug/commands.py index 63f6d0195..49f65df56 100644 --- a/anta/cli/debug/commands.py +++ b/anta/cli/debug/commands.py @@ -79,7 +79,7 @@ def run_template(template: str, params: List[str], ofmt: Literal["json", "text"] # I do not assume the following line, but click make me do it v: Literal[1, "latest"] = version if version == "latest" else 1 t = AntaTemplate(template=template, ofmt=ofmt, version=v, revision=revision) - c = t.render(template_params) + c = t.render(**template_params) # type: ignore asyncio.run(device.collect(c)) if ofmt == "json": console.print(c.json_output) diff --git a/anta/cli/utils.py b/anta/cli/utils.py index bb445ba80..54d5fdfcc 100644 --- a/anta/cli/utils.py +++ b/anta/cli/utils.py @@ -11,14 +11,13 @@ import enum import logging from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, List, Optional, Tuple import click from yaml import safe_load import anta.loader from anta.inventory import AntaInventory -from anta.result_manager.models import TestResult from anta.tools.misc import anta_log_exception logger = logging.getLogger(__name__) @@ -26,6 +25,8 @@ if TYPE_CHECKING: from click import Option + from anta.models import AntaTest + class ExitCode(enum.IntEnum): """ @@ -80,7 +81,7 @@ def parse_tags(ctx: click.Context, param: Option, value: str) -> Optional[List[s return None -def parse_catalog(ctx: click.Context, param: Option, value: str) -> List[Tuple[Callable[..., TestResult], Dict[Any, Any]]]: +def parse_catalog(ctx: click.Context, param: Option, value: str) -> List[Tuple[AntaTest, dict[str, Any] | None]]: # pylint: disable=unused-argument """ Click option callback to parse an ANTA tests catalog YAML file diff --git a/anta/custom_types.py b/anta/custom_types.py new file mode 100644 index 000000000..1e593472a --- /dev/null +++ b/anta/custom_types.py @@ -0,0 +1,23 @@ +# Copyright (c) 2023 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 that provides predefined types for AntaTest.Input instances +""" +from typing import Literal + +from pydantic import Field +from pydantic.functional_validators import AfterValidator +from typing_extensions import Annotated + + +def aaa_group_prefix(v: str) -> str: + """Prefix the AAA method with 'group' if it is known""" + built_in_methods = ["local", "none", "logging"] + return f"group {v}" if v not in built_in_methods and not v.startswith("group ") else v + + +AAAAuthMethod = Annotated[str, AfterValidator(aaa_group_prefix)] +Vlan = Annotated[int, Field(ge=0, le=4094)] +TestStatus = Literal["unset", "success", "failure", "error", "skipped"] +Interface = Annotated[str, Field(pattern=r"^(Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\/[0-9]+)*$")] diff --git a/anta/decorators.py b/anta/decorators.py index 6fadd35e9..e3b49ff98 100644 --- a/anta/decorators.py +++ b/anta/decorators.py @@ -90,13 +90,13 @@ async def wrapper(*args: Any, **kwargs: Any) -> TestResult: elif family == "rtc": command = AntaCommand(command="show bgp rt-membership summary") else: - anta_test.result.is_error(f"Wrong address family for bgp decorator: {family}") + anta_test.result.is_error(message=f"Wrong address family for bgp decorator: {family}") return anta_test.result await anta_test.device.collect(command=command) if not command.collected and command.failed is not None: - anta_test.result.is_error(f"{command.command}: {exc_to_str(command.failed)}") + anta_test.result.is_error(message=f"{command.command}: {exc_to_str(command.failed)}") return anta_test.result if "vrfs" not in command.json_output: anta_test.result.is_skipped(f"no BGP configuration for {family} on this device") diff --git a/anta/loader.py b/anta/loader.py index 69791ab34..10d559124 100644 --- a/anta/loader.py +++ b/anta/loader.py @@ -10,12 +10,12 @@ import logging import sys from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Tuple from rich.logging import RichHandler from anta import __DEBUG__ -from anta.result_manager.models import TestResult +from anta.models import AntaTest logger = logging.getLogger(__name__) @@ -75,17 +75,52 @@ def setup_logging(level: str = logging.getLevelName(logging.INFO), file: Path | logger.debug("ANTA Debug Mode enabled") -def parse_catalog(test_catalog: Dict[Any, Any], package: Optional[str] = None) -> List[Tuple[Callable[..., TestResult], Dict[Any, Any]]]: +def parse_catalog(test_catalog: dict[str, Any], package: str | None = None) -> list[Tuple[AntaTest, dict[str, Any] | None]]: """ - Function to parse the catalog and return a list of tests + Function to parse the catalog and return a list of tests with their inputs + + A valid test catalog must follow the following structure: + : + - : + + + Example: + anta.tests.connectivity: + - VerifyReachability: + hosts: + - dst: 8.8.8.8 + src: 172.16.0.1 + - dst: 1.1.1.1 + src: 172.16.0.1 + result_overwrite: + categories: + - "Overwritten category 1" + description: "Test with overwritten description" + custom_field: "Test run by John Doe" + + Also supports nesting for Python module definition: + anta.tests: + connectivity: + - VerifyReachability: + hosts: + - dst: 8.8.8.8 + src: 172.16.0.1 + - dst: 1.1.1.1 + src: 172.16.0.1 + result_overwrite: + categories: + - "Overwritten category 1" + description: "Test with overwritten description" + custom_field: "Test run by John Doe" Args: - test_catalog (Dict[Any, Any]): List of tests defined in catalog YAML file + test_catalog: Python dictionary representing the test catalog YAML file Returns: - List[Tuple[Callable[..., TestResult], Dict[Any, Any]]]: List of python function tests to run. + tests: List of tuples (test, inputs) where test is a reference of an AntaTest subclass + and inputs is a dictionary """ - tests: List[Tuple[Callable[..., TestResult], Dict[Any, Any]]] = [] + tests: list[Tuple[AntaTest, dict[str, Any] | None]] = [] if not test_catalog: return tests for key, value in test_catalog.items(): @@ -95,21 +130,27 @@ def parse_catalog(test_catalog: Dict[Any, Any], package: Optional[str] = None) - try: module = importlib.import_module(f"{key}") except ModuleNotFoundError: - logger.error(f"No test module named '{key}'") + logger.critical(f"No test module named '{key}'") sys.exit(1) if isinstance(value, list): # This is a list of tests for test in value: - for func_name, args in test.items(): + for test_name, inputs in test.items(): + # A test must be a subclass of AntaTest as defined in the Python module try: - func = getattr(module, func_name) + test = getattr(module, test_name) except AttributeError: - logger.error(f"Wrong test function name '{func_name}' in '{module.__name__}'") + logger.critical(f"Wrong test name '{test_name}' in '{module.__name__}'") sys.exit(1) - if not callable(func): - logger.error(f"'{func.__module__}.{func.__name__}' is not a function") + if not issubclass(test, AntaTest): + logger.critical(f"'{test.__module__}.{test.__name__}' is not an AntaTest subclass") + sys.exit(1) + # Test inputs can be either None or a dictionary + if inputs is None or isinstance(inputs, dict): + tests.append((test, inputs)) + else: + logger.critical(f"'{test.__module__}.{test.__name__}' inputs must be a dictionary") sys.exit(1) - tests.append((func, args if args is not None else {})) if isinstance(value, dict): # This is an inner Python module tests.extend(parse_catalog(value, package=module.__name__)) diff --git a/anta/models.py b/anta/models.py index c328bf2f0..780a24713 100644 --- a/anta/models.py +++ b/anta/models.py @@ -4,7 +4,6 @@ """ Models to define a TestStructure """ - from __future__ import annotations import logging @@ -15,7 +14,7 @@ from functools import wraps from typing import TYPE_CHECKING, Any, Callable, ClassVar, Coroutine, Dict, List, Literal, Optional, TypeVar, Union -from pydantic import BaseModel, ConfigDict, conint +from pydantic import BaseModel, ConfigDict, ValidationError, conint from rich.progress import Progress, TaskID from anta.result_manager.models import TestResult @@ -29,17 +28,10 @@ logger = logging.getLogger(__name__) -# TODO: Notes on eAPI version/revision -# eAPI models are revisioned, this means that if a model is modified in a non-backwards compatible way, then its revision will be bumped up -# (revisions are numbers, default value is 1). -# By default an eAPI request will return revision 1 of the model instance, -# this ensures that older management software will not suddenly stop working when a switch is upgraded. -# A "revision" applies to a particular CLI command whereas a "version" is global and is internally -# translated to a specific "revision" for each CLI command in the rpc. - class AntaTemplate(BaseModel): - """Class to define a test command with its API version + """Class to define a command template as Python f-string. + Can render a command from parameters. Attributes: template: Python f-string. Example: 'show vlan {vlan_id}' @@ -53,28 +45,42 @@ class AntaTemplate(BaseModel): revision: Optional[conint(ge=1, le=99)] = None # type: ignore ofmt: Literal["json", "text"] = "json" - def render(self, params: Dict[str, Any]) -> AntaCommand: + def render(self, **params: dict[str, Any]) -> AntaCommand: """Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. - Args: - params: dictionary of variables with string values to render the Python f-string + Args: + params: dictionary of variables with string values to render the Python f-string - Returns: - AntaCommand: The rendered AntaCommand. - This AntaCommand instance have a template attribute that references this - AntaTemplate instance. + Returns: + command: The rendered AntaCommand. + This AntaCommand instance have a template attribute that references this + AntaTemplate instance. """ - return AntaCommand(command=self.template.format(**params), ofmt=self.ofmt, version=self.version, revision=self.revision, template=self, params=params) + try: + return AntaCommand(command=self.template.format(**params), ofmt=self.ofmt, version=self.version, revision=self.revision, template=self, params=params) + except KeyError as e: + raise AntaTemplateRenderError(self, e.args[0]) from e class AntaCommand(BaseModel): - """Class to define a test command with its API version + """Class to define a command. + + !!! info + eAPI models are revisioned, this means that if a model is modified in a non-backwards compatible way, then its revision will be bumped up + (revisions are numbers, default value is 1). + + By default an eAPI request will return revision 1 of the model instance, + this ensures that older management software will not suddenly stop working when a switch is upgraded. + A **revision** applies to a particular CLI command whereas a **version** is global and is internally + translated to a specific **revision** for each CLI command in the RPC. + + __Revision has precedence over version.__ Attributes: command: Device command version: eAPI version - valid values are 1 or "latest" - default is "latest" - revision: Revision of the command. Valid values are 1 to 99. Revision has precedence over version. + revision: eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt: eAPI output - json or text - default is json template: AntaTemplate object used to render this command params: dictionary of variables with string values to render the template @@ -94,7 +100,7 @@ class AntaCommand(BaseModel): params: Optional[Dict[str, Any]] = None @property - def json_output(self) -> Dict[str, Any]: + def json_output(self) -> dict[str, Any]: """Get the command output as JSON""" if self.output is None: raise RuntimeError(f"There is no output for command {self.command}") @@ -138,112 +144,228 @@ def should_skip( """ +class AntaTemplateRenderError(RuntimeError): + """ + Raised when an AntaTemplate object could not be rendered + because of missing parameters + """ + + def __init__(self, template: AntaTemplate, key: str): + """Constructor for AntaTemplateRenderError + + Args: + template: The AntaTemplate instance that failed to render + key: Key that has not been provided to render the template + """ + self.template = template + self.key = key + super().__init__(f"'{self.key}' was not provided for template '{self.template.template}'") + + class AntaTest(ABC): - """Abstract class defining a test for Anta + """Abstract class defining a test in ANTA The goal of this class is to handle the heavy lifting and make writing a test as simple as possible. - TODO - complete doctstring with example + Examples: + The following is an example of an AntaTest subclass implementation: + ```python + class VerifyReachability(AntaTest): + name = "VerifyReachability" + description = "Test the network reachability to one or many destination IP(s)." + categories = ["connectivity"] + commands = [AntaTemplate(template="ping vrf {vrf} {dst} source {src} repeat 2")] + + class Input(AntaTest.Input): + hosts: List[Host] + class Host(BaseModel): + dst: IPv4Address + src: IPv4Address + vrf: str = "default" + + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render({"dst": host.dst, "src": host.src, "vrf": host.vrf}) for host in self.inputs.hosts] + + @AntaTest.anta_test + def test(self) -> None: + failures = [] + for command in self.instance_commands: + if command.params and ("src" and "dst") in command.params: + src, dst = command.params["src"], command.params["dst"] + if "2 received" not in command.json_output["messages"][0]: + failures.append((str(src), str(dst))) + if not failures: + self.result.is_success() + else: + self.result.is_failure(f"Connectivity test failed for the following source-destination pairs: {failures}") + ``` + Attributes: + device: AntaDevice instance on which this test is run + inputs: AntaTest.Input instance carrying the test inputs + instance_commands: List of AntaCommand instances of this test + result: TestResult instance representing the result of this test + logger: Python logger for this test instance """ - # pylint: disable=too-many-instance-attributes - # Mandatory class attributes # TODO - find a way to tell mypy these are mandatory for child classes - maybe Protocol name: ClassVar[str] description: ClassVar[str] categories: ClassVar[list[str]] - # Or any child type - commands: ClassVar[list[AntaCommand]] - # TODO - today we support only one template per Test - template: ClassVar[AntaTemplate] + commands: ClassVar[list[Union[AntaTemplate, AntaCommand]]] + # Optional class attributes + test_filters: ClassVar[list[AntaTestFilter]] + # Class attributes to handle the progress bar of ANTA CLI progress: Optional[Progress] = None nrfu_task: Optional[TaskID] = None - # Optional class attributes - test_filters: ClassVar[list[AntaTestFilter]] + class Input(BaseModel): + """Class defining inputs for a test in ANTA. + + Examples: + A valid test catalog will look like the following: + ```yaml + : + - : + result_overwrite: + categories: + - "Overwritten category 1" + description: "Test with overwritten description" + custom_field: "Test run by John Doe" + ``` + Attributes: + result_overwrite: Define fields to overwrite in the TestResult object + """ + + model_config = ConfigDict(extra="forbid") + result_overwrite: Optional[ResultOverwrite] = None + + class ResultOverwrite(BaseModel): + """Test inputs model to overwrite result fields + + Attributes: + description: overwrite TestResult.description + categories: overwrite TestResult.categories + custom_field: a free string that will be included in the TestResult object + """ + + description: Optional[str] = None + categories: Optional[List[str]] = None + custom_field: Optional[str] = None def __init__( self, device: AntaDevice, - template_params: list[dict[str, Any]] | None = None, - result_description: str | None = None, - result_categories: list[str] | None = None, - result_custom_field: str | None = None, - # TODO document very well the order of eos_data + inputs: dict[str, Any] | None, eos_data: list[dict[Any, Any] | str] | None = None, - labels: list[str] | None = None, ): - """ - AntaTest Constructor + """AntaTest Constructor - Doc to be completed - - Arguments: - result_custom_field (str): a free string that is included in the TestResult object + Args: + device: AntaDevice instance on which the test will be run + inputs: dictionary of attributes used to instantiate the AntaTest.Input instance + eos_data: Populate outputs of the test commands instead of collecting from devices. + This list must have the same length and order than the `instance_commands` instance attribute. """ - # Accept 6 input arguments - # pylint: disable=R0913 self.logger: logging.Logger = logging.getLogger(f"{self.__module__}.{self.__class__.__name__}") self.device: AntaDevice = device - self.result: TestResult = TestResult( - name=device.name, - test=self.name, - categories=result_categories or self.categories, - description=result_description or self.description, - custom_field=result_custom_field, - ) - self.labels: List[str] = labels or [] - self.instance_commands: List[AntaCommand] = [] - - # TODO - check optimization for deepcopy - # Generating instance_commands from list of commands and template - if hasattr(self.__class__, "commands") and (cmds := self.__class__.commands) is not None: - self.instance_commands.extend(deepcopy(cmds)) - if hasattr(self.__class__, "template") and (tpl := self.__class__.template) is not None: - if template_params is None: - self.result.is_error("Command has template but no params were given") - return - self.template_params = template_params - for param in template_params: - try: - self.instance_commands.append(tpl.render(param)) - except KeyError: - self.result.is_error(f"Cannot render template '{tpl.template}': wrong parameters") - return + self.inputs: AntaTest.Input + self.instance_commands: list[AntaCommand] = [] + self.result: TestResult = TestResult(name=device.name, test=self.name, categories=self.categories, description=self.description) + self._init_inputs(inputs) + if self.result.result == "unset": + self._init_commands(eos_data) + + def _init_inputs(self, inputs: dict[str, Any] | None) -> None: + """Instantiate the `inputs` instance attribute with an `AntaTest.Input` instance + to validate test inputs from defined model. + Overwrite result fields based on `ResultOverwrite` input definition. + + Any input validation error will set this test result status as 'error'.""" + try: + if inputs is not None: + self.inputs = self.Input(**inputs) + else: + self.inputs = self.Input() + except ValidationError as e: + message = f"{self.__module__}.{self.__class__.__name__}: Inputs are not valid\n{e}" + self.logger.error(message) + self.result.is_error(message=message, exception=e) + return + if res_ow := self.inputs.result_overwrite: + if res_ow.categories: + self.result.categories = res_ow.categories + if res_ow.description: + self.result.description = res_ow.description + self.result.custom_field = res_ow.custom_field + + def _init_commands(self, eos_data: list[dict[Any, Any] | str] | None) -> None: + """Instantiate the `instance_commands` instance attribute from the `commands` class attribute. + - Copy of the `AntaCommand` instances + - Render all `AntaTemplate` instances using the `render()` method + + Any template rendering error will set this test result status as 'error'. + Any exception in user code in `render()` will set this test result status as 'error'. + """ + if self.__class__.commands: + for cmd in self.__class__.commands: + if isinstance(cmd, AntaCommand): + self.instance_commands.append(deepcopy(cmd)) + elif isinstance(cmd, AntaTemplate): + try: + self.instance_commands.extend(self.render(cmd)) + except AntaTemplateRenderError as e: + self.result.is_error(message=f"Cannot render template {{{e.template}}}", exception=e) + return + except NotImplementedError as e: + self.result.is_error(message=e.args[0], exception=e) + return + except Exception as e: # pylint: disable=broad-exception-caught + # render() is user-defined code. + # We need to catch everything if we want the AntaTest object + # to live until the reporting + message = f"Exception in {self.__module__}.{self.__class__.__name__}.render()" + anta_log_exception(e, message, self.logger) + self.result.is_error(message=f"{message}: {exc_to_str(e)}", exception=e) + return if eos_data is not None: - self.logger.debug("Test initialized with input data") + self.logger.debug(f"Test {self.name} initialized with input data") self.save_commands_data(eos_data) - def save_commands_data(self, eos_data: list[dict[Any, Any] | str]) -> None: - """Called at init or at test execution time""" + def save_commands_data(self, eos_data: list[dict[str, Any] | str]) -> None: + """Populate output of all AntaCommand instances in `instance_commands`""" if len(eos_data) != len(self.instance_commands): - self.result.is_error("Test initialization error: Trying to save more data than there are commands for the test") + self.result.is_error(message="Test initialization error: Trying to save more data than there are commands for the test") return for index, data in enumerate(eos_data or []): self.instance_commands[index].output = data - def all_data_collected(self) -> bool: - """returns True if output is populated for every command""" + def __init_subclass__(cls) -> None: + """Verify that the mandatory class attributes are defined""" + mandatory_attributes = ["name", "description", "categories", "commands"] + for attr in mandatory_attributes: + if not hasattr(cls, attr): + raise NotImplementedError(f"Class {cls.__module__}.{cls.__name__} is missing required class attribute {attr}") + + @property + def collected(self) -> bool: + """Returns True if all commands for this test have been collected.""" return all(command.collected for command in self.instance_commands) - def get_failed_commands(self) -> List[AntaCommand]: - """returns a list of all the commands that have a populated failed field""" + @property + def failed_commands(self) -> list[AntaCommand]: + """Returns a list of all the commands that have failed.""" return [command for command in self.instance_commands if command.failed is not None] - def __init_subclass__(cls) -> None: - """ - Verify that the mandatory class attributes are defined - """ - mandatory_attributes = ["name", "description", "categories"] - for attr in mandatory_attributes: - if not hasattr(cls, attr): - raise NotImplementedError(f"Class {cls} is missing required class attribute {attr}") - # Check that either commands or template exist - if not (hasattr(cls, "commands") or hasattr(cls, "template")): - raise NotImplementedError(f"Class {cls} is missing required either commands or template attribute") + def render(self, template: AntaTemplate) -> list[AntaCommand]: + """Render an AntaTemplate instance of this AntaTest using the provided + AntaTest.Input instance at self.inputs. + + This is not an abstract method because it does not need to be implemented if there is + no AntaTemplate for this test.""" + raise NotImplementedError(f"AntaTemplate are provided but render() method has not been implemented for {self.__module__}.{self.name}") async def collect(self) -> None: """ @@ -254,12 +376,19 @@ async def collect(self) -> None: except Exception as e: # pylint: disable=broad-exception-caught message = f"Exception raised while collecting commands for test {self.name} (on device {self.device.name})" anta_log_exception(e, message, self.logger) - self.result.is_error(exc_to_str(e)) + self.result.is_error(message=exc_to_str(e)) @staticmethod def anta_test(function: F) -> Callable[..., Coroutine[Any, Any, TestResult]]: """ - Decorator for anta_test that handles injecting test data if given and collecting it using asyncio if missing + Decorator for the `test()` method. + + This decorator implements (in this order): + + 1. Instantiate the command outputs if `eos_data` is provided to the `test()` method + 2. Collect the commands from the device + 3. Run the `test()` method + 4. Catches any exception in `test()` user code and set the `result` instance attribute """ @wraps(function) @@ -269,14 +398,12 @@ async def wrapper( **kwargs: Any, ) -> TestResult: """ - Wraps the test function and implement (in this order): - 1. Instantiate the command outputs if `eos_data` is provided - 2. Collect missing command outputs from the device - 3. Run the test function - 4. Catches and set the result if the test function raises an exception + Args: + eos_data: Populate outputs of the test commands instead of collecting from devices. + This list must have the same length and order than the `instance_commands` instance attribute. Returns: - TestResult: self.result, populated with the correct exit status + result: TestResult instance attribute populated with error status if any """ def format_td(seconds: float, digits: int = 3) -> str: @@ -295,22 +422,24 @@ def format_td(seconds: float, digits: int = 3) -> str: self.logger.debug(f"Test {self.name} initialized with input data {eos_data}") # If some data is missing, try to collect - if not self.all_data_collected(): + if not self.collected: await self.collect() if self.result.result != "unset": return self.result try: - if cmds := self.get_failed_commands(): + if self.failed_commands: self.result.is_error( - "\n".join([f"{cmd.command} has failed: {exc_to_str(cmd.failed)}" if cmd.failed else f"{cmd.command} has failed" for cmd in cmds]) + message="\n".join( + [f"{cmd.command} has failed: {exc_to_str(cmd.failed)}" if cmd.failed else f"{cmd.command} has failed" for cmd in self.failed_commands] + ) ) return self.result function(self, **kwargs) except Exception as e: # pylint: disable=broad-exception-caught message = f"Exception raised for test {self.name} (on device {self.device.name})" anta_log_exception(e, message, self.logger) - self.result.is_error(exc_to_str(e)) + self.result.is_error(message=exc_to_str(e)) test_duration = time.time() - start_time self.logger.debug(f"Executing test {self.name} on device {self.device.name} took {format_td(test_duration)}") @@ -331,14 +460,18 @@ def update_progress(cls) -> None: @abstractmethod def test(self) -> Coroutine[Any, Any, TestResult]: """ - This abstract method is the core of the test. - It MUST set the correct status of self.result with the appropriate error messages - - it must be implemented as follow - - @AntaTest.anta_test - def test(self) -> None: - ''' - assert code - ''' + This abstract method is the core of the test logic. + It must set the correct status of the `result` instance attribute + with the appropriate outcome of the test. + + Examples: + It must be implemented using the `AntaTest.anta_test` decorator: + ```python + @AntaTest.anta_test + def test(self) -> None: + self.result.is_success() + for command in self.instance_commands: + if not self._test_command(command): # _test_command() is an arbitrary test logic + self.result.is_failure("Failure reson") + ``` """ diff --git a/anta/reporter/models.py b/anta/reporter/models.py index 200c2d49c..ad1abb52c 100644 --- a/anta/reporter/models.py +++ b/anta/reporter/models.py @@ -3,10 +3,10 @@ # that can be found in the LICENSE file. """Models related to anta.result_manager module.""" -from pydantic import BaseModel, validator +from pydantic import BaseModel from rich.text import Text -from ..result_manager.models import RESULT_OPTIONS +from anta.custom_types import TestStatus class ColorManager(BaseModel): @@ -17,29 +17,9 @@ class ColorManager(BaseModel): color (str): Associated color. """ - level: str + level: TestStatus color: str - @validator("level", allow_reuse=True) - def name_must_be_in(cls, v: str) -> str: - """ - Status validator - - Validate status is a supported one - - Args: - v (str): User defined level - - Raises: - ValueError: If level is unsupported - - Returns: - str: level value - """ - if v not in RESULT_OPTIONS: - raise ValueError(f"must be one of {RESULT_OPTIONS}") - return v - def style_rich(self) -> Text: """ Build a rich Text syntax with color diff --git a/anta/result_manager/__init__.py b/anta/result_manager/__init__.py index 03c9a3a66..906e4ca46 100644 --- a/anta/result_manager/__init__.py +++ b/anta/result_manager/__init__.py @@ -9,7 +9,10 @@ import logging from typing import Any, List -from anta.result_manager.models import RESULT_OPTIONS, ListResult, TestResult +from pydantic import TypeAdapter + +from anta.custom_types import TestStatus +from anta.result_manager.models import ListResult, TestResult from anta.tools.pydantic import pydantic_to_dict logger = logging.getLogger(__name__) @@ -87,7 +90,7 @@ def __init__(self) -> None: """ self._result_entries = ListResult() # Initialize status - self.status = "unset" + self.status: TestStatus = "unset" self.error_status = False def __len__(self) -> int: @@ -96,16 +99,15 @@ def __len__(self) -> int: """ return len(self._result_entries) - def _update_status(self, test_status: str) -> None: + def _update_status(self, test_status: TestStatus) -> None: """ Update ResultManager status based on the table above. """ - if test_status not in RESULT_OPTIONS: - raise ValueError("{test_status} is not a valid result option") + ResultValidator = TypeAdapter(TestStatus) + ResultValidator.validate_python(test_status) if test_status == "error": self.error_status = True return - if self.status == "unset": self.status = test_status elif self.status == "skipped" and test_status in {"success", "failure"}: diff --git a/anta/result_manager/models.py b/anta/result_manager/models.py index 46b9aecb8..ac8b18dab 100644 --- a/anta/result_manager/models.py +++ b/anta/result_manager/models.py @@ -2,12 +2,13 @@ # Use of this source code is governed by the Apache License 2.0 # that can be found in the LICENSE file. """Models related to anta.result_manager module.""" +from __future__ import annotations from typing import Iterator, List, Optional -from pydantic import BaseModel, RootModel, field_validator +from pydantic import BaseModel, ConfigDict, RootModel -RESULT_OPTIONS = ["unset", "success", "failure", "error", "skipped"] +from anta.custom_types import TestStatus class TestResult(BaseModel): @@ -15,107 +16,76 @@ class TestResult(BaseModel): Describe the result of a test from a single device. Attributes: - name (str): Device name where the test has run. - test (str): Test name runs on the device. - categories (List[str]): List of categories the TestResult belongs to, by default the AntaTest categories. - description (str): TestResult description, by default the AntaTest description. - results (str): Result of the test. Can be one of ["unset", "success", "failure", "error", "skipped"]. - message (str, optional): Message to report after the test if any. - custom_field (str, optional): Custom field to store a string for flexibility in integrating with ANTA + name: Device name where the test has run. + test: Test name runs on the device. + categories: List of categories the TestResult belongs to, by default the AntaTest categories. + description: TestResult description, by default the AntaTest description. + results: Result of the test. Can be one of ["unset", "success", "failure", "error", "skipped"]. + message: Message to report after the test if any. + error: Exception object if the test result is "error" and an Exception occured + custom_field: Custom field to store a string for flexibility in integrating with ANTA """ + # This is required if we want to keep an Exception object in the error field + model_config = ConfigDict(arbitrary_types_allowed=True) + name: str test: str categories: List[str] description: str - result: str = "unset" + result: TestStatus = "unset" messages: List[str] = [] + error: Optional[Exception] = None custom_field: Optional[str] = None - @classmethod - @field_validator("result") - def name_must_be_in(cls, v: str) -> str: - """ - Status validator - - Validate status is a supported one - - Args: - v (str): User defined status - - Raises: - ValueError: If status is unsupported - - Returns: - str: status value - """ - if v not in RESULT_OPTIONS: - raise ValueError(f"must be one of {RESULT_OPTIONS}") - return v - - def is_success(self, message: str = "") -> bool: + def is_success(self, message: str | None = None) -> None: """ Helper to set status to success Args: - message (str): Optional message related to the test - - Returns: - bool: Always true + message: Optional message related to the test """ - return self._set_status("success", message) + self._set_status("success", message) - def is_failure(self, message: str = "") -> bool: + def is_failure(self, message: str | None = None) -> None: """ Helper to set status to failure Args: - message (str): Optional message related to the test - - Returns: - bool: Always true + message: Optional message related to the test """ - return self._set_status("failure", message) + self._set_status("failure", message) - def is_skipped(self, message: str = "") -> bool: + def is_skipped(self, message: str | None = None) -> None: """ Helper to set status to skipped Args: - message (str): Optional message related to the test - - Returns: - bool: Always true + message: Optional message related to the test """ - return self._set_status("skipped", message) + self._set_status("skipped", message) - def is_error(self, message: str = "") -> bool: + def is_error(self, message: str | None = None, exception: Exception | None = None) -> None: """ Helper to set status to error Args: - message (str): Optional message related to the test - - Returns: - bool: Always true + exception: Optional Exception objet related to the error """ - return self._set_status("error", message) + self._set_status("error", message) + self.error = exception - def _set_status(self, status: str, message: str = "") -> bool: + def _set_status(self, status: TestStatus, message: str | None = None) -> None: """ Set status and insert optional message Args: - status (str): status of the test - message (str): optional message - - Returns: - bool: Always true + status: status of the test + message: optional message """ self.result = status - if message != "": + if message is not None: self.messages.append(message) - return True def __str__(self) -> str: """ diff --git a/anta/runner.py b/anta/runner.py index ad892b017..3416b3cff 100644 --- a/anta/runner.py +++ b/anta/runner.py @@ -4,31 +4,25 @@ """ ANTA runner function """ +from __future__ import annotations import asyncio import itertools import logging -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import List, Optional, Tuple from anta.inventory import AntaInventory from anta.models import AntaTest from anta.result_manager import ResultManager -from anta.result_manager.models import TestResult from anta.tools.misc import anta_log_exception logger = logging.getLogger(__name__) -# Keys from the ANTA test catalog file tranferred to kwargs for AntaTest initialization. -TEST_CATALOG_PARAMS = [ - "result_overwrite", - "template_params", -] - async def main( manager: ResultManager, inventory: AntaInventory, - tests: List[Tuple[Callable[..., TestResult], Dict[Any, Any]]], + tests: List[Tuple[AntaTest, AntaTest.Input]], tags: Optional[List[str]] = None, established_only: bool = True, ) -> None: @@ -37,35 +31,15 @@ async def main( Use this as an entrypoint to the test framwork in your script. Args: - manager (ResultManager): ResultManager object to populate with the test results. - inventory (AntaInventory): AntaInventory object that includes the device(s). - tests (List[...]): ANTA test catalog. Output of anta.loader.parse_catalog(). - tags (Optional[List[str]]): List of tags to filter devices from the inventory. Defaults to None. - established_only (bool): Include only established device(s). Defaults to True. - - Example: - anta.tests.routing.bgp: - - VerifyBGPIPv4UnicastCount: - number: 3 - template_params: - - vrf: default - - anta.tests.connectivity: - - VerifyReachability: - result_overwrite: - categories: - - "Overwritten category 1" - description: "Test with overwritten description" - custom_field: "Test run by John Doe" - template_params: - - src: Loopback0 - dst: 10.1.0.1 + manager: ResultManager object to populate with the test results. + inventory: AntaInventory object that includes the device(s). + tests: ANTA test catalog. Output of anta.loader.parse_catalog(). + tags: List of tags to filter devices from the inventory. Defaults to None. + established_only: Include only established device(s). Defaults to True. Returns: any: ResultManager object gets updated with the test results. """ - # Accept 6 arguments here - # pylint: disable=R0913 await inventory.connect_inventory() @@ -74,19 +48,12 @@ async def main( coros = [] for device, test in itertools.product(inventory.get_inventory(established_only=established_only, tags=tags).values(), tests): - kwargs = {k: v for k, v in test[1].items() if k in TEST_CATALOG_PARAMS} - - if "result_overwrite" in kwargs: - result_overwrite = kwargs.pop("result_overwrite") - result_overwrite = {"result_" + k: v for k, v in result_overwrite.items()} - kwargs.update(result_overwrite) - - test_params = {k: v for k, v in test[1].items() if k not in TEST_CATALOG_PARAMS} - + test_class = test[0] + test_inputs = test[1] try: # Instantiate AntaTest object - test_instance = test[0](device=device, **kwargs) - coros.append(test_instance.test(eos_data=None, **test_params)) + test_instance = test_class(device=device, inputs=test_inputs) + coros.append(test_instance.test(eos_data=None)) except Exception as e: # pylint: disable=broad-exception-caught message = "Error when creating ANTA tests" anta_log_exception(e, message, logger) @@ -96,9 +63,10 @@ async def main( logger.info("Running ANTA tests...") res = await asyncio.gather(*coros, return_exceptions=True) + logger.debug(res) for r in res: if isinstance(r, Exception): message = "Error in main ANTA Runner" anta_log_exception(r, message, logger) - res.remove(r) - manager.add_test_results(res) + else: + manager.add_test_result(r) diff --git a/anta/tests/aaa.py b/anta/tests/aaa.py index fba4fd083..b8acda8fe 100644 --- a/anta/tests/aaa.py +++ b/anta/tests/aaa.py @@ -4,41 +4,17 @@ """ Test functions related to the EOS various AAA settings """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import List, Optional +from ipaddress import IPv4Address +from typing import List, Literal, Set +from anta.custom_types import AAAAuthMethod from anta.models import AntaCommand, AntaTest -def _check_group_methods(methods: List[str]) -> List[str]: - """ - Verifies if the provided methods in various AAA tests start with 'group'. - - Args: - methods: List of AAA methods. Methods should be in the right order. - """ - built_in_methods = ["local", "none", "logging"] - - return [f"group {method}" if method not in built_in_methods and not method.startswith("group ") else method for method in methods] - - -def _check_auth_type(auth_types: List[str], valid_auth_types: List[str]) -> None: - """ - Verifies if the provided auth types in various AAA tests are valid. - - Args: - auth_types: List of AAA auth types to validate. - valid_auth_types: List of valid AAA auth types to validate against. - """ - if len(auth_types) > len(valid_auth_types): - raise ValueError(f"Too many parameters provided in auth_types. Valid parameters are: {valid_auth_types}") - - for auth_type in auth_types: - if auth_type not in valid_auth_types: - raise ValueError(f"Wrong parameter provided in auth_types. Valid parameters are: {valid_auth_types}") - - class VerifyTacacsSourceIntf(AntaTest): """ Verifies TACACS source-interface for a specified VRF. @@ -46,7 +22,6 @@ class VerifyTacacsSourceIntf(AntaTest): Expected Results: * success: The test will pass if the provided TACACS source-interface is configured in the specified VRF. * failure: The test will fail if the provided TACACS source-interface is NOT configured in the specified VRF. - * skipped: The test will be skipped if source-interface or VRF is not provided. """ name = "VerifyTacacsSourceIntf" @@ -54,29 +29,22 @@ class VerifyTacacsSourceIntf(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show tacacs")] - @AntaTest.anta_test - def test(self, intf: Optional[str] = None, vrf: str = "default") -> None: - """ - Run VerifyTacacsSourceIntf validation. - - Args: - intf: Source-interface to use as source IP of TACACS messages. - vrf: The name of the VRF to transport TACACS messages. Defaults to 'default'. - """ - if not intf or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because intf or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + intf: str + """Source-interface to use as source IP of TACACS messages""" + vrf: str = "default" + """The name of the VRF to transport TACACS messages""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - try: - if command_output["srcIntf"][vrf] == intf: + if command_output["srcIntf"][self.inputs.vrf] == self.inputs.intf: self.result.is_success() else: - self.result.is_failure(f"Wrong source-interface configured in VRF {vrf}") - + self.result.is_failure(f"Wrong source-interface configured in VRF {self.inputs.vrf}") except KeyError: - self.result.is_failure(f"Source-interface {intf} is not configured in VRF {vrf}") + self.result.is_failure(f"Source-interface {self.inputs.intf} is not configured in VRF {self.inputs.vrf}") class VerifyTacacsServers(AntaTest): @@ -86,7 +54,6 @@ class VerifyTacacsServers(AntaTest): Expected Results: * success: The test will pass if the provided TACACS servers are configured in the specified VRF. * failure: The test will fail if the provided TACACS servers are NOT configured in the specified VRF. - * skipped: The test will be skipped if TACACS servers or VRF are not provided. """ name = "VerifyTacacsServers" @@ -94,37 +61,30 @@ class VerifyTacacsServers(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show tacacs")] - @AntaTest.anta_test - def test(self, servers: Optional[List[str]] = None, vrf: str = "default") -> None: - """ - Run VerifyTacacsServers validation. - - Args: - servers: List of TACACS servers IP addresses. - vrf: The name of the VRF to transport TACACS messages. Defaults to 'default'. - """ - if not servers or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because servers or vrf were not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + servers: List[IPv4Address] + """List of TACACS servers""" + vrf: str = "default" + """The name of the VRF to transport TACACS messages""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - tacacs_servers = command_output["tacacsServers"] - if not tacacs_servers: self.result.is_failure("No TACACS servers are configured") return - not_configured = [ - server - for server in servers - if not any(server == tacacs_server["serverInfo"]["hostname"] and vrf == tacacs_server["serverInfo"]["vrf"] for tacacs_server in tacacs_servers) + str(server) + for server in self.inputs.servers + if not any( + str(server) == tacacs_server["serverInfo"]["hostname"] and self.inputs.vrf == tacacs_server["serverInfo"]["vrf"] for tacacs_server in tacacs_servers + ) ] - if not not_configured: self.result.is_success() else: - self.result.is_failure(f"TACACS servers {not_configured} are not configured in VRF {vrf}") + self.result.is_failure(f"TACACS servers {not_configured} are not configured in VRF {self.inputs.vrf}") class VerifyTacacsServerGroups(AntaTest): @@ -134,7 +94,6 @@ class VerifyTacacsServerGroups(AntaTest): Expected Results: * success: The test will pass if the provided TACACS server group(s) are configured. * failure: The test will fail if one or all the provided TACACS server group(s) are NOT configured. - * skipped: The test will be skipped if TACACS server group(s) are not provided. """ name = "VerifyTacacsServerGroups" @@ -142,28 +101,18 @@ class VerifyTacacsServerGroups(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show tacacs")] - @AntaTest.anta_test - def test(self, groups: Optional[List[str]] = None) -> None: - """ - Run VerifyTacacsServerGroups validation. - - Args: - groups: List of TACACS server group. - """ - if not groups: - self.result.is_skipped(f"{self.__class__.name} did not run because groups were not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + groups: List[str] + """List of TACACS server group""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - tacacs_groups = command_output["groups"] - if not tacacs_groups: self.result.is_failure("No TACACS server group(s) are configured") return - - not_configured = [group for group in groups if group not in tacacs_groups] - + not_configured = [group for group in self.inputs.groups if group not in tacacs_groups] if not not_configured: self.result.is_success() else: @@ -177,7 +126,6 @@ class VerifyAuthenMethods(AntaTest): Expected Results: * success: The test will pass if the provided AAA authentication method list is matching in the configured authentication types. * failure: The test will fail if the provided AAA authentication method list is NOT matching in the configured authentication types. - * skipped: The test will be skipped if the AAA authentication method list or authentication type list are not provided. """ name = "VerifyAuthenMethods" @@ -185,46 +133,35 @@ class VerifyAuthenMethods(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show aaa methods authentication")] - @AntaTest.anta_test - def test(self, methods: Optional[List[str]] = None, auth_types: Optional[List[str]] = None) -> None: - """ - Run VerifyAuthenMethods validation. - - Args: - methods: List of AAA authentication methods. Methods should be in the right order. - auth_types: List of authentication types to verify. List elements must be: login, enable, dot1x. - """ - if not methods or not auth_types: - self.result.is_skipped(f"{self.__class__.name} did not run because methods or auth_types were not supplied") - return - - methods_with_group = _check_group_methods(methods) - - _check_auth_type(auth_types, ["login", "enable", "dot1x"]) + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + methods: List[AAAAuthMethod] + """List of AAA authentication methods. Methods should be in the right order""" + types: Set[Literal["login", "enable", "dot1x"]] + """List of authentication types to verify""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - not_matching = [] - - for auth_type in auth_types: - auth_type_key = f"{auth_type}AuthenMethods" - - if auth_type_key == "loginAuthenMethods": - if not command_output[auth_type_key].get("login"): + for k, v in command_output.items(): + auth_type = k.replace("AuthenMethods", "") + if auth_type not in self.inputs.types: + # We do not need to verify this accounting type + continue + if auth_type == "login": + if "login" not in v: self.result.is_failure("AAA authentication methods are not configured for login console") return - - if command_output[auth_type_key]["login"]["methods"] != methods_with_group: - self.result.is_failure(f"AAA authentication methods {methods} are not matching for login console") + if v["login"]["methods"] != self.inputs.methods: + self.result.is_failure(f"AAA authentication methods {self.inputs.methods} are not matching for login console") return - - if command_output[auth_type_key]["default"]["methods"] != methods_with_group: - not_matching.append(auth_type) - + for methods in v.values(): + if methods["methods"] != self.inputs.methods: + not_matching.append(auth_type) if not not_matching: self.result.is_success() else: - self.result.is_failure(f"AAA authentication methods {methods} are not matching for {not_matching}") + self.result.is_failure(f"AAA authentication methods {self.inputs.methods} are not matching for {not_matching}") class VerifyAuthzMethods(AntaTest): @@ -234,7 +171,6 @@ class VerifyAuthzMethods(AntaTest): Expected Results: * success: The test will pass if the provided AAA authorization method list is matching in the configured authorization types. * failure: The test will fail if the provided AAA authorization method list is NOT matching in the configured authorization types. - * skipped: The test will be skipped if the AAA authentication method list or authorization type list are not provided. """ name = "VerifyAuthzMethods" @@ -242,39 +178,28 @@ class VerifyAuthzMethods(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show aaa methods authorization")] - @AntaTest.anta_test - def test(self, methods: Optional[List[str]] = None, auth_types: Optional[List[str]] = None) -> None: - """ - Run VerifyAuthzMethods validation. - - Args: - methods: List of AAA authorization methods. Methods should be in the right order. - auth_types: List of authorization types to verify. List elements must be: commands, exec. - """ - if not methods or not auth_types: - self.result.is_skipped(f"{self.__class__.name} did not run because methods or auth_types were not supplied") - return - - _check_auth_type(auth_types, ["commands", "exec"]) - - methods_with_group = _check_group_methods(methods) + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + methods: List[AAAAuthMethod] + """List of AAA authorization methods. Methods should be in the right order""" + types: Set[Literal["commands", "exec"]] + """List of authorization types to verify""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - not_matching = [] - - for auth_type in auth_types: - auth_type_key = f"{auth_type}AuthzMethods" - - method_key = list(command_output[auth_type_key].keys())[0] - - if command_output[auth_type_key][method_key]["methods"] != methods_with_group: - not_matching.append(auth_type) - + for k, v in command_output.items(): + authz_type = k.replace("AuthzMethods", "") + if authz_type not in self.inputs.types: + # We do not need to verify this accounting type + continue + for methods in v.values(): + if methods["methods"] != self.inputs.methods: + not_matching.append(authz_type) if not not_matching: self.result.is_success() else: - self.result.is_failure(f"AAA authorization methods {methods} are not matching for {not_matching}") + self.result.is_failure(f"AAA authorization methods {self.inputs.methods} are not matching for {not_matching}") class VerifyAcctDefaultMethods(AntaTest): @@ -284,7 +209,6 @@ class VerifyAcctDefaultMethods(AntaTest): Expected Results: * success: The test will pass if the provided AAA accounting default method list is matching in the configured accounting types. * failure: The test will fail if the provided AAA accounting default method list is NOT matching in the configured accounting types. - * skipped: The test will be skipped if the AAA accounting default method list or accounting type list are not provided. """ name = "VerifyAcctDefaultMethods" @@ -292,47 +216,34 @@ class VerifyAcctDefaultMethods(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show aaa methods accounting")] - @AntaTest.anta_test - def test(self, methods: Optional[List[str]] = None, auth_types: Optional[List[str]] = None) -> None: - """ - Run VerifyAcctDefaultMethods validation. - - Args: - methods: List of AAA accounting default methods. Methods should be in the right order. - auth_types: List of accounting types to verify. List elements must be: commands, exec, system, dot1x. - """ - if not methods or not auth_types: - self.result.is_skipped(f"{self.__class__.name} did not run because methods or auth_types were not supplied") - return - - methods_with_group = _check_group_methods(methods) - - _check_auth_type(auth_types, ["system", "exec", "commands", "dot1x"]) + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + methods: List[AAAAuthMethod] + """List of AAA accounting methods. Methods should be in the right order""" + types: Set[Literal["commands", "exec", "system", "dot1x"]] + """List of accounting types to verify""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - not_matching = [] not_configured = [] - - for auth_type in auth_types: - auth_type_key = f"{auth_type}AcctMethods" - - method_key = list(command_output[auth_type_key].keys())[0] - - if not command_output[auth_type_key][method_key].get("defaultAction"): - not_configured.append(auth_type) - - if command_output[auth_type_key][method_key]["defaultMethods"] != methods_with_group: - not_matching.append(auth_type) - + for k, v in command_output.items(): + acct_type = k.replace("AcctMethods", "") + if acct_type not in self.inputs.types: + # We do not need to verify this accounting type + continue + for methods in v.values(): + if "defaultAction" not in methods: + not_configured.append(acct_type) + if methods["defaultMethods"] != self.inputs.methods: + not_matching.append(acct_type) if not_configured: self.result.is_failure(f"AAA default accounting is not configured for {not_configured}") return - if not not_matching: self.result.is_success() else: - self.result.is_failure(f"AAA accounting default methods {methods} are not matching for {not_matching}") + self.result.is_failure(f"AAA accounting default methods {self.inputs.methods} are not matching for {not_matching}") class VerifyAcctConsoleMethods(AntaTest): @@ -342,7 +253,6 @@ class VerifyAcctConsoleMethods(AntaTest): Expected Results: * success: The test will pass if the provided AAA accounting console method list is matching in the configured accounting types. * failure: The test will fail if the provided AAA accounting console method list is NOT matching in the configured accounting types. - * skipped: The test will be skipped if the AAA accounting console method list or accounting type list are not provided. """ name = "VerifyAcctConsoleMethods" @@ -350,44 +260,31 @@ class VerifyAcctConsoleMethods(AntaTest): categories = ["aaa"] commands = [AntaCommand(command="show aaa methods accounting")] - @AntaTest.anta_test - def test(self, methods: Optional[List[str]] = None, auth_types: Optional[List[str]] = None) -> None: - """ - Run VerifyAcctConsoleMethods validation. - - Args: - methods: List of AAA accounting console methods. Methods should be in the right order. - auth_types: List of accounting types to verify. List elements must be: commands, exec, system, dot1x. - """ - if not methods or not auth_types: - self.result.is_skipped(f"{self.__class__.name} did not run because methods or auth_types were not supplied") - return - - methods_with_group = _check_group_methods(methods) - - _check_auth_type(auth_types, ["system", "exec", "commands", "dot1x"]) + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + methods: List[AAAAuthMethod] + """List of AAA accounting console methods. Methods should be in the right order""" + types: Set[Literal["commands", "exec", "system", "dot1x"]] + """List of accounting console types to verify""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - not_matching = [] not_configured = [] - - for auth_type in auth_types: - auth_type_key = f"{auth_type}AcctMethods" - - method_key = list(command_output[auth_type_key].keys())[0] - - if not command_output[auth_type_key][method_key].get("consoleAction"): - not_configured.append(auth_type) - - if command_output[auth_type_key][method_key]["consoleMethods"] != methods_with_group: - not_matching.append(auth_type) - + for k, v in command_output.items(): + acct_type = k.replace("AcctMethods", "") + if acct_type not in self.inputs.types: + # We do not need to verify this accounting type + continue + for methods in v.values(): + if "consoleAction" not in methods: + not_configured.append(acct_type) + if methods["consoleMethods"] != self.inputs.methods: + not_matching.append(acct_type) if not_configured: self.result.is_failure(f"AAA console accounting is not configured for {not_configured}") return - if not not_matching: self.result.is_success() else: - self.result.is_failure(f"AAA accounting console methods {methods} are not matching for {not_matching}") + self.result.is_failure(f"AAA accounting console methods {self.inputs.methods} are not matching for {not_matching}") diff --git a/anta/tests/configuration.py b/anta/tests/configuration.py index 2ee5fb0ad..79ac5687c 100644 --- a/anta/tests/configuration.py +++ b/anta/tests/configuration.py @@ -4,9 +4,8 @@ """ Test functions related to the device configuration """ - -# pylint: disable = too-few-public-methods - +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations from anta.models import AntaCommand, AntaTest @@ -14,20 +13,17 @@ class VerifyZeroTouch(AntaTest): """ - Verifies ZeroTouch is disabled. + Verifies ZeroTouch is disabled """ name = "VerifyZeroTouch" - description = "Verifies ZeroTouch is disabled." + description = "Verifies ZeroTouch is disabled" categories = ["configuration"] commands = [AntaCommand(command="show zerotouch")] @AntaTest.anta_test def test(self) -> None: - """Run VerifyZeroTouch validation""" - command_output = self.instance_commands[0].output - assert isinstance(command_output, dict) if command_output["mode"] == "disabled": self.result.is_success() @@ -37,17 +33,16 @@ def test(self) -> None: class VerifyRunningConfigDiffs(AntaTest): """ - Verifies there is no difference between the running-config and the startup-config. + Verifies there is no difference between the running-config and the startup-config """ name = "VerifyRunningConfigDiffs" - description = "" + description = "Verifies there is no difference between the running-config and the startup-config" categories = ["configuration"] commands = [AntaCommand(command="show running-config diffs", ofmt="text")] @AntaTest.anta_test def test(self) -> None: - """Run VerifyRunningConfigDiffs validation""" command_output = self.instance_commands[0].output if command_output is None or command_output == "": self.result.is_success() diff --git a/anta/tests/connectivity.py b/anta/tests/connectivity.py index 7f9f54098..3b54da694 100644 --- a/anta/tests/connectivity.py +++ b/anta/tests/connectivity.py @@ -4,10 +4,21 @@ """ Test functions related to various connectivity checks """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations +from ipaddress import IPv4Address +from typing import TYPE_CHECKING, List, Union + +from pydantic import BaseModel + +from anta.custom_types import Interface from anta.models import AntaTemplate, AntaTest +if TYPE_CHECKING: + from anta.models import AntaCommand + class VerifyReachability(AntaTest): """ @@ -16,34 +27,39 @@ class VerifyReachability(AntaTest): Expected Results: * success: The test will pass if all destination IP(s) are reachable. * failure: The test will fail if one or many destination IP(s) are unreachable. - * error: The test will give an error if the destination IP(s) or the source interface/IP(s) are not provided as template_params. """ name = "VerifyReachability" description = "Test the network reachability to one or many destination IP(s)." categories = ["connectivity"] - template = AntaTemplate(template="ping {dst} source {src} repeat 2") + commands = [AntaTemplate(template="ping vrf {vrf} {destination} source {source} repeat 2")] + + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + hosts: List[Host] + """List of hosts to ping""" + + class Host(BaseModel): + """Remote host to ping""" + + destination: IPv4Address + """IPv4 address to ping""" + source: Union[IPv4Address, Interface] + """IPv4 address source IP or Egress interface to use""" + vrf: str = "default" + """VRF context""" + + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(destination=host.destination, source=host.source, vrf=host.vrf) for host in self.inputs.hosts] @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyReachability validation. - """ - failures = [] - for command in self.instance_commands: - if command.params and ("src" and "dst") in command.params: - src, dst = command.params["src"], command.params["dst"] - else: - self.result.is_error("The destination IP(s) or the source interface/IP(s) are not provided as template_params") - return - + if command.params and "source" in command.params and "destination" in command.params: + src, dst = command.params["source"], command.params["destination"] if "2 received" not in command.json_output["messages"][0]: - failures.append((src, dst)) - + failures.append((str(src), str(dst))) if not failures: self.result.is_success() - else: self.result.is_failure(f"Connectivity test failed for the following source-destination pairs: {failures}") diff --git a/anta/tests/field_notices.py b/anta/tests/field_notices.py index 06ce1821e..9b65459c6 100644 --- a/anta/tests/field_notices.py +++ b/anta/tests/field_notices.py @@ -27,9 +27,7 @@ class VerifyFieldNotice44Resolution(AntaTest): # TODO maybe implement ONLY ON PLATFORMS instead @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self) -> None: # type: ignore[override] - """Run VerifyFieldNotice44Resolution validation""" - + def test(self) -> None: command_output = self.instance_commands[0].json_output devices = [ @@ -117,9 +115,7 @@ class VerifyFieldNotice72Resolution(AntaTest): # TODO maybe implement ONLY ON PLATFORMS instead @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self) -> None: # type: ignore[override] - """Run VerifyFieldNotice72Resolution validation""" - + def test(self) -> None: command_output = self.instance_commands[0].json_output devices = ["DCS-7280SR3-48YC8", "DCS-7280SR3K-48YC8"] @@ -165,5 +161,5 @@ def test(self) -> None: # type: ignore[override] self.result.is_success("FN72 is mitigated") return # We should never hit this point - self.result.is_error("Error in running test - FixedSystemvrm1 not found") + self.result.is_error(message="Error in running test - FixedSystemvrm1 not found") return diff --git a/anta/tests/hardware.py b/anta/tests/hardware.py index 23a47f44b..5bf95f742 100644 --- a/anta/tests/hardware.py +++ b/anta/tests/hardware.py @@ -4,9 +4,11 @@ """ Test functions related to the hardware or environment """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import List, Optional +from typing import List from anta.decorators import skip_on_platforms from anta.models import AntaCommand, AntaTest @@ -19,7 +21,6 @@ class VerifyTransceiversManufacturers(AntaTest): Expected Results: * success: The test will pass if all transceivers are from approved manufacturers. * failure: The test will fail if some transceivers are from unapproved manufacturers. - * skipped: The test will be skipped if a list of approved manufacturers is not provided or if it's run on a virtualized platform. """ name = "VerifyTransceiversManufacturers" @@ -27,21 +28,17 @@ class VerifyTransceiversManufacturers(AntaTest): categories = ["hardware"] commands = [AntaCommand(command="show inventory", ofmt="json")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + manufacturers: List[str] + """List of approved transceivers manufacturers""" + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self, manufacturers: Optional[List[str]] = None) -> None: - """ - Run VerifyTransceiversManufacturers validation - - Args: - manufacturers: List of approved transceivers manufacturers. - """ - if not manufacturers: - self.result.is_skipped(f"{self.__class__.name} was not run because manufacturers list was not provided") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output - wrong_manufacturers = {interface: value["mfgName"] for interface, value in command_output["xcvrSlots"].items() if value["mfgName"] not in manufacturers} + wrong_manufacturers = { + interface: value["mfgName"] for interface, value in command_output["xcvrSlots"].items() if value["mfgName"] not in self.inputs.manufacturers + } if not wrong_manufacturers: self.result.is_success() else: @@ -55,7 +52,6 @@ class VerifyTemperature(AntaTest): Expected Results: * success: The test will pass if the device temperature is currently OK: 'temperatureOk'. * failure: The test will fail if the device temperature is NOT OK. - * skipped: Test test will be skipped if it's run on a virtualized platform. """ name = "VerifyTemperature" @@ -66,9 +62,6 @@ class VerifyTemperature(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyTemperature validation - """ command_output = self.instance_commands[0].json_output temperature_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" if temperature_status == "temperatureOk": @@ -84,7 +77,6 @@ class VerifyTransceiversTemperature(AntaTest): Expected Results: * success: The test will pass if all transceivers status are OK: 'ok'. * failure: The test will fail if some transceivers are NOT OK. - * skipped: Test test will be skipped if it's run on a virtualized platform. """ name = "VerifyTransceiversTemperature" @@ -95,9 +87,6 @@ class VerifyTransceiversTemperature(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyTransceiversTemperature validation - """ command_output = self.instance_commands[0].json_output sensors = command_output["tempSensors"] if "tempSensors" in command_output.keys() else "" wrong_sensors = { @@ -121,7 +110,6 @@ class VerifyEnvironmentSystemCooling(AntaTest): Expected Results: * success: The test will pass if the system cooling status is OK: 'coolingOk'. * failure: The test will fail if the system cooling status is NOT OK. - * skipped: The test will be skipped if it's run on a virtualized platform. """ name = "VerifyEnvironmentSystemCooling" @@ -132,13 +120,8 @@ class VerifyEnvironmentSystemCooling(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyEnvironmentCooling validation - """ - command_output = self.instance_commands[0].json_output sys_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" - self.result.is_success() if sys_status != "coolingOk": self.result.is_failure(f"Device system cooling is not OK: '{sys_status}'") @@ -151,7 +134,6 @@ class VerifyEnvironmentCooling(AntaTest): Expected Results: * success: The test will pass if the fans status are within the accepted states list. * failure: The test will fail if some fans status is not within the accepted states list. - * skipped: The test will be skipped if the accepted states list is not provided or if it's run on a virtualized platform. """ name = "VerifyEnvironmentCooling" @@ -159,30 +141,24 @@ class VerifyEnvironmentCooling(AntaTest): categories = ["hardware"] commands = [AntaCommand(command="show system environment cooling", ofmt="json")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + states: List[str] + """Accepted states list for fan status""" + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self, accepted_states: Optional[List[str]] = None) -> None: - """ - Run VerifyEnvironmentCooling validation - - Args: - accepted_states: Accepted states list for fan status. - """ - if not accepted_states: - self.result.is_skipped(f"{self.__class__.name} was not run because accepted_states list was not provided") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output self.result.is_success() # First go through power supplies fans for power_supply in command_output.get("powerSupplySlots", []): for fan in power_supply.get("fans", []): - if (state := fan["status"]) not in accepted_states: + if (state := fan["status"]) not in self.inputs.states: self.result.is_failure(f"Fan {fan['label']} on PowerSupply {power_supply['label']} is: '{state}'") # Then go through fan trays for fan_tray in command_output.get("fanTraySlots", []): for fan in fan_tray.get("fans", []): - if (state := fan["status"]) not in accepted_states: + if (state := fan["status"]) not in self.inputs.states: self.result.is_failure(f"Fan {fan['label']} on Fan Tray {fan_tray['label']} is: '{state}'") @@ -193,7 +169,6 @@ class VerifyEnvironmentPower(AntaTest): Expected Results: * success: The test will pass if the power supplies status are within the accepted states list. * failure: The test will fail if some power supplies status is not within the accepted states list. - * skipped: The test will be skipped if the accepted states list is not provided or if it's run on a virtualized platform. """ name = "VerifyEnvironmentPower" @@ -201,23 +176,17 @@ class VerifyEnvironmentPower(AntaTest): categories = ["hardware"] commands = [AntaCommand(command="show system environment power", ofmt="json")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + states: List[str] + """Accepted states list for power supplies status""" + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self, accepted_states: Optional[List[str]] = None) -> None: - """ - Run VerifyEnvironmentPower validation - - Args: - accepted_states: Accepted states list for power supplies status. - """ - if not accepted_states: - self.result.is_skipped(f"{self.__class__.name} was not run because accepted_states list was not provided") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output power_supplies = command_output["powerSupplies"] if "powerSupplies" in command_output.keys() else "{}" wrong_power_supplies = { - powersupply: {"state": value["state"]} for powersupply, value in dict(power_supplies).items() if value["state"] not in accepted_states + powersupply: {"state": value["state"]} for powersupply, value in dict(power_supplies).items() if value["state"] not in self.inputs.states } if not wrong_power_supplies: self.result.is_success() @@ -232,7 +201,6 @@ class VerifyAdverseDrops(AntaTest): Expected Results: * success: The test will pass if there are no adverse drops. * failure: The test will fail if there are adverse drops. - * skipped: The test will be skipped if it's run on a virtualized platform. """ name = "VerifyAdverseDrops" @@ -243,9 +211,6 @@ class VerifyAdverseDrops(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyAdverseDrops validation - """ command_output = self.instance_commands[0].json_output total_adverse_drop = command_output["totalAdverseDrops"] if "totalAdverseDrops" in command_output.keys() else "" if total_adverse_drop == 0: diff --git a/anta/tests/interfaces.py b/anta/tests/interfaces.py index 09f65dc0f..bf146ff2b 100644 --- a/anta/tests/interfaces.py +++ b/anta/tests/interfaces.py @@ -4,9 +4,14 @@ """ Test functions related to the device interfaces """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations import re -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List + +from pydantic import conint from anta.decorators import skip_on_platforms from anta.models import AntaCommand, AntaTemplate, AntaTest @@ -25,10 +30,7 @@ class VerifyInterfaceUtilization(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyInterfaceUtilization validation""" - command_output = self.instance_commands[0].text_output - wrong_interfaces = {} for line in command_output.split("\n")[1:]: if len(line) > 0: @@ -38,7 +40,6 @@ def test(self) -> None: wrong_interfaces[line.split()[0]] = line.split()[-5] elif float(line.split()[-2].replace("%", "")) > 75.0: wrong_interfaces[line.split()[0]] = line.split()[-2] - if not wrong_interfaces: self.result.is_success() else: @@ -61,12 +62,8 @@ class VerifyInterfaceErrors(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyInterfaceErrors validation - """ command_output = self.instance_commands[0].json_output - - wrong_interfaces: List[Dict[str, Dict[str, int]]] = [] + wrong_interfaces: list[dict[str, dict[str, int]]] = [] for interface, counters in command_output["interfaceErrorCounters"].items(): if any(value > 0 for value in counters.values()) and not any(interface in wrong_interface for wrong_interface in wrong_interfaces): wrong_interfaces.append({interface: counters}) @@ -88,12 +85,8 @@ class VerifyInterfaceDiscards(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyInterfaceDiscards validation""" - command_output = self.instance_commands[0].json_output - - wrong_interfaces: List[Dict[str, Dict[str, int]]] = [] - + wrong_interfaces: list[dict[str, dict[str, int]]] = [] for interface, outer_v in command_output["interfaces"].items(): wrong_interfaces.extend({interface: outer_v} for counter, value in outer_v.items() if value > 0) if not wrong_interfaces: @@ -114,12 +107,8 @@ class VerifyInterfaceErrDisabled(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyInterfaceErrDisabled validation""" - command_output = self.instance_commands[0].json_output - errdisabled_interfaces = [interface for interface, value in command_output["interfaceStatuses"].items() if value["linkStatus"] == "errdisabled"] - if errdisabled_interfaces: self.result.is_failure(f"The following interfaces are in error disabled state: {errdisabled_interfaces}") else: @@ -136,24 +125,15 @@ class VerifyInterfacesStatus(AntaTest): categories = ["interfaces"] commands = [AntaCommand(command="show interfaces description")] - @AntaTest.anta_test - def test(self, minimum: Optional[int] = None) -> None: - """ - Run VerifyInterfacesStatus validation - - Args: - minimum: Expected minimum number of Ethernet interfaces up/up. - """ - - if minimum is None or minimum < 0: - self.result.is_skipped(f"VerifyInterfacesStatus was not run as an invalid minimum value was given {minimum}.") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + minimum: conint(ge=0) # type: ignore + """Expected minimum number of Ethernet interfaces up/up""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - count_up_up = 0 other_ethernet_interfaces = [] - for interface in command_output["interfaceDescriptions"]: interface_dict = command_output["interfaceDescriptions"][interface] if "Ethernet" in interface: @@ -161,11 +141,10 @@ def test(self, minimum: Optional[int] = None) -> None: count_up_up += 1 else: other_ethernet_interfaces.append(interface) - - if count_up_up >= minimum: + if count_up_up >= self.inputs.minimum: self.result.is_success() else: - self.result.is_failure(f"Only {count_up_up}, less than {minimum} Ethernet interfaces are UP/UP") + self.result.is_failure(f"Only {count_up_up}, less than {self.inputs.minimum} Ethernet interfaces are UP/UP") self.result.messages.append(f"The following Ethernet interfaces are not UP/UP: {other_ethernet_interfaces}") @@ -182,17 +161,13 @@ class VerifyStormControlDrops(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """Run VerifyStormControlDrops validation""" - command_output = self.instance_commands[0].json_output - - storm_controlled_interfaces: Dict[str, Dict[str, Any]] = {} + storm_controlled_interfaces: dict[str, dict[str, Any]] = {} for interface, interface_dict in command_output["interfaces"].items(): for traffic_type, traffic_type_dict in interface_dict["trafficTypes"].items(): if "drop" in traffic_type_dict and traffic_type_dict["drop"] != 0: storm_controlled_interface_dict = storm_controlled_interfaces.setdefault(interface, {}) storm_controlled_interface_dict.update({traffic_type: traffic_type_dict["drop"]}) - if not storm_controlled_interfaces: self.result.is_success() else: @@ -212,15 +187,11 @@ class VerifyPortChannels(AntaTest): @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """Run VerifyPortChannels validation""" - command_output = self.instance_commands[0].json_output - - po_with_invactive_ports: List[Dict[str, str]] = [] + po_with_invactive_ports: list[dict[str, str]] = [] for portchannel, portchannel_dict in command_output["portChannels"].items(): if len(portchannel_dict["inactivePorts"]) != 0: po_with_invactive_ports.extend({portchannel: portchannel_dict["inactivePorts"]}) - if not po_with_invactive_ports: self.result.is_success() else: @@ -239,16 +210,12 @@ class VerifyIllegalLACP(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyIllegalLACP validation""" - command_output = self.instance_commands[0].json_output - - po_with_illegal_lacp: List[Dict[str, Dict[str, int]]] = [] + po_with_illegal_lacp: list[dict[str, dict[str, int]]] = [] for portchannel, portchannel_dict in command_output["portChannels"].items(): po_with_illegal_lacp.extend( {portchannel: interface} for interface, interface_dict in portchannel_dict["interfaces"].items() if interface_dict["illegalRxCount"] != 0 ) - if not po_with_illegal_lacp: self.result.is_success() else: @@ -265,37 +232,27 @@ class VerifyLoopbackCount(AntaTest): categories = ["interfaces"] commands = [AntaCommand(command="show ip interface brief")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None) -> None: - """ - Run VerifyLoopbackCount validation - - Args: - number: Number of loopback interfaces expected to be present. - """ - - if number is None: - self.result.is_skipped("VerifyLoopbackCount was not run as no number value was given.") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type: ignore + """Number of loopback interfaces expected to be present""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - loopback_count = 0 down_loopback_interfaces = [] - for interface in command_output["interfaces"]: interface_dict = command_output["interfaces"][interface] if "Loopback" in interface: loopback_count += 1 if not (interface_dict["lineProtocolStatus"] == "up" and interface_dict["interfaceStatus"] == "connected"): down_loopback_interfaces.append(interface) - - if loopback_count == number and len(down_loopback_interfaces) == 0: + if loopback_count == self.inputs.number and len(down_loopback_interfaces) == 0: self.result.is_success() else: self.result.is_failure() - if loopback_count != number: - self.result.is_failure(f"Found {loopback_count} Loopbacks when expecting {number}") + if loopback_count != self.inputs.number: + self.result.is_failure(f"Found {loopback_count} Loopbacks when expecting {self.inputs.number}") elif len(down_loopback_interfaces) != 0: self.result.is_failure(f"The following Loopbacks are not up: {down_loopback_interfaces}") @@ -312,18 +269,13 @@ class VerifySVI(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifySVI validation""" - command_output = self.instance_commands[0].json_output - down_svis = [] - for interface in command_output["interfaces"]: interface_dict = command_output["interfaces"][interface] if "Vlan" in interface: if not (interface_dict["lineProtocolStatus"] == "up" and interface_dict["interfaceStatus"] == "connected"): down_svis.append(interface) - if len(down_svis) == 0: self.result.is_success() else: @@ -331,19 +283,15 @@ def test(self) -> None: class VerifyL3MTU(AntaTest): - """ Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 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 and also an MTU per interface and also ignored some interfaces. - Default ignored interfaces: ["Management", "Loopback", "Vxlan", "Tunnel"] - Expected Results: * success: The test will pass if all layer 3 interfaces have the proper MTU configured. * failure: The test will fail if one or many layer 3 interfaces have the wrong MTU configured. - * skipped: The test will be skipped if the MTU value is not provided. """ name = "VerifyL3MTU" @@ -351,50 +299,31 @@ class VerifyL3MTU(AntaTest): categories = ["interfaces"] commands = [AntaCommand(command="show interfaces")] - NOT_SUPPORTED_INTERFACES: List[str] = ["Management", "Loopback", "Vxlan", "Tunnel"] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + mtu: int = 1500 + """Default MTU we should have configured on all non-excluded interfaces""" + ignored_interfaces: List[str] = ["Management", "Loopback", "Vxlan", "Tunnel"] + """A list of L3 interfaces to ignore""" + specific_mtu: List[Dict[str, int]] = [] + """A list of dictionary of L3 interfaces with their specific MTU configured""" @AntaTest.anta_test - def test(self, mtu: int = 1500, ignored_interfaces: Optional[List[str]] = None, specific_mtu: Optional[List[Dict[str, int]]] = None) -> None: - """ - Verifies the global L3 Maximum Transfer Unit (MTU) for 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 and also an MTU per interface and also ignored some interfaces. - - Args: - mtu (int, optional): Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500. - ignored_interfaces (List[str]): A list of L3 interfaces to ignore. It will replace the built-in exclusion. - specific_mtu (Optional[List[Dict[str, int]]]): A list of dictionary of L3 interfaces with their specific MTU configured. - """ - if not mtu: - self.result.is_skipped(f"{self.__class__.name} did not run because mtu was not supplied") - return - - if ignored_interfaces is None: - ignored_interfaces = self.NOT_SUPPORTED_INTERFACES - + def test(self) -> None: # Parameter to save incorrect interface settings - wrong_l3mtu_intf: List[Dict[str, int]] = [] - + wrong_l3mtu_intf: list[dict[str, int]] = [] command_output = self.instance_commands[0].json_output - # Set list of interfaces with specific settings - specific_interfaces: List[str] = [] - if specific_mtu is not None: - for d in specific_mtu: + specific_interfaces: list[str] = [] + if self.inputs.specific_mtu: + for d in self.inputs.specific_mtu: specific_interfaces.extend(d) - # Set default value if there is no specific settings. - else: - specific_mtu = [] - for interface, values in command_output["interfaces"].items(): - if re.findall(r"[a-z]+", interface, re.IGNORECASE)[0] not in ignored_interfaces and values["forwardingModel"] == "routed": + 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: - wrong_l3mtu_intf.extend({interface: values["mtu"]} for custom_data in specific_mtu if values["mtu"] != custom_data[interface]) + wrong_l3mtu_intf.extend({interface: values["mtu"]} for custom_data in self.inputs.specific_mtu if values["mtu"] != custom_data[interface]) # Comparison with generic setting - elif values["mtu"] != mtu: + elif values["mtu"] != self.inputs.mtu: wrong_l3mtu_intf.append({interface: values["mtu"]}) - if wrong_l3mtu_intf: self.result.is_failure(f"Some interfaces do not have correct MTU configured:\n{wrong_l3mtu_intf}") else: @@ -408,49 +337,44 @@ class VerifyIPProxyARP(AntaTest): Expected Results: * success: The test will pass if Proxy-ARP is enabled on the specified interface(s). * failure: The test will fail if Proxy-ARP is disabled on the specified interface(s). - * error: The test will give an error if a list of interface(s) is not provided as template_params. - """ name = "VerifyIPProxyARP" description = "Verifies if Proxy-ARP is enabled for the provided list of interface(s)." categories = ["interfaces"] - template = AntaTemplate(template="show ip interface {intf}") + commands = [AntaTemplate(template="show ip interface {intf}")] + + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + interfaces: List[str] + """List of interfaces to be tested""" + + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(intf=intf) for intf in self.inputs.interfaces] @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyIPProxyARP validation. - """ - disabled_intf = [] for command in self.instance_commands: if command.params and "intf" in command.params: intf = command.params["intf"] if not command.json_output["interfaces"][intf]["proxyArp"]: disabled_intf.append(intf) - if disabled_intf: self.result.is_failure(f"The following interface(s) have Proxy-ARP disabled: {disabled_intf}") - else: self.result.is_success() class VerifyL2MTU(AntaTest): - """ Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 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. - Default ignored interfaces: ["Management", "Loopback", "Tunnel", "Vxlan"] - Expected Results: * success: The test will pass if all layer 2 interfaces have the proper MTU configured. * failure: The test will fail if one or many layer 2 interfaces have the wrong MTU configured. - * skipped: The test will be skipped if the MTU value is not provided. """ name = "VerifyL2MTU" @@ -458,50 +382,31 @@ class VerifyL2MTU(AntaTest): categories = ["interfaces"] commands = [AntaCommand(command="show interfaces")] - NOT_SUPPORTED_INTERFACES: List[str] = ["Management", "Loopback", "Tunnel", "Vxlan"] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + mtu: int = 9214 + """Default MTU we should have configured on all non-excluded interfaces""" + ignored_interfaces: List[str] = ["Management", "Loopback", "Vxlan", "Tunnel"] + """A list of L2 interfaces to ignore""" + specific_mtu: List[Dict[str, int]] = [] + """A list of dictionary of L2 interfaces with their specific MTU configured""" @AntaTest.anta_test - def test(self, mtu: int = 9214, ignored_interfaces: Optional[List[str]] = None, specific_mtu: Optional[List[Dict[str, int]]] = None) -> None: - """ - Verifies the global L2 Maximum Transfer Unit (MTU) for 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. - - Args: - mtu (int, optional): Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214. - ignored_interfaces (List[str]): A list of L2 interfaces to ignore. It will replace the built-in exclusion. - specific_mtu (Optional[List[Dict[str, int]]]): A list of dictionary of L2 interfaces with their specific MTU configured. - """ - if not mtu: - self.result.is_skipped(f"{self.__class__.name} did not run because mtu was not supplied") - return - - if ignored_interfaces is None: - ignored_interfaces = self.NOT_SUPPORTED_INTERFACES - + def test(self) -> None: # Parameter to save incorrect interface settings - wrong_l2mtu_intf: List[Dict[str, int]] = [] - + wrong_l2mtu_intf: list[dict[str, int]] = [] command_output = self.instance_commands[0].json_output - # Set list of interfaces with specific settings - specific_interfaces: List[str] = [] - if specific_mtu is not None: - for d in specific_mtu: + specific_interfaces: list[str] = [] + if self.inputs.specific_mtu: + for d in self.inputs.specific_mtu: specific_interfaces.extend(d) - # Set default value if there is no specific settings. - else: - specific_mtu = [] - for interface, values in command_output["interfaces"].items(): - if re.findall(r"[a-z]+", interface, re.IGNORECASE)[0] not in ignored_interfaces and values["forwardingModel"] == "bridged": + if re.findall(r"[a-z]+", interface, re.IGNORECASE)[0] not in self.inputs.ignored_interfaces and values["forwardingModel"] == "bridged": if interface in specific_interfaces: - wrong_l2mtu_intf.extend({interface: values["mtu"]} for custom_data in specific_mtu if values["mtu"] != custom_data[interface]) + wrong_l2mtu_intf.extend({interface: values["mtu"]} for custom_data in self.inputs.specific_mtu if values["mtu"] != custom_data[interface]) # Comparison with generic setting - elif values["mtu"] != mtu: + elif values["mtu"] != self.inputs.mtu: wrong_l2mtu_intf.append({interface: values["mtu"]}) - if wrong_l2mtu_intf: self.result.is_failure(f"Some L2 interfaces do not have correct MTU configured:\n{wrong_l2mtu_intf}") else: diff --git a/anta/tests/logging.py b/anta/tests/logging.py index 2b5f74e7c..b52628f0f 100644 --- a/anta/tests/logging.py +++ b/anta/tests/logging.py @@ -6,11 +6,14 @@ NOTE: 'show logging' does not support json output yet """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations import logging import re -from typing import List, Optional +from ipaddress import IPv4Address +from typing import List from anta.models import AntaCommand, AntaTest @@ -47,21 +50,14 @@ class VerifyLoggingPersistent(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyLoggingPersistent validation. - """ self.result.is_success() - log_output = self.instance_commands[0].text_output dir_flash_output = self.instance_commands[1].text_output - if "Persistent logging: disabled" in _get_logging_states(self.logger, log_output): self.result.is_failure("Persistent logging is disabled") return - pattern = r"-rw-\s+(\d+)" persist_logs = re.search(pattern, dir_flash_output) - if not persist_logs or int(persist_logs.group(1)) == 0: self.result.is_failure("No persistent logs are saved in flash") @@ -73,7 +69,6 @@ class VerifyLoggingSourceIntf(AntaTest): Expected Results: * success: The test will pass if the provided logging source-interface is configured in the specified VRF. * failure: The test will fail if the provided logging source-interface is NOT configured in the specified VRF. - * skipped: The test will be skipped if source-interface or VRF is not provided. """ name = "VerifyLoggingSourceInt" @@ -81,27 +76,20 @@ class VerifyLoggingSourceIntf(AntaTest): categories = ["logging"] commands = [AntaCommand(command="show logging", ofmt="text")] - @AntaTest.anta_test - def test(self, intf: Optional[str] = None, vrf: str = "default") -> None: - """ - Run VerifyLoggingSrcDst validation. - - Args: - intf: Source-interface to use as source IP of log messages. - vrf: The name of the VRF to transport log messages. Defaults to 'default'. - """ - if not intf or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because intf or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + interface: str + """Source-interface to use as source IP of log messages""" + vrf: str = "default" + """The name of the VRF to transport log messages""" + @AntaTest.anta_test + def test(self) -> None: output = self.instance_commands[0].text_output - - pattern = rf"Logging source-interface '{intf}'.*VRF {vrf}" - + pattern = rf"Logging source-interface '{self.inputs.interface}'.*VRF {self.inputs.vrf}" if re.search(pattern, _get_logging_states(self.logger, output)): self.result.is_success() else: - self.result.is_failure(f"Source-interface '{intf}' is not configured in VRF {vrf}") + self.result.is_failure(f"Source-interface '{self.inputs.interface}' is not configured in VRF {self.inputs.vrf}") class VerifyLoggingHosts(AntaTest): @@ -111,7 +99,6 @@ class VerifyLoggingHosts(AntaTest): Expected Results: * success: The test will pass if the provided syslog servers are configured in the specified VRF. * failure: The test will fail if the provided syslog servers are NOT configured in the specified VRF. - * skipped: The test will be skipped if syslog servers or VRF are not provided. """ name = "VerifyLoggingHosts" @@ -119,32 +106,25 @@ class VerifyLoggingHosts(AntaTest): categories = ["logging"] commands = [AntaCommand(command="show logging", ofmt="text")] - @AntaTest.anta_test - def test(self, hosts: Optional[List[str]] = None, vrf: str = "default") -> None: - """ - Run VerifyLoggingHosts validation. - - Args: - hosts: List of hosts (syslog servers) IP addresses. - vrf: The name of the VRF to transport log messages. Defaults to 'default'. - """ - if not hosts or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because hosts or vrf were not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + hosts: List[IPv4Address] + """List of hosts (syslog servers) IP addresses""" + vrf: str = "default" + """The name of the VRF to transport log messages""" + @AntaTest.anta_test + def test(self) -> None: output = self.instance_commands[0].text_output - not_configured = [] - - for host in hosts: - pattern = rf"Logging to '{host}'.*VRF {vrf}" + for host in self.inputs.hosts: + pattern = rf"Logging to '{str(host)}'.*VRF {self.inputs.vrf}" if not re.search(pattern, _get_logging_states(self.logger, output)): - not_configured.append(host) + not_configured.append(str(host)) if not not_configured: self.result.is_success() else: - self.result.is_failure(f"Syslog servers {not_configured} are not configured in VRF {vrf}") + self.result.is_failure(f"Syslog servers {not_configured} are not configured in VRF {self.inputs.vrf}") class VerifyLoggingLogsGeneration(AntaTest): @@ -166,19 +146,13 @@ class VerifyLoggingLogsGeneration(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyLoggingLogs validation. - """ log_pattern = r"ANTA VerifyLoggingLogsGeneration validation" - output = self.instance_commands[1].text_output lines = output.strip().split("\n")[::-1] - for line in lines: if re.search(log_pattern, line): self.result.is_success() return - self.result.is_failure("Logs are not generated") @@ -202,22 +176,16 @@ class VerifyLoggingHostname(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyLoggingHostname validation. - """ output_hostname = self.instance_commands[0].json_output output_logging = self.instance_commands[2].text_output fqdn = output_hostname["fqdn"] lines = output_logging.strip().split("\n")[::-1] - log_pattern = r"ANTA VerifyLoggingHostname validation" - last_line_with_pattern = "" for line in lines: if re.search(log_pattern, line): last_line_with_pattern = line break - if fqdn in last_line_with_pattern: self.result.is_success() else: @@ -243,22 +211,15 @@ class VerifyLoggingTimestamp(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyLoggingTimestamp validation. - """ log_pattern = r"ANTA VerifyLoggingTimestamp validation" timestamp_pattern = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}-\d{2}:\d{2}" - output = self.instance_commands[1].text_output - lines = output.strip().split("\n")[::-1] - last_line_with_pattern = "" for line in lines: if re.search(log_pattern, line): last_line_with_pattern = line break - if re.search(timestamp_pattern, last_line_with_pattern): self.result.is_success() else: @@ -281,12 +242,8 @@ class VerifyLoggingAccounting(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyLoggingAccountingvalidation. - """ pattern = r"cmd=show aaa accounting logs" output = self.instance_commands[0].text_output - if re.search(pattern, output): self.result.is_success() else: diff --git a/anta/tests/mlag.py b/anta/tests/mlag.py index 444aa1e38..5fcd06661 100644 --- a/anta/tests/mlag.py +++ b/anta/tests/mlag.py @@ -4,7 +4,11 @@ """ Test functions related to Multi-chassis Link Aggregation (MLAG) """ -from typing import Optional +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations + +from pydantic import conint from anta.models import AntaCommand, AntaTest from anta.tools.get_value import get_value @@ -29,19 +33,12 @@ class VerifyMlagStatus(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyMlagStatus validation - """ - command_output = self.instance_commands[0].json_output - if command_output["state"] == "disabled": self.result.is_skipped("MLAG is disabled") return - keys_to_verify = ["state", "negStatus", "localIntfStatus", "peerLinkStatus"] verified_output = {key: get_value(command_output, key) for key in keys_to_verify} - if ( verified_output["state"] == "active" and verified_output["negStatus"] == "connected" @@ -70,16 +67,10 @@ class VerifyMlagInterfaces(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyMlagInterfaces validation - """ - command_output = self.instance_commands[0].json_output - if command_output["state"] == "disabled": self.result.is_skipped("MLAG is disabled") return - if command_output["mlagPorts"]["Inactive"] == 0 and command_output["mlagPorts"]["Active-partial"] == 0: self.result.is_success() else: @@ -104,23 +95,15 @@ class VerifyMlagConfigSanity(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyMlagConfigSanity validation - """ - command_output = self.instance_commands[0].json_output - if (mlag_status := get_value(command_output, "mlagActive")) is None: - self.result.is_error("Incorrect JSON response - 'mlagActive' state was not found") + self.result.is_error(message="Incorrect JSON response - 'mlagActive' state was not found") return - if mlag_status is False: self.result.is_skipped("MLAG is disabled") return - keys_to_verify = ["globalConfiguration", "interfaceConfiguration"] verified_output = {key: get_value(command_output, key) for key in keys_to_verify} - if not any(verified_output.values()): self.result.is_success() else: @@ -134,7 +117,7 @@ class VerifyMlagReloadDelay(AntaTest): Expected Results: * success: The test will pass if the reload-delay parameters are configured properly. * failure: The test will fail if the reload-delay parameters are NOT configured properly. - * skipped: The test will be skipped if the reload-delay parameters are NOT provided or if MLAG is 'disabled'. + * skipped: The test will be skipped if MLAG is 'disabled'. """ name = "VerifyMlagReloadDelay" @@ -142,30 +125,21 @@ class VerifyMlagReloadDelay(AntaTest): categories = ["mlag"] commands = [AntaCommand(command="show mlag", ofmt="json")] - @AntaTest.anta_test - def test(self, reload_delay: Optional[int] = None, reload_delay_non_mlag: Optional[int] = None) -> None: - """ - Run VerifyMlagReloadDelay validation - - Args: - reload_delay: Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled. - reload_delay_non_mlag: Delay (seconds) after reboot until ports that are not part of an MLAG are enabled. - """ - - if not reload_delay or not reload_delay_non_mlag: - self.result.is_skipped(f"{self.__class__.name} did not run because reload_delay or reload_delay_non_mlag were not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + reload_delay: conint(ge=0) # type: ignore + """Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled""" + reload_delay_non_mlag: conint(ge=0) # type: ignore + """Delay (seconds) after reboot until ports that are not part of an MLAG are enabled""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - if command_output["state"] == "disabled": self.result.is_skipped("MLAG is disabled") return - keys_to_verify = ["reloadDelay", "reloadDelayNonMlag"] verified_output = {key: get_value(command_output, key) for key in keys_to_verify} - - if verified_output["reloadDelay"] == reload_delay and verified_output["reloadDelayNonMlag"] == reload_delay_non_mlag: + if verified_output["reloadDelay"] == self.inputs.reload_delay and verified_output["reloadDelayNonMlag"] == self.inputs.reload_delay_non_mlag: self.result.is_success() else: @@ -179,7 +153,7 @@ class VerifyMlagDualPrimary(AntaTest): Expected Results: * success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly. * failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly. - * skipped: The test will be skipped if the dual-primary parameters are NOT provided or if MLAG is 'disabled'. + * skipped: The test will be skipped if MLAG is 'disabled'. """ name = "VerifyMlagDualPrimary" @@ -187,48 +161,34 @@ class VerifyMlagDualPrimary(AntaTest): categories = ["mlag"] commands = [AntaCommand(command="show mlag detail", ofmt="json")] - @AntaTest.anta_test - def test( - self, detection_delay: Optional[int] = None, errdisabled: bool = False, recovery_delay: Optional[int] = None, recovery_delay_non_mlag: Optional[int] = None - ) -> None: - """ - Run VerifyMlagDualPrimary validation - - Args: - detection_delay: Delay detection for seconds. - errdisabled: Errdisabled all interfaces when dual-primary is detected. Defaults to False. - recovery_delay: Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled. - recovery_delay_non_mlag: Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled. - """ - - if detection_delay is None or errdisabled is None or recovery_delay is None or recovery_delay_non_mlag is None: - self.result.is_skipped( - f"{self.__class__.name} did not run because detection_delay, errdisabled, recovery_delay or recovery_delay_non_mlag were not supplied" - ) - return - - errdisabled_action = "errdisableAllInterfaces" if errdisabled else "none" + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + detection_delay: conint(ge=0) # type: ignore + """Delay detection (seconds)""" + errdisabled: bool = False + """Errdisabled all interfaces when dual-primary is detected""" + recovery_delay: conint(ge=0) # type: ignore + """Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled""" + recovery_delay_non_mlag: conint(ge=0) # type: ignore + """Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled""" + @AntaTest.anta_test + def test(self) -> None: + errdisabled_action = "errdisableAllInterfaces" if self.inputs.errdisabled else "none" command_output = self.instance_commands[0].json_output - if command_output["state"] == "disabled": self.result.is_skipped("MLAG is disabled") return - if command_output["dualPrimaryDetectionState"] == "disabled": self.result.is_failure("Dual-primary detection is disabled") return - keys_to_verify = ["detail.dualPrimaryDetectionDelay", "detail.dualPrimaryAction", "dualPrimaryMlagRecoveryDelay", "dualPrimaryNonMlagRecoveryDelay"] verified_output = {key: get_value(command_output, key) for key in keys_to_verify} - if ( - verified_output["detail.dualPrimaryDetectionDelay"] == detection_delay + verified_output["detail.dualPrimaryDetectionDelay"] == self.inputs.detection_delay and verified_output["detail.dualPrimaryAction"] == errdisabled_action - and verified_output["dualPrimaryMlagRecoveryDelay"] == recovery_delay - and verified_output["dualPrimaryNonMlagRecoveryDelay"] == recovery_delay_non_mlag + and verified_output["dualPrimaryMlagRecoveryDelay"] == self.inputs.recovery_delay + and verified_output["dualPrimaryNonMlagRecoveryDelay"] == self.inputs.recovery_delay_non_mlag ): self.result.is_success() - else: self.result.is_failure(f"The dual-primary parameters are not configured properly: {verified_output}") diff --git a/anta/tests/multicast.py b/anta/tests/multicast.py index 967538e0c..207aaaa97 100644 --- a/anta/tests/multicast.py +++ b/anta/tests/multicast.py @@ -4,19 +4,19 @@ """ Test functions related to multicast """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations -from typing import List, Optional +from typing import Dict +from anta.custom_types import Vlan from anta.models import AntaCommand, AntaTest class VerifyIGMPSnoopingVlans(AntaTest): """ Verifies the IGMP snooping configuration for some VLANs. - - Args: - vlans (List[str]): A list of VLANs - configuration (str): Expected IGMP snooping configuration (enabled or disabled) for these VLANs. """ name = "VerifyIGMPSnoopingVlans" @@ -24,42 +24,27 @@ class VerifyIGMPSnoopingVlans(AntaTest): categories = ["multicast", "igmp"] commands = [AntaCommand(command="show ip igmp snooping")] - @AntaTest.anta_test - def test(self, vlans: Optional[List[str]] = None, configuration: Optional[str] = None) -> None: - """ - Run VerifyIGMPSnoopingVlans validation - - Args: - vlans: List of VLANs. - configuration: Expected IGMP configuration (enabled or disabled) for these VLANs. - """ - - if not vlans or not configuration: - self.result.is_skipped("VerifyIGMPSnoopingVlans was not run as no vlans or configuration was given") - return - if configuration not in ["enabled", "disabled"]: - self.result.is_error(f"VerifyIGMPSnoopingVlans was not run as 'configuration': {configuration} is not in the allowed values: ['enabled', 'disabled'])") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + vlans: Dict[Vlan, bool] + """Dictionary of VLANs with associated IGMP configuration status (True=enabled, False=disabled)""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - self.result.is_success() - for vlan in vlans: - if vlan not in command_output["vlans"]: + for vlan, enabled in self.inputs.vlans.items(): + if str(vlan) not in command_output["vlans"]: self.result.is_failure(f"Supplied vlan {vlan} is not present on the device.") continue igmp_state = command_output["vlans"][str(vlan)]["igmpSnoopingState"] - if igmp_state != configuration: + if igmp_state != "enabled" if enabled else igmp_state != "disabled": self.result.is_failure(f"IGMP state for vlan {vlan} is {igmp_state}") class VerifyIGMPSnoopingGlobal(AntaTest): """ Verifies the IGMP snooping global configuration. - - Args: - configuration (str): Expected global IGMP snooping configuration (enabled or disabled). """ name = "VerifyIGMPSnoopingGlobal" @@ -67,25 +52,14 @@ class VerifyIGMPSnoopingGlobal(AntaTest): categories = ["multicast", "igmp"] commands = [AntaCommand(command="show ip igmp snooping")] - @AntaTest.anta_test - def test(self, configuration: Optional[str] = None) -> None: - """ - Run VerifyIGMPSnoopingGlobal validation - - Args: - configuration: Expected global IGMP configuration (enabled or disabled). - """ - - if not configuration: - self.result.is_skipped("VerifyIGMPSnoopingGlobal was not run as no configuration was given") - return - - if configuration not in ["enabled", "disabled"]: - self.result.is_error(f"VerifyIGMPSnoopingGlobal was not run as 'configuration': {configuration} is not in the allowed values: ['enabled', 'disabled'])") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + enabled: bool + """Expected global IGMP snooping configuration (True=enabled, False=disabled)""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - self.result.is_success() - if (igmp_state := command_output["igmpSnoopingState"]) != configuration: + igmp_state = command_output["igmpSnoopingState"] + if igmp_state != "enabled" if self.inputs.enabled else igmp_state != "disabled": self.result.is_failure(f"IGMP state is not valid: {igmp_state}") diff --git a/anta/tests/profiles.py b/anta/tests/profiles.py index 5ec0ea409..af343e384 100644 --- a/anta/tests/profiles.py +++ b/anta/tests/profiles.py @@ -4,8 +4,11 @@ """ Test functions related to ASIC profiles """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations -from typing import Optional +from typing import Literal from anta.decorators import skip_on_platforms from anta.models import AntaCommand, AntaTest @@ -21,24 +24,18 @@ class VerifyUnifiedForwardingTableMode(AntaTest): categories = ["profiles"] commands = [AntaCommand(command="show platform trident forwarding-table partition", ofmt="json")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + mode: Literal[0, 1, 2, 3, 4, "flexible"] + """Expected UFT mode""" + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self, mode: Optional[str] = None) -> None: - """ - Run VerifyUnifiedForwardingTableMode validation - - Args: - mode: Expected UFT mode. - """ - if not mode: - self.result.is_skipped("VerifyUnifiedForwardingTableMode was not run as no mode was given") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output - if command_output["uftMode"] == mode: + if command_output["uftMode"] == str(self.inputs.mode): self.result.is_success() else: - self.result.is_failure(f"Device is not running correct UFT mode (expected: {mode} / running: {command_output['uftMode']})") + self.result.is_failure(f"Device is not running correct UFT mode (expected: {self.inputs.mode} / running: {command_output['uftMode']})") class VerifyTcamProfile(AntaTest): @@ -51,21 +48,15 @@ class VerifyTcamProfile(AntaTest): categories = ["profiles"] commands = [AntaCommand(command="show hardware tcam profile", ofmt="json")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + profile: str + """Expected TCAM profile""" + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test - def test(self, profile: Optional[str] = None) -> None: - """ - Run VerifyTcamProfile validation - - Args: - profile: Expected TCAM profile. - """ - if not profile: - self.result.is_skipped("VerifyTcamProfile was not run as no profile was given") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output - if command_output["pmfProfiles"]["FixedSystem"]["status"] == command_output["pmfProfiles"]["FixedSystem"]["config"] == profile: + if command_output["pmfProfiles"]["FixedSystem"]["status"] == command_output["pmfProfiles"]["FixedSystem"]["config"] == self.inputs.profile: self.result.is_success() else: self.result.is_failure(f"Incorrect profile running on device: {command_output['pmfProfiles']['FixedSystem']['status']}") diff --git a/anta/tests/routing/bgp.py b/anta/tests/routing/bgp.py index 1527c1318..cf8e00b75 100644 --- a/anta/tests/routing/bgp.py +++ b/anta/tests/routing/bgp.py @@ -4,18 +4,34 @@ """ BGP test functions """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Any, Dict from anta.decorators import check_bgp_family_enable from anta.models import AntaCommand, AntaTemplate, AntaTest -def _check_bgp_vrfs(bgp_vrfs: Dict[str, Any]) -> Dict[str, Any]: +def _check_bgp_vrfs(bgp_vrfs: dict[str, Any]) -> dict[str, Any]: + """Parse the output of 'show bgp [ADDR FAMILY] summary vrf [VRF]' + and returns a dictionary with the following structure: + { + "VRF_NAME": { + "PEER": + { + "peerState": BGP_STATE, + "inMsgQueue": MSG_COUNT, + "outMsgQueue": MSG_COUNT, + } + } + } + + Args: + bgp_vrfs: output of 'show bgp [ADDR FAMILY] summary vrf [VRF]' """ - TODO - """ - state_issue: Dict[str, Any] = {} + state_issue: dict[str, Any] = {} for vrf in bgp_vrfs: for peer in bgp_vrfs[vrf]["peers"]: if ( @@ -56,11 +72,8 @@ class VerifyBGPIPv4UnicastState(AntaTest): @check_bgp_family_enable("ipv4") @AntaTest.anta_test def test(self) -> None: - """Run VerifyBGPIPv4UnicastState validation""" - command_output = self.instance_commands[0].json_output state_issue = _check_bgp_vrfs(command_output["vrfs"]) - if not state_issue: self.result.is_success() else: @@ -71,12 +84,13 @@ class VerifyBGPIPv4UnicastCount(AntaTest): """ Verifies all IPv4 unicast BGP sessions are established and all BGP messages queues for these sessions are empty - and the actual number of BGP IPv4 unicast neighbors is the one we expect. + and the actual number of BGP IPv4 unicast neighbors is the one we expect + in all VRFs specified as input. - * self.result = "skipped" if the `number` or `vrf` parameter is missing * self.result = "success" if all IPv4 unicast BGP sessions are established and if all BGP messages queues for these sessions are empty - and if the actual number of BGP IPv4 unicast neighbors is equal to `number. + and if the actual number of BGP IPv4 unicast neighbors is equal to `number + in all VRFs specified as input. * self.result = "failure" otherwise. """ @@ -86,36 +100,32 @@ class VerifyBGPIPv4UnicastCount(AntaTest): " the actual number of BGP IPv4 unicast neighbors is the one we expect." ) categories = ["routing", "bgp"] - template = AntaTemplate(template="show bgp ipv4 unicast summary vrf {vrf}") + commands = [AntaTemplate(template="show bgp ipv4 unicast summary vrf {vrf}")] - @check_bgp_family_enable("ipv4") - @AntaTest.anta_test - def test(self, number: Optional[int] = None) -> None: - """ - Run VerifyBGPIPv4UnicastCount validation + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + vrfs: Dict[str, int] + """VRFs associated with neighbors count to verify""" - Args: - number: The expected number of BGP IPv4 unicast neighbors. - vrf: VRF to verify (template parameter) - """ - - if not number: - self.result.is_skipped("VerifyBGPIPv4UnicastCount could not run because number was not supplied") - return + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(vrf=vrf) for vrf in self.inputs.vrfs] + @check_bgp_family_enable("ipv4") + @AntaTest.anta_test + def test(self) -> None: self.result.is_success() - for command in self.instance_commands: if command.params and "vrf" in command.params: vrf = command.params["vrf"] - - peers = command.json_output["vrfs"][vrf]["peers"] - state_issue = _check_bgp_vrfs(command.json_output["vrfs"]) - - if len(peers) != number: - self.result.is_failure(f"Expecting {number} BGP peer in vrf {vrf} and got {len(peers)}") - if state_issue: - self.result.is_failure(f"The following IPv4 peers are not established: {state_issue}") + count = self.inputs.vrfs[vrf] + if vrf not in command.json_output["vrfs"]: + self.result.is_failure(f"VRF {vrf} is not configured") + return + peers = command.json_output["vrfs"][vrf]["peers"] + state_issue = _check_bgp_vrfs(command.json_output["vrfs"]) + if len(peers) != count: + self.result.is_failure(f"Expecting {count} BGP peer(s) in vrf {vrf} but got {len(peers)} peer(s)") + if state_issue: + self.result.is_failure(f"The following IPv4 peer(s) are not established: {state_issue}") class VerifyBGPIPv6UnicastState(AntaTest): @@ -137,12 +147,8 @@ class VerifyBGPIPv6UnicastState(AntaTest): @check_bgp_family_enable("ipv6") @AntaTest.anta_test def test(self) -> None: - """Run VerifyBGPIPv6UnicastState validation""" - command_output = self.instance_commands[0].json_output - state_issue = _check_bgp_vrfs(command_output["vrfs"]) - if not state_issue: self.result.is_success() else: @@ -166,15 +172,10 @@ class VerifyBGPEVPNState(AntaTest): @check_bgp_family_enable("evpn") @AntaTest.anta_test def test(self) -> None: - """Run VerifyBGPEVPNState validation""" - command_output = self.instance_commands[0].json_output - bgp_vrfs = command_output["vrfs"] - peers = bgp_vrfs["default"]["peers"] non_established_peers = [peer for peer, peer_dict in peers.items() if peer_dict["peerState"] != "Established"] - if not non_established_peers: self.result.is_success() else: @@ -186,7 +187,6 @@ class VerifyBGPEVPNCount(AntaTest): Verifies all EVPN BGP sessions are established (default VRF) and the actual number of BGP EVPN neighbors is the one we expect (default VRF). - * self.result = "skipped" if the `number` parameter is missing * self.result = "success" if all EVPN BGP sessions are Established and if the actual number of BGP EVPN neighbors is the one we expect. * self.result = "failure" otherwise. @@ -197,30 +197,22 @@ class VerifyBGPEVPNCount(AntaTest): categories = ["routing", "bgp"] commands = [AntaCommand(command="show bgp evpn summary")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: int + """The expected number of BGP EVPN neighbors in the default VRF""" + @check_bgp_family_enable("evpn") @AntaTest.anta_test - def test(self, number: Optional[int] = None) -> None: - """ - Run VerifyBGPEVPNCount validation - - Args: - number: The expected number of BGP EVPN neighbors in the default VRF. - """ - if not number: - self.result.is_skipped("VerifyBGPEVPNCount could not run because number was not supplied.") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output - peers = command_output["vrfs"]["default"]["peers"] non_established_peers = [peer for peer, peer_dict in peers.items() if peer_dict["peerState"] != "Established"] - - if not non_established_peers and len(peers) == number: + if not non_established_peers and len(peers) == self.inputs.number: self.result.is_success() else: self.result.is_failure() - if len(peers) != number: - self.result.is_failure(f"Expecting {number} BGP EVPN peers and got {len(peers)}") + if len(peers) != self.inputs.number: + self.result.is_failure(f"Expecting {self.inputs.number} BGP EVPN peers and got {len(peers)}") if non_established_peers: self.result.is_failure(f"The following EVPN peers are not established: {non_established_peers}") @@ -242,15 +234,10 @@ class VerifyBGPRTCState(AntaTest): @check_bgp_family_enable("rtc") @AntaTest.anta_test def test(self) -> None: - """Run VerifyBGPRTCState validation""" - command_output = self.instance_commands[0].json_output - bgp_vrfs = command_output["vrfs"] - peers = bgp_vrfs["default"]["peers"] non_established_peers = [peer for peer, peer_dict in peers.items() if peer_dict["peerState"] != "Established"] - if not non_established_peers: self.result.is_success() else: @@ -262,7 +249,6 @@ class VerifyBGPRTCCount(AntaTest): Verifies all RTC BGP sessions are established (default VRF) and the actual number of BGP RTC neighbors is the one we expect (default VRF). - * self.result = "skipped" if the `number` parameter is missing * self.result = "success" if all RTC BGP sessions are Established and if the actual number of BGP RTC neighbors is the one we expect. * self.result = "failure" otherwise. @@ -273,29 +259,21 @@ class VerifyBGPRTCCount(AntaTest): categories = ["routing", "bgp"] commands = [AntaCommand(command="show bgp rt-membership summary")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: int + """The expected number of BGP RTC neighbors in the default VRF""" + @check_bgp_family_enable("rtc") @AntaTest.anta_test - def test(self, number: Optional[int] = None) -> None: - """ - Run VerifyBGPRTCCount validation - - Args: - number: The expected number of BGP RTC neighbors (default VRF). - """ - if not number: - self.result.is_skipped("VerifyBGPRTCCount could not run because number was not supplied") - return - + def test(self) -> None: command_output = self.instance_commands[0].json_output - peers = command_output["vrfs"]["default"]["peers"] non_established_peers = [peer for peer, peer_dict in peers.items() if peer_dict["peerState"] != "Established"] - - if not non_established_peers and len(peers) == number: + if not non_established_peers and len(peers) == self.inputs.number: self.result.is_success() else: self.result.is_failure() - if len(peers) != number: - self.result.is_failure(f"Expecting {number} BGP RTC peers and got {len(peers)}") + if len(peers) != self.inputs.number: + self.result.is_failure(f"Expecting {self.inputs.number} BGP RTC peers and got {len(peers)}") if non_established_peers: self.result.is_failure(f"The following RTC peers are not established: {non_established_peers}") diff --git a/anta/tests/routing/generic.py b/anta/tests/routing/generic.py index 34bcaab2a..4bc5055fd 100644 --- a/anta/tests/routing/generic.py +++ b/anta/tests/routing/generic.py @@ -4,8 +4,11 @@ """ Generic routing test functions """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from typing import Literal -from typing import Optional +from pydantic import model_validator from anta.models import AntaCommand, AntaTest @@ -14,8 +17,6 @@ class VerifyRoutingProtocolModel(AntaTest): """ Verifies the configured routing protocol model is the one we expect. And if there is no mismatch between the configured and operating routing protocol model. - - model(str): Expected routing protocol model (multi-agent or ribd). Default is multi-agent """ name = "VerifyRoutingProtocolModel" @@ -23,62 +24,55 @@ class VerifyRoutingProtocolModel(AntaTest): "Verifies the configured routing protocol model is the expected one and if there is no mismatch between the configured and operating routing protocol model." ) categories = ["routing", "generic"] - # "revision": 3 - commands = [AntaCommand(command="show ip route summary")] + commands = [AntaCommand(command="show ip route summary", revision=3)] - @AntaTest.anta_test - def test(self, model: Optional[str] = "multi-agent") -> None: - """Run VerifyRoutingProtocolModel validation""" + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + model: Literal["multi-agent", "ribd"] = "multi-agent" + """Expected routing protocol model""" - if not model: - self.result.is_skipped("VerifyRoutingProtocolModel was not run as no model was given") - return + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - configured_model = command_output["protoModelStatus"]["configuredProtoModel"] operating_model = command_output["protoModelStatus"]["operatingProtoModel"] - if configured_model == operating_model == model: + if configured_model == operating_model == self.inputs.model: self.result.is_success() else: - self.result.is_failure(f"routing model is misconfigured: configured: {configured_model} - operating: {operating_model} - expected: {model}") + self.result.is_failure(f"routing model is misconfigured: configured: {configured_model} - operating: {operating_model} - expected: {self.inputs.model}") class VerifyRoutingTableSize(AntaTest): """ Verifies the size of the IP routing table (default VRF). Should be between the two provided thresholds. - - Args: - minimum(int): Expected minimum routing table (default VRF) size. - maximum(int): Expected maximum routing table (default VRF) size. """ name = "VerifyRoutingTableSize" description = "Verifies the size of the IP routing table (default VRF). Should be between the two provided thresholds." categories = ["routing", "generic"] - # "revision": 3 - commands = [AntaCommand(command="show ip route summary")] + commands = [AntaCommand(command="show ip route summary", revision=3)] - @AntaTest.anta_test - def test(self, minimum: Optional[int] = None, maximum: Optional[int] = None) -> None: - """Run VerifyRoutingTableSize validation""" - - if not minimum or not maximum: - self.result.is_skipped(f"VerifyRoutingTableSize was not run as either minimum {minimum} or maximum {maximum} was not provided") - return - if not isinstance(minimum, int) or not isinstance(maximum, int): - self.result.is_error(f"VerifyRoutingTableSize was not run as either minimum {minimum} or maximum {maximum} is not a valid value (integer)") - return - if maximum < minimum: - self.result.is_error(f"VerifyRoutingTableSize was not run as minimum {minimum} is greate than maximum {maximum}.") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + minimum: int + """Expected minimum routing table (default VRF) size""" + maximum: int + """Expected maximum routing table (default VRF) size""" + @model_validator(mode="after") # type: ignore + def check_min_max(self) -> AntaTest.Input: + """Validate that maximum is greater than minimum""" + if self.minimum > self.maximum: + raise ValueError(f"Minimum {self.minimum} is greater than maximum {self.maximum}") + return self + + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output total_routes = int(command_output["vrfs"]["default"]["totalRoutes"]) - if minimum <= total_routes <= maximum: + if self.inputs.minimum <= total_routes <= self.inputs.maximum: self.result.is_success() else: - self.result.is_failure(f"routing-table has {total_routes} routes and not between min ({minimum}) and maximum ({maximum})") + self.result.is_failure(f"routing-table has {total_routes} routes and not between min ({self.inputs.minimum}) and maximum ({self.inputs.maximum})") class VerifyBFD(AntaTest): @@ -94,12 +88,8 @@ class VerifyBFD(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyBFD validation""" - command_output = self.instance_commands[0].json_output - self.result.is_success() - for _, vrf_data in command_output["vrfs"].items(): for _, neighbor_data in vrf_data["ipv4Neighbors"].items(): for peer, peer_data in neighbor_data["peerStats"].items(): diff --git a/anta/tests/routing/ospf.py b/anta/tests/routing/ospf.py index c69206b37..ba3f69e75 100644 --- a/anta/tests/routing/ospf.py +++ b/anta/tests/routing/ospf.py @@ -4,14 +4,16 @@ """ OSPF test functions """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import Any, Dict, List, Optional +from typing import Any from anta.models import AntaCommand, AntaTest -def _count_ospf_neighbor(ospf_neighbor_json: Dict[str, Any]) -> int: +def _count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int: """ Count the number of OSPF neighbors """ @@ -22,7 +24,7 @@ def _count_ospf_neighbor(ospf_neighbor_json: Dict[str, Any]) -> int: return count -def _get_not_full_ospf_neighbors(ospf_neighbor_json: Dict[str, Any]) -> List[Dict[str, Any]]: +def _get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: """ Return the OSPF neighbors whose adjacency state is not "full" """ @@ -39,7 +41,6 @@ def _get_not_full_ospf_neighbors(ospf_neighbor_json: Dict[str, Any]) -> List[Dic "state": state, } ) - return not_full_neighbors @@ -55,16 +56,11 @@ class VerifyOSPFNeighborState(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyOSPFNeighborState validation""" - command_output = self.instance_commands[0].json_output - if _count_ospf_neighbor(command_output) == 0: self.result.is_skipped("no OSPF neighbor found") return - self.result.is_success() - not_full_neighbors = _get_not_full_ospf_neighbors(command_output) if not_full_neighbors: self.result.is_failure(f"Some neighbors are not correctly configured: {not_full_neighbors}.") @@ -73,9 +69,6 @@ def test(self) -> None: class VerifyOSPFNeighborCount(AntaTest): """ Verifies the number of OSPF neighbors in FULL state is the one we expect. - - Args: - number (int): The expected number of OSPF neighbors in FULL state. """ name = "VerifyOSPFNeighborCount" @@ -83,24 +76,19 @@ class VerifyOSPFNeighborCount(AntaTest): categories = ["routing", "ospf"] commands = [AntaCommand(command="show ip ospf neighbor")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None) -> None: - """Run VerifyOSPFNeighborCount validation""" - if not (isinstance(number, int) and number >= 0): - self.result.is_skipped(f"VerifyOSPFNeighborCount was not run as the number given '{number}' is not a valid value.") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: int + """The expected number of OSPF neighbors in FULL state""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - if (neighbor_count := _count_ospf_neighbor(command_output)) == 0: self.result.is_skipped("no OSPF neighbor found") return - self.result.is_success() - - if neighbor_count != number: - self.result.is_failure(f"device has {neighbor_count} neighbors (expected {number})") - + if neighbor_count != self.inputs.number: + self.result.is_failure(f"device has {neighbor_count} neighbors (expected {self.inputs.number})") not_full_neighbors = _get_not_full_ospf_neighbors(command_output) print(not_full_neighbors) if not_full_neighbors: diff --git a/anta/tests/security.py b/anta/tests/security.py index b45cd63ad..e1364f61d 100644 --- a/anta/tests/security.py +++ b/anta/tests/security.py @@ -4,9 +4,11 @@ """ Test functions related to the EOS various security settings """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import Optional +from pydantic import conint from anta.models import AntaCommand, AntaTest @@ -27,10 +29,6 @@ class VerifySSHStatus(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifySSHStatus validation. - """ - command_output = self.instance_commands[0].text_output line = [line for line in command_output.split("\n") if line.startswith("SSHD status")][0] @@ -49,7 +47,6 @@ class VerifySSHIPv4Acl(AntaTest): Expected results: * success: The test will pass if the SSHD agent has the provided number of IPv4 ACL(s) in the specified VRF. * failure: The test will fail if the SSHD agent has not the right number of IPv4 ACL(s) in the specified VRF. - * skipped: The test will be skipped if the number of IPv4 ACL(s) or VRF parameter is not provided. """ name = "VerifySSHIPv4Acl" @@ -57,35 +54,26 @@ class VerifySSHIPv4Acl(AntaTest): categories = ["security"] commands = [AntaCommand(command="show management ssh ip access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifySSHIPv4Acl validation. - - Args: - number: The number of expected IPv4 ACL(s). - vrf: The name of the VRF in which to check for the SSHD agent. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv4 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for the SSHD agent""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv4_acl_list = command_output["ipAclList"]["aclList"] ipv4_acl_number = len(ipv4_acl_list) not_configured_acl_list = [] - - if ipv4_acl_number != number: - self.result.is_failure(f"Expected {number} SSH IPv4 ACL(s) in vrf {vrf} but got {ipv4_acl_number}") + if ipv4_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} SSH IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}") return - for ipv4_acl in ipv4_acl_list: - if vrf not in ipv4_acl["configuredVrfs"] or vrf not in ipv4_acl["activeVrfs"]: + if self.inputs.vrf not in ipv4_acl["configuredVrfs"] or self.inputs.vrf not in ipv4_acl["activeVrfs"]: not_configured_acl_list.append(ipv4_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"SSH IPv4 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"SSH IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() @@ -97,7 +85,6 @@ class VerifySSHIPv6Acl(AntaTest): Expected results: * success: The test will pass if the SSHD agent has the provided number of IPv6 ACL(s) in the specified VRF. * failure: The test will fail if the SSHD agent has not the right number of IPv6 ACL(s) in the specified VRF. - * skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided. """ name = "VerifySSHIPv6Acl" @@ -105,35 +92,26 @@ class VerifySSHIPv6Acl(AntaTest): categories = ["security"] commands = [AntaCommand(command="show management ssh ipv6 access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifySSHIPv6Acl validation. - - Args: - number: The number of expected IPv6 ACL(s). - vrf: The name of the VRF in which to check for the SSHD agent. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv6 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for the SSHD agent""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv6_acl_list = command_output["ipv6AclList"]["aclList"] ipv6_acl_number = len(ipv6_acl_list) not_configured_acl_list = [] - - if ipv6_acl_number != number: - self.result.is_failure(f"Expected {number} SSH IPv6 ACL(s) in vrf {vrf} but got {ipv6_acl_number}") + if ipv6_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} SSH IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}") return - for ipv6_acl in ipv6_acl_list: - if vrf not in ipv6_acl["configuredVrfs"] or vrf not in ipv6_acl["activeVrfs"]: + if self.inputs.vrf not in ipv6_acl["configuredVrfs"] or self.inputs.vrf not in ipv6_acl["activeVrfs"]: not_configured_acl_list.append(ipv6_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"SSH IPv6 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"SSH IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() @@ -154,12 +132,7 @@ class VerifyTelnetStatus(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyTelnetStatus validation. - """ - command_output = self.instance_commands[0].json_output - if command_output["serverState"] == "disabled": self.result.is_success() else: @@ -182,12 +155,7 @@ class VerifyAPIHttpStatus(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyAPIHTTPStatus validation. - """ - command_output = self.instance_commands[0].json_output - if command_output["enabled"] and not command_output["httpServer"]["running"]: self.result.is_success() else: @@ -201,7 +169,6 @@ class VerifyAPIHttpsSSL(AntaTest): Expected results: * success: The test will pass if the eAPI HTTPS server SSL profile is configured and valid. * failure: The test will fail if the eAPI HTTPS server SSL profile is NOT configured, misconfigured or invalid. - * skipped: The test will be skipped if the SSL profile is not provided. """ name = "VerifyAPIHttpsSSL" @@ -209,28 +176,21 @@ class VerifyAPIHttpsSSL(AntaTest): categories = ["security"] commands = [AntaCommand(command="show management api http-commands")] - @AntaTest.anta_test - def test(self, profile: Optional[str] = None) -> None: - """ - Run VerifyAPIHttpsSSL validation. - - Args: - profile: SSL profile to verify. - """ - if not profile: - self.result.is_skipped(f"{self.__class__.name} did not run because profile was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + profile: str + """SSL profile to verify""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - try: - if command_output["sslProfile"]["name"] == profile and command_output["sslProfile"]["state"] == "valid": + if command_output["sslProfile"]["name"] == self.inputs.profile and command_output["sslProfile"]["state"] == "valid": self.result.is_success() else: - self.result.is_failure(f"eAPI HTTPS server SSL profile ({profile}) is misconfigured or invalid") + self.result.is_failure(f"eAPI HTTPS server SSL profile ({self.inputs.profile}) is misconfigured or invalid") except KeyError: - self.result.is_failure(f"eAPI HTTPS server SSL profile ({profile}) is not configured") + self.result.is_failure(f"eAPI HTTPS server SSL profile ({self.inputs.profile}) is not configured") class VerifyAPIIPv4Acl(AntaTest): @@ -240,7 +200,6 @@ class VerifyAPIIPv4Acl(AntaTest): Expected results: * success: The test will pass if eAPI has the provided number of IPv4 ACL(s) in the specified VRF. * failure: The test will fail if eAPI has not the right number of IPv4 ACL(s) in the specified VRF. - * skipped: The test will be skipped if the number of IPv4 ACL(s) or VRF parameter is not provided. """ name = "VerifyAPIIPv4Acl" @@ -248,35 +207,26 @@ class VerifyAPIIPv4Acl(AntaTest): categories = ["security"] commands = [AntaCommand(command="show management api http-commands ip access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifyAPIIPv4Acl validation. - - Args: - number: The number of expected IPv4 ACL(s). - vrf: The name of the VRF in which to check for eAPI. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv4 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for eAPI""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv4_acl_list = command_output["ipAclList"]["aclList"] ipv4_acl_number = len(ipv4_acl_list) not_configured_acl_list = [] - - if ipv4_acl_number != number: - self.result.is_failure(f"Expected {number} eAPI IPv4 ACL(s) in vrf {vrf} but got {ipv4_acl_number}") + if ipv4_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} eAPI IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}") return - for ipv4_acl in ipv4_acl_list: - if vrf not in ipv4_acl["configuredVrfs"] or vrf not in ipv4_acl["activeVrfs"]: + if self.inputs.vrf not in ipv4_acl["configuredVrfs"] or self.inputs.vrf not in ipv4_acl["activeVrfs"]: not_configured_acl_list.append(ipv4_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"eAPI IPv4 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"eAPI IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() @@ -296,34 +246,25 @@ class VerifyAPIIPv6Acl(AntaTest): categories = ["security"] commands = [AntaCommand(command="show management api http-commands ipv6 access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifyAPIIPv6Acl validation. - - Args: - number: The number of expected IPv6 ACL(s). - vrf: The name of the VRF in which to check for eAPI. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv6 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for eAPI""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv6_acl_list = command_output["ipv6AclList"]["aclList"] ipv6_acl_number = len(ipv6_acl_list) not_configured_acl_list = [] - - if ipv6_acl_number != number: - self.result.is_failure(f"Expected {number} eAPI IPv6 ACL(s) in vrf {vrf} but got {ipv6_acl_number}") + if ipv6_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} eAPI IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}") return - for ipv6_acl in ipv6_acl_list: - if vrf not in ipv6_acl["configuredVrfs"] or vrf not in ipv6_acl["activeVrfs"]: + if self.inputs.vrf not in ipv6_acl["configuredVrfs"] or self.inputs.vrf not in ipv6_acl["activeVrfs"]: not_configured_acl_list.append(ipv6_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"eAPI IPv6 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"eAPI IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 2d25feb34..7a6e03166 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -4,9 +4,11 @@ """ Test functions related to the EOS various SNMP settings """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import Optional +from pydantic import conint from anta.models import AntaCommand, AntaTest @@ -18,7 +20,6 @@ class VerifySnmpStatus(AntaTest): Expected Results: * success: The test will pass if the SNMP agent is enabled in the specified VRF. * failure: The test will fail if the SNMP agent is disabled in the specified VRF. - * skipped: The test will be skipped if the VRF parameter is not provided. """ name = "VerifySnmpStatus" @@ -26,23 +27,17 @@ class VerifySnmpStatus(AntaTest): categories = ["snmp"] commands = [AntaCommand(command="show snmp")] + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + vrf: str = "default" + """The name of the VRF in which to check for the SNMP agent""" + @AntaTest.anta_test - def test(self, vrf: str = "default") -> None: - """ - Run VerifySnmpStatus validation. - - Args: - vrf: The name of the VRF in which to check for the SNMP agent. Defaults to 'default'. - """ - if not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because vrf was not supplied") + def test(self) -> None: + command_output = self.instance_commands[0].json_output + if command_output["enabled"] and self.inputs.vrf in command_output["vrfs"]["snmpVrfs"]: + self.result.is_success() else: - command_output = self.instance_commands[0].json_output - - if command_output["enabled"] and vrf in command_output["vrfs"]["snmpVrfs"]: - self.result.is_success() - else: - self.result.is_failure(f"SNMP agent disabled in vrf {vrf}") + self.result.is_failure(f"SNMP agent disabled in vrf {self.inputs.vrf}") class VerifySnmpIPv4Acl(AntaTest): @@ -52,7 +47,6 @@ class VerifySnmpIPv4Acl(AntaTest): Expected results: * success: The test will pass if the SNMP agent has the provided number of IPv4 ACL(s) in the specified VRF. * failure: The test will fail if the SNMP agent has not the right number of IPv4 ACL(s) in the specified VRF. - * skipped: The test will be skipped if the number of IPv4 ACL(s) or VRF parameter is not provided. """ name = "VerifySnmpIPv4Acl" @@ -60,35 +54,26 @@ class VerifySnmpIPv4Acl(AntaTest): categories = ["snmp"] commands = [AntaCommand(command="show snmp ipv4 access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifySnmpIPv4Acl validation. - - Args: - number: The number of expected IPv4 ACL(s). - vrf: The name of the VRF in which to check for the SNMP agent. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv4 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for the SNMP agent""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv4_acl_list = command_output["ipAclList"]["aclList"] ipv4_acl_number = len(ipv4_acl_list) not_configured_acl_list = [] - - if ipv4_acl_number != number: - self.result.is_failure(f"Expected {number} SNMP IPv4 ACL(s) in vrf {vrf} but got {ipv4_acl_number}") + if ipv4_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} SNMP IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}") return - for ipv4_acl in ipv4_acl_list: - if vrf not in ipv4_acl["configuredVrfs"] or vrf not in ipv4_acl["activeVrfs"]: + if self.inputs.vrf not in ipv4_acl["configuredVrfs"] or self.inputs.vrf not in ipv4_acl["activeVrfs"]: not_configured_acl_list.append(ipv4_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"SNMP IPv4 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"SNMP IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() @@ -100,7 +85,6 @@ class VerifySnmpIPv6Acl(AntaTest): Expected results: * success: The test will pass if the SNMP agent has the provided number of IPv6 ACL(s) in the specified VRF. * failure: The test will fail if the SNMP agent has not the right number of IPv6 ACL(s) in the specified VRF. - * skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided. """ name = "VerifySnmpIPv6Acl" @@ -108,34 +92,25 @@ class VerifySnmpIPv6Acl(AntaTest): categories = ["snmp"] commands = [AntaCommand(command="show snmp ipv6 access-list summary")] - @AntaTest.anta_test - def test(self, number: Optional[int] = None, vrf: str = "default") -> None: - """ - Run VerifySnmpIPv6Acl validation. - - Args: - number: The number of expected IPv6 ACL(s). - vrf: The name of the VRF in which to check for the SNMP agent. Defaults to 'default'. - """ - if not number or not vrf: - self.result.is_skipped(f"{self.__class__.name} did not run because number or vrf was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + number: conint(ge=0) # type:ignore + """The number of expected IPv6 ACL(s)""" + vrf: str = "default" + """The name of the VRF in which to check for the SNMP agent""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - ipv6_acl_list = command_output["ipv6AclList"]["aclList"] ipv6_acl_number = len(ipv6_acl_list) not_configured_acl_list = [] - - if ipv6_acl_number != number: - self.result.is_failure(f"Expected {number} SNMP IPv6 ACL(s) in vrf {vrf} but got {ipv6_acl_number}") + if ipv6_acl_number != self.inputs.number: + self.result.is_failure(f"Expected {self.inputs.number} SNMP IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}") return - for ipv6_acl in ipv6_acl_list: - if vrf not in ipv6_acl["configuredVrfs"] or vrf not in ipv6_acl["activeVrfs"]: + if self.inputs.vrf not in ipv6_acl["configuredVrfs"] or self.inputs.vrf not in ipv6_acl["activeVrfs"]: not_configured_acl_list.append(ipv6_acl["name"]) - if not_configured_acl_list: - self.result.is_failure(f"SNMP IPv6 ACL(s) not configured or active in vrf {vrf}: {not_configured_acl_list}") + self.result.is_failure(f"SNMP IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl_list}") else: self.result.is_success() diff --git a/anta/tests/software.py b/anta/tests/software.py index 6923485ea..88270cdc1 100644 --- a/anta/tests/software.py +++ b/anta/tests/software.py @@ -4,8 +4,11 @@ """ Test functions related to the EOS software """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined +from __future__ import annotations -from typing import List, Optional +from typing import List from anta.models import AntaCommand, AntaTest @@ -20,24 +23,17 @@ class VerifyEOSVersion(AntaTest): categories = ["software"] commands = [AntaCommand(command="show version")] - @AntaTest.anta_test - def test(self, versions: Optional[List[str]] = None) -> None: - """ - Run VerifyEOSVersion validation - - Args: - versions: List of allowed EOS versions. - """ - if not versions: - self.result.is_skipped("VerifyEOSVersion was not run as no versions were given") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + versions: List[str] + """List of allowed EOS versions""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - - if command_output["version"] in versions: + if command_output["version"] in self.inputs.versions: self.result.is_success() else: - self.result.is_failure(f'device is running version {command_output["version"]} not in expected versions: {versions}') + self.result.is_failure(f'device is running version {command_output["version"]} not in expected versions: {self.inputs.versions}') class VerifyTerminAttrVersion(AntaTest): @@ -50,26 +46,18 @@ class VerifyTerminAttrVersion(AntaTest): categories = ["software"] commands = [AntaCommand(command="show version detail")] - @AntaTest.anta_test - def test(self, versions: Optional[List[str]] = None) -> None: - """ - Run VerifyTerminAttrVersion validation - - Args: - versions: List of allowed TerminAttr versions. - """ - - if not versions: - self.result.is_skipped("VerifyTerminAttrVersion was not run as no versions were given") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + versions: List[str] + """List of allowed TerminAttr versions""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - command_output_data = command_output["details"]["packages"]["TerminAttr-core"]["version"] - if command_output_data in versions: + if command_output_data in self.inputs.versions: self.result.is_success() else: - self.result.is_failure(f"device is running TerminAttr version {command_output_data} and is not in the allowed list: {versions}") + self.result.is_failure(f"device is running TerminAttr version {command_output_data} and is not in the allowed list: {self.inputs.versions}") class VerifyEOSExtensions(AntaTest): @@ -84,22 +72,16 @@ class VerifyEOSExtensions(AntaTest): @AntaTest.anta_test def test(self) -> None: - """Run VerifyEOSExtensions validation""" - boot_extensions = [] - show_extensions_command_output = self.instance_commands[0].json_output show_boot_extensions_command_output = self.instance_commands[1].json_output - installed_extensions = [ extension for extension, extension_data in show_extensions_command_output["extensions"].items() if extension_data["status"] == "installed" ] - for extension in show_boot_extensions_command_output["extensions"]: extension = extension.strip("\n") if extension != "": boot_extensions.append(extension) - installed_extensions.sort() boot_extensions.sort() if installed_extensions == boot_extensions: diff --git a/anta/tests/stp.py b/anta/tests/stp.py index 64b68538d..2d365f151 100644 --- a/anta/tests/stp.py +++ b/anta/tests/stp.py @@ -4,10 +4,13 @@ """ Test functions related to various Spanning Tree Protocol (STP) settings """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import List, Optional +from typing import List, Literal +from anta.custom_types import Vlan from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools.get_value import get_value @@ -19,60 +22,37 @@ class VerifySTPMode(AntaTest): Expected Results: * success: The test will pass if the STP mode is configured properly in the specified VLAN(s). * failure: The test will fail if the STP mode is NOT configured properly for one or more specified VLAN(s). - * skipped: The test will be skipped if the STP mode is not provided. - * error: The test will give an error if a list of VLAN(s) is not provided as template_params. """ name = "VerifySTPMode" description = "Verifies the configured STP mode for a provided list of VLAN(s)." categories = ["stp"] - template = AntaTemplate(template="show spanning-tree vlan {vlan}") + commands = [AntaTemplate(template="show spanning-tree vlan {vlan}")] - @staticmethod - def _check_stp_mode(mode: str) -> None: - """ - Verifies if the provided STP mode is compatible with Arista EOS devices. + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + mode: Literal["mstp", "rstp", "rapidPvst"] = "mstp" + """STP mode to verify""" + vlans: List[Vlan] + """List of VLAN on which to verify STP mode""" - Args: - mode: The STP mode to verify. - """ - stp_modes = ["mstp", "rstp", "rapidPvst"] - - if mode not in stp_modes: - raise ValueError(f"Wrong STP mode provided. Valid modes are: {stp_modes}") + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(vlan=vlan) for vlan in self.inputs.vlans] @AntaTest.anta_test - def test(self, mode: str = "mstp") -> None: - """ - Run VerifySTPVersion validation. - - Args: - mode: STP mode to verify. Defaults to 'mstp'. - """ - if not mode: - self.result.is_skipped(f"{self.__class__.name} did not run because mode was not supplied") - return - - self._check_stp_mode(mode) - + def test(self) -> None: not_configured = [] wrong_stp_mode = [] - for command in self.instance_commands: if command.params and "vlan" in command.params: vlan_id = command.params["vlan"] if not (stp_mode := get_value(command.json_output, f"spanningTreeVlanInstances.{vlan_id}.spanningTreeVlanInstance.protocol")): not_configured.append(vlan_id) - - elif stp_mode != mode: + elif stp_mode != self.inputs.mode: wrong_stp_mode.append(vlan_id) - if not_configured: - self.result.is_failure(f"STP mode '{mode}' not configured for the following VLAN(s): {not_configured}") - + self.result.is_failure(f"STP mode '{self.inputs.mode}' not configured for the following VLAN(s): {not_configured}") if wrong_stp_mode: self.result.is_failure(f"Wrong STP mode configured for the following VLAN(s): {wrong_stp_mode}") - if not not_configured and not wrong_stp_mode: self.result.is_success() @@ -93,12 +73,7 @@ class VerifySTPBlockedPorts(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifySTPBlockedPorts validation - """ - command_output = self.instance_commands[0].json_output - if not (stp_instances := command_output["spanningTreeInstances"]): self.result.is_success() else: @@ -123,16 +98,10 @@ class VerifySTPCounters(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifySTPBlockedPorts validation - """ - command_output = self.instance_commands[0].json_output - interfaces_with_errors = [ interface for interface, counters in command_output["interfaces"].items() if counters["bpduTaggedError"] or counters["bpduOtherError"] != 0 ] - if interfaces_with_errors: self.result.is_failure(f"The following interfaces have STP BPDU packet errors: {interfaces_with_errors}") else: @@ -146,43 +115,39 @@ class VerifySTPForwardingPorts(AntaTest): Expected Results: * success: The test will pass if all interfaces are in a forwarding state for the specified VLAN(s). * failure: The test will fail if one or many interfaces are NOT in a forwarding state in the specified VLAN(s). - * error: The test will give an error if a list of VLAN(s) is not provided as template_params. """ name = "VerifySTPForwardingPorts" description = "Verifies that all interfaces are forwarding for a provided list of VLAN(s)." categories = ["stp"] - template = AntaTemplate(template="show spanning-tree topology vlan {vlan} status") + commands = [AntaTemplate(template="show spanning-tree topology vlan {vlan} status")] + + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + vlans: List[Vlan] + """List of VLAN on which to verify forwarding states""" + + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(vlan=vlan) for vlan in self.inputs.vlans] @AntaTest.anta_test def test(self) -> None: - """ - Run VerifySTPForwardingPorts validation. - """ - not_configured = [] not_forwarding = [] - for command in self.instance_commands: if command.params and "vlan" in command.params: vlan_id = command.params["vlan"] - if not (topologies := get_value(command.json_output, "topologies")): not_configured.append(vlan_id) else: for value in topologies.values(): if int(vlan_id) in value["vlans"]: interfaces_not_forwarding = [interface for interface, state in value["interfaces"].items() if state["state"] != "forwarding"] - if interfaces_not_forwarding: not_forwarding.append({f"VLAN {vlan_id}": interfaces_not_forwarding}) - if not_configured: self.result.is_failure(f"STP instance is not configured for the following VLAN(s): {not_configured}") - if not_forwarding: self.result.is_failure(f"The following VLAN(s) have interface(s) that are not in a fowarding state: {not_forwarding}") - if not not_configured and not interfaces_not_forwarding: self.result.is_success() @@ -194,7 +159,6 @@ class VerifySTPRootPriority(AntaTest): Expected Results: * success: The test will pass if the STP root priority is configured properly for the specified VLAN or MST instance ID(s). * failure: The test will fail if the STP root priority is NOT configured properly for the specified VLAN or MST instance ID(s). - * skipped: The test will be skipped if the STP root priority is not provided. """ name = "VerifySTPRootPriority" @@ -202,25 +166,18 @@ class VerifySTPRootPriority(AntaTest): categories = ["stp"] commands = [AntaCommand(command="show spanning-tree root detail")] - @AntaTest.anta_test - def test(self, priority: Optional[int] = None, instances: Optional[List[int]] = None) -> None: - """ - Run VerifySTPRootPriority validation. - - Args: - priority: STP root priority to verify. - instances: List of VLAN or MST instance ID(s). By default, ALL VLAN or MST instance ID(s) will be verified. - """ - if not priority: - self.result.is_skipped(f"{self.__class__.name} did not run because priority was not supplied") - return + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + priority: int + """STP root priority to verify""" + instances: List[Vlan] = [] + """List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified.""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - if not (stp_instances := command_output["instances"]): self.result.is_failure("No STP instances configured") return - for instance in stp_instances: if instance.startswith("MST"): prefix = "MST" @@ -228,11 +185,10 @@ def test(self, priority: Optional[int] = None, instances: Optional[List[int]] = if instance.startswith("VL"): prefix = "VL" break - - check_instances = [f"{prefix}{instance_id}" for instance_id in instances] if instances else command_output["instances"].keys() - - wrong_priority_instances = [instance for instance in check_instances if get_value(command_output, f"instances.{instance}.rootBridge.priority") != priority] - + check_instances = [f"{prefix}{instance_id}" for instance_id in self.inputs.instances] if self.inputs.instances else command_output["instances"].keys() + wrong_priority_instances = [ + instance for instance in check_instances if get_value(command_output, f"instances.{instance}.rootBridge.priority") != self.inputs.priority + ] if wrong_priority_instances: self.result.is_failure(f"The following instance(s) have the wrong STP root priority configured: {wrong_priority_instances}") else: diff --git a/anta/tests/system.py b/anta/tests/system.py index 930c49cde..5009d8eee 100644 --- a/anta/tests/system.py +++ b/anta/tests/system.py @@ -4,10 +4,13 @@ """ Test functions related to system-level features and protocols """ +# Mypy does not understand AntaTest.Input typing +# mypy: disable-error-code=attr-defined from __future__ import annotations import re -from typing import Optional + +from pydantic import conint from anta.models import AntaCommand, AntaTest @@ -19,7 +22,6 @@ class VerifyUptime(AntaTest): Expected Results: * success: The test will pass if the device uptime is higher than the provided value. * failure: The test will fail if the device uptime is lower than the provided value. - * skipped: The test will be skipped if the provided uptime value is invalid or negative. """ name = "VerifyUptime" @@ -27,22 +29,14 @@ class VerifyUptime(AntaTest): categories = ["system"] commands = [AntaCommand(command="show uptime")] - @AntaTest.anta_test - def test(self, minimum: Optional[int] = None) -> None: - """ - Run VerifyUptime validation - - Args: - minimum: Minimum uptime in seconds. - """ + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + minimum: conint(ge=0) # type: ignore + """Minimum uptime in seconds""" + @AntaTest.anta_test + def test(self) -> None: command_output = self.instance_commands[0].json_output - - if not (isinstance(minimum, (int, float))) or minimum < 0: - self.result.is_skipped(f"{self.__class__.name} was not run since the provided uptime value is invalid or negative") - return - - if command_output["upTime"] > minimum: + if command_output["upTime"] > self.inputs.minimum: self.result.is_success() else: self.result.is_failure(f"Device uptime is {command_output['upTime']} seconds") @@ -52,7 +46,7 @@ class VerifyReloadCause(AntaTest): """ This test verifies the last reload cause of the device. - Expected Results: + 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. * error: The test will report an error if the reload cause is NOT available. @@ -65,21 +59,14 @@ class VerifyReloadCause(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyReloadCause validation - """ - command_output = self.instance_commands[0].json_output - if "resetCauses" not in command_output.keys(): - self.result.is_error("No reload causes available") + self.result.is_error(message="No reload causes available") return - if len(command_output["resetCauses"]) == 0: # 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 [ @@ -110,16 +97,10 @@ class VerifyCoredump(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyCoredump validation - """ command_output = self.instance_commands[0].json_output - core_files = command_output["coreFiles"] - if "minidump" in core_files: core_files.remove("minidump") - if not core_files: self.result.is_success() else: @@ -142,11 +123,7 @@ class VerifyAgentLogs(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyAgentLogs validation - """ command_output = self.instance_commands[0].text_output - if len(command_output) == 0: self.result.is_success() else: @@ -171,12 +148,8 @@ class VerifyCPUUtilization(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyCPUUtilization validation - """ command_output = self.instance_commands[0].json_output command_output_data = command_output["cpuInfo"]["%Cpu(s)"]["idle"] - if command_output_data > 25: self.result.is_success() else: @@ -199,11 +172,7 @@ class VerifyMemoryUtilization(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyMemoryUtilization validation - """ command_output = self.instance_commands[0].json_output - memory_usage = command_output["memFree"] / command_output["memTotal"] if memory_usage > 0.25: self.result.is_success() @@ -227,13 +196,8 @@ class VerifyFileSystemUtilization(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyFileSystemUtilization validation - """ command_output = self.instance_commands[0].text_output - self.result.is_success() - for line in command_output.split("\n")[1:]: if "loop" not in line and len(line) > 0 and (percentage := int(line.split()[4].replace("%", ""))) > 75: self.result.is_failure(f"Mount point {line} is higher than 75%: reported {percentage}%") @@ -255,11 +219,7 @@ class VerifyNTP(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyNTP validation - """ command_output = self.instance_commands[0].text_output - if command_output.split("\n")[0].split(" ")[0] == "synchronised": self.result.is_success() else: diff --git a/anta/tests/vxlan.py b/anta/tests/vxlan.py index 8841ea53d..e62e0fe91 100644 --- a/anta/tests/vxlan.py +++ b/anta/tests/vxlan.py @@ -28,12 +28,7 @@ class VerifyVxlan1Interface(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyVxlan1Interface validation - """ - command_output = self.instance_commands[0].json_output - if "Vxlan1" not in command_output["interfaceDescriptions"]: self.result.is_skipped("Vxlan1 interface is not configured") elif ( @@ -65,22 +60,15 @@ class VerifyVxlanConfigSanity(AntaTest): @AntaTest.anta_test def test(self) -> None: - """ - Run VerifyVxlanConfigSanity validation - """ - command_output = self.instance_commands[0].json_output - if "categories" not in command_output or len(command_output["categories"]) == 0: self.result.is_skipped("VXLAN is not configured") return - failed_categories = { category: content for category, content in command_output["categories"].items() if category in ["localVtep", "mlag", "pd"] and content["allCheckPass"] is not True } - if len(failed_categories) > 0: self.result.is_failure(f"VXLAN config sanity check is not passing: {failed_categories}") else: diff --git a/anta/tools/misc.py b/anta/tools/misc.py index a47ef3f00..7437dc9ba 100644 --- a/anta/tools/misc.py +++ b/anta/tools/misc.py @@ -32,25 +32,19 @@ def anta_log_exception(exception: Exception, message: Optional[str] = None, call if calling_logger is None: calling_logger = logger if __DEBUG__: - calling_logger.exception(message) + calling_logger.exception(message, exc_info=exception) else: log_message = exc_to_str(exception) if message is not None: - log_message = f"{message} {log_message}" - calling_logger.error(log_message) + log_message = f"{message}: {log_message}" + calling_logger.critical(log_message) def exc_to_str(exception: Exception) -> str: """ Helper function that returns a human readable string from an Exception object """ - res = f"{type(exception).__name__}" - if str(exception): - res += f" ({str(exception)})" - elif hasattr(exception, "errmsg"): - # TODO - remove when we bump aio-eapi once our PR is merged there - res += f" ({exception.errmsg})" - return res + return f"{type(exception).__name__}{f' ({exception})' if str(exception) else ''}" def tb_to_str(exception: Exception) -> str: diff --git a/docs/advanced_usages/as-python-lib.md b/docs/advanced_usages/as-python-lib.md index 14e810762..d1e93a75b 100644 --- a/docs/advanced_usages/as-python-lib.md +++ b/docs/advanced_usages/as-python-lib.md @@ -247,15 +247,15 @@ asyncio.run(test.test()) assert test.result.result == "success" ``` -### Commands for test +### Classes for commands -To make it easier to get data, ANTA defines 2 different classes to manage commands to send to device: +To make it easier to get data, ANTA defines 2 different classes to manage commands to send to devices: -#### `anta.models.AntaCommand` +#### [AntaCommand](../api/models.md#anta.models.AntaCommand) Class -Abstract a command with following information: +Represent a command with following information: -- Command to run, +- Command to run - Ouput format expected - eAPI version - Output of the command @@ -285,15 +285,10 @@ cmd2 = AntaCommand(command="show running-config diffs", ofmt="text") commands = [AntaCommand(command="show bfd peers", revision=1)] ``` - -#### `anta.models.AntaTemplate` +#### [AntaTemplate](../api/models.md#anta.models.AntaTemplate) Class Because some command can require more dynamic than just a command with no parameter provided by user, ANTA supports command template: you define a template in your test class and user provide parameters when creating test object. -!!! warning "Warning on AntaTemplate" - * In its current versiom, an AntaTest class supports only __ONE__ AntaTemplate. - * The current interface to pass template parameter to a template is an area of future improvements. Feedbacks are welcome. - ```python class RunArbitraryTemplateCommand(AntaTest): diff --git a/docs/advanced_usages/custom-tests.md b/docs/advanced_usages/custom-tests.md index 8cd5b3b58..bd040fd25 100644 --- a/docs/advanced_usages/custom-tests.md +++ b/docs/advanced_usages/custom-tests.md @@ -4,128 +4,156 @@ ~ that can be found in the LICENSE file. --> -# Create your own custom tests - !!! info "" - This documentation applies for both create tests in ANTA package or your custom package. - -ANTA is not only a CLI with a collection of built-in tests, it is also a framework you can extend by building your own tests library. + This documentation applies for both creating tests in ANTA or creating your own test package. -For that, you need to create your own Python package as described in this [hitchhiker's guide](https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/) to package Python code. We assume it is well known and we won't focus on this aspect. Thus, your package must be impartable by ANTA hence available in `$PYTHONPATH` by any method. +ANTA is not only a Python library with a CLI and a collection of built-in tests, it is also a framework you can extend by building your own tests. ## Generic approach -ANTA comes with a class to use to build test. This class provides all the toolset required to define, collect and test data. The next code is an example of how to use ANTA to build a test - -```python -from __future__ import annotations +A test is a Python class where a test function is defined and will be run by the framework. -import logging -from typing import Any, Dict, List, Optional, cast +ANTA provides an abstract class [AntaTest](../api/models.md#anta.models.AntaTest). This class does the heavy lifting and provide the logic to define, collect and test data. The code below is an example of a simple test in ANTA, which is an [AntaTest](../api/models.md#anta.models.AntaTest) subclass: +```python from anta.models import AntaTest, AntaCommand +from anta.decorators import skip_on_platforms class VerifyTemperature(AntaTest): """ - Verifies device temparture is currently OK. + This test verifies if the device temperature is within acceptable limits. + + Expected Results: + * success: The test will pass if the device temperature is currently OK: 'temperatureOk'. + * failure: The test will fail if the device temperature is NOT OK. """ name = "VerifyTemperature" - description = "Verifies device temparture is currently OK" + description = "Verifies if the device temperature is within the acceptable range." categories = ["hardware"] commands = [AntaCommand(command="show system environment temperature", ofmt="json")] + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: - """Run VerifyTemperature validation""" - command_output = cast(Dict[str, Dict[Any, Any]], self.instance_commands[0].output) + command_output = self.instance_commands[0].json_output temperature_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" if temperature_status == "temperatureOk": self.result.is_success() else: - self.result.is_failure(f"Device temperature is not OK, systemStatus: {temperature_status }") + self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'") ``` -## Python imports +[AntaTest](../api/models.md#anta.models.AntaTest) also provide more advanced capabilities like [AntaCommand](../api/models.md#anta.models.AntaCommand) templating using the [AntaTemplate](../api/models.md#anta.models.AntaTemplate) class or test inputs definition and validation using [AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) [pydantic](https://docs.pydantic.dev/latest/) model. This will be discussed in the sections below. -### Mandatory imports +## [AntaTest](../api/models.md#anta.models.AntaTest) structure -The following elements have to be imported: +### Class Attributes -- [anta.models.AntaTest](../api/models.md#anta.models.AntaTest): class that gives you all the tooling for your test -- [anta.models.AntaCommand](../api/models.md#anta.models.AntaCommand): A class to abstract an Arista EOS command +- `name` (`str`): Name of the test. Used during reporting. +- `description` (`str`): A human readable description of your test. +- `categories` (`list[str]`): A list of categories in which the test belongs. +- `commands` (`list[Union[AntaTemplate, AntaCommand]]`): A list of command to collect from devices. This list __must__ be a list of [AntaCommand](../api/models.md#anta.models.AntaCommand) or [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances. Rendering [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances will be discussed later. -```python -from anta.models import AntaTest, AntaCommand +!!! info + All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation. +### Instance Attributes -class VerifyTemperature(AntaTest): - """ - Verifies device temparture is currently OK. - """ - ... +!!! info + You can access an instance attribute in your code using the `self` reference. E.g. you can access the test input values using `self.inputs`. - @AntaTest.anta_test - def test(self) -> None: - pass -``` +::: anta.models.AntaTest + options: + show_docstring_attributes: true + show_root_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + members: false + show_source: false + show_root_toc_entry: false + heading_level: 10 -### Optional ANTA imports -Besides these 3 main imports, anta provides some additional and optional decorators: +!!! note "Logger object" + ANTA already provides comprehensive logging at every steps of a test execution. The [AntaTest](../api/models.md#anta.models.AntaTest) class also provides a `logger` attribute that is a Python logger specific to the test instance. See [Python documentation](https://docs.python.org/3/library/logging.html) for more information. -- `anta.decorators.skip_on_platforms`: To skip a test for a function not available for some platform -- `anta.decorators.check_bgp_family_enable`: To run tests only if specific BGP family is active. +!!! note "AntaDevice object" + Even if `device` is not a private attribute, you should not need to access this object in your code. +### Test Inputs -```python -from anta.decorators import skip_on_platforms +[AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) is a [pydantic model](https://docs.pydantic.dev/latest/usage/models/) that allow test developers to define their test inputs. [pydantic](https://docs.pydantic.dev/latest/) provides out of the box [error handling](https://docs.pydantic.dev/latest/usage/models/#error-handling) for test input validation based on the type hints defined by the test developer. +The base definition of [AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) provides common test inputs for all [AntaTest](../api/models.md#anta.models.AntaTest) instances: -class VerifyTransceiversManufacturers(AntaTest): - ... - @skip_on_platforms(["cEOSLab", "vEOS-lab"]) - @AntaTest.anta_test - def test(self, manufacturers: Optional[List[str]] = None) -> None: - pass -``` +#### [Input](../api/models.md#anta.models.AntaTest.Input) model -### Optional python imports +::: anta.models.AntaTest.Input + options: + show_docstring_attributes: true + show_root_heading: false + show_category_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + show_source: false + members: false + show_root_toc_entry: false + heading_level: 10 -And finally, you are free to import any other python library you may want to use in your package. +#### [ResultOverwrite](../api/models.md#anta.models.AntaTest.Input.ResultOverwrite) model -!!! info "logging function" - It is strongly recommended to import `logging` to help development process and being able to log some outputs usefull for test development. +::: anta.models.AntaTest.Input.ResultOverwrite + options: + show_docstring_attributes: true + show_root_heading: false + show_category_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + show_source: false + show_root_toc_entry: false + heading_level: 10 -If your test development is part of a pull request for ANTA, it is stringly advised to also import `typing` since our code testing requires to be compatible with Mypy. +!!! note + The pydantic model is configured using the [`extra=forbid`](https://docs.pydantic.dev/latest/usage/model_config/#extra-attributes) that will fail input validation if extra fields are provided. -## Code for a test +### Methods -A test is a python class where a test function is defined and will be run by the framework. So first you need to declare your class and then define your test function. +- [test(self) -> None](../api/models.md#anta.models.AntaTest.test): This is an abstract method that __must__ be implemented. It contains the test logic that can access the collected command outputs using the `instance_commands` instance attribute, access the test inputs using the `inputs` instance attribute and __must__ set the `result` instance attribute accordingly. It must be implemented using the `AntaTest.anta_test` decorator that provides logging and will collect commands before executing the `test()` method. +- [render(self, template: AntaTemplate) -> list[AntaCommand]](../api/models.md#anta.models.AntaTest.render): This method only needs to be implemented if [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances are present in the `commands` class attribute. It will be called for every [AntaTemplate](../api/models.md#anta.models.AntaTemplate) occurence and __must__ return a list of [AntaCommand](../api/models.md#anta.models.AntaCommand) using the [AntaTemplate.render()](../api/models.md#anta.models.AntaTemplate.render) method. It can access test inputs using the `inputs` instance attribute. -### Create Test Class +## Test execution -To create class, you have to provide 4 elements: +Below is a high level description of the test execution flow in ANTA: -__Metadata information__ +1. ANTA will parse the test catalog to get the list of [AntaTest](../api/models.md#anta.models.AntaTest) subclasses to instantiate and their associated input values. We consider a single [AntaTest](../api/models.md#anta.models.AntaTest) subclass in the following steps. -- `name`: Name of the test -- `description`: A human readable description of your test -- `categories`: a list of categories to sort test. +2. ANTA will instantiate the [AntaTest](../api/models.md#anta.models.AntaTest) subclass and a single device will be provided to the test instance. The `Input` model defined in the class will also be instantiated at this moment. If any [ValidationError](https://docs.pydantic.dev/latest/errors/errors/) is raised, the test execution will be stopped. -__Commands to run__ +3. If there is any [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instance in the `commands` class attribute, [render()](../api/models.md#anta.models.AntaTest.render) will be called for every occurrence. At this moment, the `instance_commands` attribute has been initialized. If any rendering error occurs, the test execution will be stopped. -- `commands`: a list of command to run. This list _must_ be a list of `AntaCommand` which is described in the next part of this document. -- `template`: a command template (`AntaTemplate`) to run where variables are provided during test execution. +4. The `AntaTest.anta_test` decorator will collect the commands from the device and update the `instance_commands` attribute with the outputs. If any collection error occurs, the test execution will be stopped. -```python -from __future__ import annotations +5. The [test()](../api/models.md#anta.models.AntaTest.test) method is executed. -import logging -from typing import Any, Dict, List, Optional, cast +## Writing an AntaTest subclass -from anta.models import AntaTest, AntaCommand +In this section, we will go into all the details of writing an [AntaTest](../api/models.md#anta.models.AntaTest) subclass. + +### Class definition + +Import [anta.models.AntaTest](../api/models.md#anta.models.AntaTest) and define your own class. +Define the mandatory class attributes using [anta.models.AntaCommand](../api/models.md#anta.models.AntaCommand), [anta.models.AntaTemplate](../api/models.md#anta.models.AntaTemplate) or both. + +```python +from anta.models import AntaTest, AntaCommand, AntaTemplate class (AntaTest): @@ -135,112 +163,143 @@ class (AntaTest): name = "YourTestName" # should be your class name description = "" - categories = [""] + categories = ["", ""] commands = [ AntaCommand( - command="", + command="", ofmt="", - version="", + version="", + revision="", # revision has precedence over version + ), + AntaTemplate( + template="", + ofmt="", + version="", revision="", # revision has precedence over version ) ] ``` -This class will inherit methods from AntaTest and specfically the `__init__(self,...)` method to build your object. This function takes following arguments when you instantiate an object: - -- `device (InventoryDevice)`: Device object where to test happens. -- `template_params`: If template is used in the test definition, then we provide data to build list of commands. -- `eos_data`: Potential EOS data to pass if we don't want to connect to device to grab data. -- `labels`: a list of labels. It is not used yet and it is for futur use. +### Inputs definition -### Function definition - -The code here can be very simple as well as very complex and will depend of what you expect to do. But in all situation, the same baseline can be leverage: +If the user needs to provide inputs for your test, you need to define a [pydantic model](https://docs.pydantic.dev/latest/usage/models/) that defines the schema of the test inputs: ```python class (AntaTest): ... - @AntaTest.anta_test - def test(self) -> None: - pass + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + : + """""" ``` -If you want to support option in your test, just declare your options in your test method: +To define an input field type, refer to the [pydantic documentation](https://docs.pydantic.dev/latest/usage/types/types/) about types. +You can also leverage [anta.custom_types](../api/types.md) that provides reusable types defined in ANTA tests. + +Regarding required, optional and nullable fields, refer to this [documentation](https://docs.pydantic.dev/latest/migration/#required-optional-and-nullable-fields) on how to define them. + +!!! note + All the `pydantic` features are supported. For instance you can define [validators](https://docs.pydantic.dev/latest/usage/validators/) for complex input validation. + +### Template rendering + +Define the `render()` method if you have [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances in your `commands` class attribute: ```python class (AntaTest): ... - @AntaTest.anta_test - def test(self, my_param1: Optional[str] = None) -> None: - pass + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(