Merging upstream version 1.2.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
ae7b7df396
commit
afeccccd6a
154 changed files with 7346 additions and 5000 deletions
21
.github/workflows/code-testing.yml
vendored
21
.github/workflows/code-testing.yml
vendored
|
@ -46,7 +46,7 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
needs: file-changes
|
needs: file-changes
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -108,7 +108,7 @@ jobs:
|
||||||
needs: [lint-python, type-python]
|
needs: [lint-python, type-python]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python: ["3.9", "3.10", "3.11", "3.12"]
|
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
|
@ -119,6 +119,23 @@ jobs:
|
||||||
run: pip install tox tox-gh-actions
|
run: pip install tox tox-gh-actions
|
||||||
- name: "Run pytest via tox for ${{ matrix.python }}"
|
- name: "Run pytest via tox for ${{ matrix.python }}"
|
||||||
run: tox
|
run: tox
|
||||||
|
test-python-windows:
|
||||||
|
name: Pytest on 3.12 for windows
|
||||||
|
runs-on: windows-2022
|
||||||
|
needs: [lint-python, type-python]
|
||||||
|
env:
|
||||||
|
# Required to prevent asyncssh to fail.
|
||||||
|
USERNAME: WindowsUser
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install tox tox-gh-actions
|
||||||
|
- name: Run pytest via tox for 3.12 on Windows
|
||||||
|
run: tox
|
||||||
test-documentation:
|
test-documentation:
|
||||||
name: Build offline documentation for testing
|
name: Build offline documentation for testing
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
@ -7,8 +7,13 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pypi:
|
pypi:
|
||||||
name: Publish version to Pypi servers
|
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
url: https://pypi.org/p/anta
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -19,11 +24,8 @@ jobs:
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: |
|
run: |
|
||||||
python -m build
|
python -m build
|
||||||
- name: Publish package to Pypi
|
- name: Publish distribution 📦 to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
||||||
|
|
||||||
release-coverage:
|
release-coverage:
|
||||||
name: Updated ANTA release coverage badge
|
name: Updated ANTA release coverage badge
|
||||||
|
|
|
@ -46,7 +46,7 @@ repos:
|
||||||
- '<!--| ~| -->'
|
- '<!--| ~| -->'
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.6.9
|
rev: v0.8.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: Run Ruff linter
|
name: Run Ruff linter
|
||||||
|
@ -55,7 +55,7 @@ repos:
|
||||||
name: Run Ruff formatter
|
name: Run Ruff formatter
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/pylint
|
- repo: https://github.com/pycqa/pylint
|
||||||
rev: "v3.3.1"
|
rev: "v3.3.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pylint
|
- id: pylint
|
||||||
name: Check code style with pylint
|
name: Check code style with pylint
|
||||||
|
@ -85,7 +85,7 @@ repos:
|
||||||
types: [text]
|
types: [text]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.11.2
|
rev: v1.14.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: Check typing with mypy
|
name: Check typing with mypy
|
||||||
|
@ -100,7 +100,7 @@ repos:
|
||||||
files: ^(anta|tests)/
|
files: ^(anta|tests)/
|
||||||
|
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||||
rev: v0.42.0
|
rev: v0.43.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdownlint
|
- id: markdownlint
|
||||||
name: Check Markdown files style.
|
name: Check Markdown files style.
|
||||||
|
@ -108,3 +108,19 @@ repos:
|
||||||
- --config=.github/markdownlint.yaml
|
- --config=.github/markdownlint.yaml
|
||||||
- --ignore-path=.github/markdownlintignore
|
- --ignore-path=.github/markdownlintignore
|
||||||
- --fix
|
- --fix
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: examples-test
|
||||||
|
name: Generate examples/tests.yaml
|
||||||
|
entry: >-
|
||||||
|
sh -c "docs/scripts/generate_examples_tests.py"
|
||||||
|
language: python
|
||||||
|
types: [python]
|
||||||
|
files: anta/
|
||||||
|
verbose: true
|
||||||
|
pass_filenames: false
|
||||||
|
additional_dependencies:
|
||||||
|
- anta[cli]
|
||||||
|
# TODO: next can go once we have it added to anta properly
|
||||||
|
- numpydoc
|
||||||
|
|
|
@ -35,7 +35,7 @@ except ImportError as exc:
|
||||||
|
|
||||||
cli = build_cli(exc)
|
cli = build_cli(exc)
|
||||||
|
|
||||||
__all__ = ["cli", "anta"]
|
__all__ = ["anta", "cli"]
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
|
@ -84,7 +84,10 @@ def snapshot(inventory: AntaInventory, tags: set[str] | None, commands_list: Pat
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--configure",
|
"--configure",
|
||||||
help="Ensure devices have 'aaa authorization exec default local' configured (required for SCP on EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR NETWORK.",
|
help=(
|
||||||
|
"[DEPRECATED] Ensure devices have 'aaa authorization exec default local' configured (required for SCP on EOS). "
|
||||||
|
"THIS WILL CHANGE THE CONFIGURATION OF YOUR NETWORK."
|
||||||
|
),
|
||||||
default=False,
|
default=False,
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
show_default=True,
|
show_default=True,
|
||||||
|
|
|
@ -128,6 +128,13 @@ async def collect_show_tech(inv: AntaInventory, root_dir: Path, *, configure: bo
|
||||||
logger.error("Unable to collect tech-support on %s: configuration 'aaa authorization exec default local' is not present", device.name)
|
logger.error("Unable to collect tech-support on %s: configuration 'aaa authorization exec default local' is not present", device.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# TODO: ANTA 2.0.0
|
||||||
|
msg = (
|
||||||
|
"[DEPRECATED] Using '--configure' for collecting show-techs is deprecated and will be removed in ANTA 2.0.0. "
|
||||||
|
"Please add the required configuration on your devices before running this command from ANTA."
|
||||||
|
)
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
commands = []
|
commands = []
|
||||||
# TODO: @mtache - add `config` field to `AntaCommand` object to handle this use case.
|
# TODO: @mtache - add `config` field to `AntaCommand` object to handle this use case.
|
||||||
# Otherwise mypy complains about enable as it is only implemented for AsyncEOSDevice
|
# Otherwise mypy complains about enable as it is only implemented for AsyncEOSDevice
|
||||||
|
|
|
@ -17,3 +17,4 @@ get.add_command(commands.from_cvp)
|
||||||
get.add_command(commands.from_ansible)
|
get.add_command(commands.from_ansible)
|
||||||
get.add_command(commands.inventory)
|
get.add_command(commands.inventory)
|
||||||
get.add_command(commands.tags)
|
get.add_command(commands.tags)
|
||||||
|
get.add_command(commands.tests)
|
||||||
|
|
|
@ -22,7 +22,7 @@ from anta.cli.console import console
|
||||||
from anta.cli.get.utils import inventory_output_options
|
from anta.cli.get.utils import inventory_output_options
|
||||||
from anta.cli.utils import ExitCode, inventory_options
|
from anta.cli.utils import ExitCode, inventory_options
|
||||||
|
|
||||||
from .utils import create_inventory_from_ansible, create_inventory_from_cvp, get_cv_token
|
from .utils import create_inventory_from_ansible, create_inventory_from_cvp, explore_package, get_cv_token
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from anta.inventory import AntaInventory
|
from anta.inventory import AntaInventory
|
||||||
|
@ -75,7 +75,11 @@ def from_cvp(ctx: click.Context, output: Path, host: str, username: str, passwor
|
||||||
# Get devices under a container
|
# Get devices under a container
|
||||||
logger.info("Getting inventory for container %s from CloudVision instance '%s'", container, host)
|
logger.info("Getting inventory for container %s from CloudVision instance '%s'", container, host)
|
||||||
cvp_inventory = clnt.api.get_devices_in_container(container)
|
cvp_inventory = clnt.api.get_devices_in_container(container)
|
||||||
create_inventory_from_cvp(cvp_inventory, output)
|
try:
|
||||||
|
create_inventory_from_cvp(cvp_inventory, output)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
ctx.exit(ExitCode.USAGE_ERROR)
|
||||||
|
|
||||||
|
|
||||||
@click.command
|
@click.command
|
||||||
|
@ -101,7 +105,7 @@ def from_ansible(ctx: click.Context, output: Path, ansible_group: str, ansible_i
|
||||||
output=output,
|
output=output,
|
||||||
ansible_group=ansible_group,
|
ansible_group=ansible_group,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except (ValueError, OSError) as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
ctx.exit(ExitCode.USAGE_ERROR)
|
ctx.exit(ExitCode.USAGE_ERROR)
|
||||||
|
|
||||||
|
@ -132,3 +136,25 @@ def tags(inventory: AntaInventory, **kwargs: Any) -> None:
|
||||||
tags.update(device.tags)
|
tags.update(device.tags)
|
||||||
console.print("Tags found:")
|
console.print("Tags found:")
|
||||||
console.print_json(json.dumps(sorted(tags), indent=2))
|
console.print_json(json.dumps(sorted(tags), indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command
|
||||||
|
@click.pass_context
|
||||||
|
@click.option("--module", help="Filter tests by module name.", default="anta.tests", show_default=True)
|
||||||
|
@click.option("--test", help="Filter by specific test name. If module is specified, searches only within that module.", type=str)
|
||||||
|
@click.option("--short", help="Display test names without their inputs.", is_flag=True, default=False)
|
||||||
|
@click.option("--count", help="Print only the number of tests found.", is_flag=True, default=False)
|
||||||
|
def tests(ctx: click.Context, module: str, test: str | None, *, short: bool, count: bool) -> None:
|
||||||
|
"""Show all builtin ANTA tests with an example output retrieved from each test documentation."""
|
||||||
|
try:
|
||||||
|
tests_found = explore_package(module, test_name=test, short=short, count=count)
|
||||||
|
if tests_found == 0:
|
||||||
|
console.print(f"""No test {f"'{test}' " if test else ""}found in '{module}'.""")
|
||||||
|
elif count:
|
||||||
|
if tests_found == 1:
|
||||||
|
console.print(f"There is 1 test available in '{module}'.")
|
||||||
|
else:
|
||||||
|
console.print(f"There are {tests_found} tests available in '{module}'.")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
ctx.exit(ExitCode.USAGE_ERROR)
|
||||||
|
|
|
@ -6,8 +6,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import pkgutil
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import stdin
|
from sys import stdin
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
@ -17,9 +23,11 @@ import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from anta.cli.console import console
|
||||||
from anta.cli.utils import ExitCode
|
from anta.cli.utils import ExitCode
|
||||||
from anta.inventory import AntaInventory
|
from anta.inventory import AntaInventory
|
||||||
from anta.inventory.models import AntaInventoryHost, AntaInventoryInput
|
from anta.inventory.models import AntaInventoryHost, AntaInventoryInput
|
||||||
|
from anta.models import AntaTest
|
||||||
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
@ -114,11 +122,28 @@ def get_cv_token(cvp_ip: str, cvp_username: str, cvp_password: str, *, verify_ce
|
||||||
|
|
||||||
|
|
||||||
def write_inventory_to_file(hosts: list[AntaInventoryHost], output: Path) -> None:
|
def write_inventory_to_file(hosts: list[AntaInventoryHost], output: Path) -> None:
|
||||||
"""Write a file inventory from pydantic models."""
|
"""Write a file inventory from pydantic models.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hosts:
|
||||||
|
the list of AntaInventoryHost to write to an inventory file
|
||||||
|
output:
|
||||||
|
the Path where the inventory should be written.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
OSError
|
||||||
|
When anything goes wrong while writing the file.
|
||||||
|
"""
|
||||||
i = AntaInventoryInput(hosts=hosts)
|
i = AntaInventoryInput(hosts=hosts)
|
||||||
with output.open(mode="w", encoding="UTF-8") as out_fd:
|
try:
|
||||||
out_fd.write(yaml.dump({AntaInventory.INVENTORY_ROOT_KEY: yaml.safe_load(i.yaml())}))
|
with output.open(mode="w", encoding="UTF-8") as out_fd:
|
||||||
logger.info("ANTA inventory file has been created: '%s'", output)
|
out_fd.write(yaml.dump({AntaInventory.INVENTORY_ROOT_KEY: yaml.safe_load(i.yaml())}))
|
||||||
|
logger.info("ANTA inventory file has been created: '%s'", output)
|
||||||
|
except OSError as exc:
|
||||||
|
msg = f"Could not write inventory to path '{output}'."
|
||||||
|
raise OSError(msg) from exc
|
||||||
|
|
||||||
|
|
||||||
def create_inventory_from_cvp(inv: list[dict[str, Any]], output: Path) -> None:
|
def create_inventory_from_cvp(inv: list[dict[str, Any]], output: Path) -> None:
|
||||||
|
@ -204,3 +229,148 @@ def create_inventory_from_ansible(inventory: Path, output: Path, ansible_group:
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
ansible_hosts = deep_yaml_parsing(ansible_inventory)
|
ansible_hosts = deep_yaml_parsing(ansible_inventory)
|
||||||
write_inventory_to_file(ansible_hosts, output)
|
write_inventory_to_file(ansible_hosts, output)
|
||||||
|
|
||||||
|
|
||||||
|
def explore_package(module_name: str, test_name: str | None = None, *, short: bool = False, count: bool = False) -> int:
|
||||||
|
"""Parse ANTA test submodules recursively and print AntaTest examples.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
module_name
|
||||||
|
Name of the module to explore (e.g., 'anta.tests.routing.bgp').
|
||||||
|
test_name
|
||||||
|
If provided, only show tests starting with this name.
|
||||||
|
short
|
||||||
|
If True, only print test names without their inputs.
|
||||||
|
count
|
||||||
|
If True, only count the tests.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int:
|
||||||
|
The number of tests found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
module_spec = importlib.util.find_spec(module_name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
# Relying on module_spec check below.
|
||||||
|
module_spec = None
|
||||||
|
except ImportError as e:
|
||||||
|
msg = "`anta get tests --module <module>` does not support relative imports"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
# Giving a second chance adding CWD to PYTHONPATH
|
||||||
|
if module_spec is None:
|
||||||
|
try:
|
||||||
|
logger.info("Could not find module `%s`, injecting CWD in PYTHONPATH and retrying...", module_name)
|
||||||
|
sys.path = [str(Path.cwd()), *sys.path]
|
||||||
|
module_spec = importlib.util.find_spec(module_name)
|
||||||
|
except ImportError:
|
||||||
|
module_spec = None
|
||||||
|
|
||||||
|
if module_spec is None or module_spec.origin is None:
|
||||||
|
msg = f"Module `{module_name}` was not found!"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
tests_found = 0
|
||||||
|
if module_spec.submodule_search_locations:
|
||||||
|
for _, sub_module_name, ispkg in pkgutil.walk_packages(module_spec.submodule_search_locations):
|
||||||
|
qname = f"{module_name}.{sub_module_name}"
|
||||||
|
if ispkg:
|
||||||
|
tests_found += explore_package(qname, test_name=test_name, short=short, count=count)
|
||||||
|
continue
|
||||||
|
tests_found += find_tests_examples(qname, test_name, short=short, count=count)
|
||||||
|
|
||||||
|
else:
|
||||||
|
tests_found += find_tests_examples(module_spec.name, test_name, short=short, count=count)
|
||||||
|
|
||||||
|
return tests_found
|
||||||
|
|
||||||
|
|
||||||
|
def find_tests_examples(qname: str, test_name: str | None, *, short: bool = False, count: bool = False) -> int:
|
||||||
|
"""Print tests from `qname`, filtered by `test_name` if provided.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
qname
|
||||||
|
Name of the module to explore (e.g., 'anta.tests.routing.bgp').
|
||||||
|
test_name
|
||||||
|
If provided, only show tests starting with this name.
|
||||||
|
short
|
||||||
|
If True, only print test names without their inputs.
|
||||||
|
count
|
||||||
|
If True, only count the tests.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int:
|
||||||
|
The number of tests found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
qname_module = importlib.import_module(qname)
|
||||||
|
except (AssertionError, ImportError) as e:
|
||||||
|
msg = f"Error when importing `{qname}` using importlib!"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
module_printed = False
|
||||||
|
tests_found = 0
|
||||||
|
|
||||||
|
for _name, obj in inspect.getmembers(qname_module):
|
||||||
|
# Only retrieves the subclasses of AntaTest
|
||||||
|
if not inspect.isclass(obj) or not issubclass(obj, AntaTest) or obj == AntaTest:
|
||||||
|
continue
|
||||||
|
if test_name and not obj.name.startswith(test_name):
|
||||||
|
continue
|
||||||
|
if not module_printed:
|
||||||
|
if not count:
|
||||||
|
console.print(f"{qname}:")
|
||||||
|
module_printed = True
|
||||||
|
tests_found += 1
|
||||||
|
if count:
|
||||||
|
continue
|
||||||
|
print_test(obj, short=short)
|
||||||
|
|
||||||
|
return tests_found
|
||||||
|
|
||||||
|
|
||||||
|
def print_test(test: type[AntaTest], *, short: bool = False) -> None:
|
||||||
|
"""Print a single test.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
test
|
||||||
|
the representation of the AntaTest as returned by inspect.getmembers
|
||||||
|
short
|
||||||
|
If True, only print test names without their inputs.
|
||||||
|
"""
|
||||||
|
if not test.__doc__ or (example := extract_examples(test.__doc__)) is None:
|
||||||
|
msg = f"Test {test.name} in module {test.__module__} is missing an Example"
|
||||||
|
raise LookupError(msg)
|
||||||
|
# Picking up only the inputs in the examples
|
||||||
|
# Need to handle the fact that we nest the routing modules in Examples.
|
||||||
|
# This is a bit fragile.
|
||||||
|
inputs = example.split("\n")
|
||||||
|
try:
|
||||||
|
test_name_line = next((i for i, input_entry in enumerate(inputs) if test.name in input_entry))
|
||||||
|
except StopIteration as e:
|
||||||
|
msg = f"Could not find the name of the test '{test.name}' in the Example section in the docstring."
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
# TODO: handle not found
|
||||||
|
console.print(f" {inputs[test_name_line].strip()}")
|
||||||
|
# Injecting the description
|
||||||
|
console.print(f" # {test.description}", soft_wrap=True)
|
||||||
|
if not short and len(inputs) > test_name_line + 2: # There are params
|
||||||
|
console.print(textwrap.indent(textwrap.dedent("\n".join(inputs[test_name_line + 1 : -1])), " " * 6))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_examples(docstring: str) -> str | None:
|
||||||
|
"""Extract the content of the Example section in a Numpy docstring.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str | None
|
||||||
|
The content of the section if present, None if the section is absent or empty.
|
||||||
|
"""
|
||||||
|
pattern = r"Examples\s*--------\s*(.*)(?:\n\s*\n|\Z)"
|
||||||
|
match = re.search(pattern, docstring, flags=re.DOTALL)
|
||||||
|
return match[1].strip() if match and match[1].strip() != "" else None
|
||||||
|
|
|
@ -116,8 +116,12 @@ def print_text(ctx: click.Context) -> None:
|
||||||
"""Print results as simple text."""
|
"""Print results as simple text."""
|
||||||
console.print()
|
console.print()
|
||||||
for test in _get_result_manager(ctx).results:
|
for test in _get_result_manager(ctx).results:
|
||||||
message = f" ({test.messages[0]!s})" if len(test.messages) > 0 else ""
|
if len(test.messages) <= 1:
|
||||||
console.print(f"{test.name} :: {test.test} :: [{test.result}]{test.result.upper()}[/{test.result}]{message}", highlight=False)
|
message = test.messages[0] if len(test.messages) == 1 else ""
|
||||||
|
console.print(f"{test.name} :: {test.test} :: [{test.result}]{test.result.upper()}[/{test.result}]({message})", highlight=False)
|
||||||
|
else: # len(test.messages) > 1
|
||||||
|
console.print(f"{test.name} :: {test.test} :: [{test.result}]{test.result.upper()}[/{test.result}]", highlight=False)
|
||||||
|
console.print("\n".join(f" {message}" for message in test.messages), highlight=False)
|
||||||
|
|
||||||
|
|
||||||
def print_jinja(results: ResultManager, template: pathlib.Path, output: pathlib.Path | None = None) -> None:
|
def print_jinja(results: ResultManager, template: pathlib.Path, output: pathlib.Path | None = None) -> None:
|
||||||
|
|
|
@ -17,3 +17,12 @@ MD_REPORT_TOC = """**Table of Contents:**
|
||||||
- [Summary Totals Per Category](#summary-totals-per-category)
|
- [Summary Totals Per Category](#summary-totals-per-category)
|
||||||
- [Test Results](#test-results)"""
|
- [Test Results](#test-results)"""
|
||||||
"""Table of Contents for the Markdown report."""
|
"""Table of Contents for the Markdown report."""
|
||||||
|
|
||||||
|
KNOWN_EOS_ERRORS = [
|
||||||
|
r"BGP inactive",
|
||||||
|
r"VRF '.*' is not active",
|
||||||
|
r".* does not support IP",
|
||||||
|
r"IS-IS (.*) is disabled because: .*",
|
||||||
|
r"No source interface .*",
|
||||||
|
]
|
||||||
|
"""List of known EOS errors that should set a test status to 'failure' with the error message."""
|
||||||
|
|
|
@ -208,3 +208,33 @@ SnmpPdu = Literal["inGetPdus", "inGetNextPdus", "inSetPdus", "outGetResponsePdus
|
||||||
SnmpErrorCounter = Literal[
|
SnmpErrorCounter = Literal[
|
||||||
"inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs"
|
"inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
IPv4RouteType = Literal[
|
||||||
|
"connected",
|
||||||
|
"static",
|
||||||
|
"kernel",
|
||||||
|
"OSPF",
|
||||||
|
"OSPF inter area",
|
||||||
|
"OSPF external type 1",
|
||||||
|
"OSPF external type 2",
|
||||||
|
"OSPF NSSA external type 1",
|
||||||
|
"OSPF NSSA external type2",
|
||||||
|
"Other BGP Routes",
|
||||||
|
"iBGP",
|
||||||
|
"eBGP",
|
||||||
|
"RIP",
|
||||||
|
"IS-IS level 1",
|
||||||
|
"IS-IS level 2",
|
||||||
|
"OSPFv3",
|
||||||
|
"BGP Aggregate",
|
||||||
|
"OSPF Summary",
|
||||||
|
"Nexthop Group Static Route",
|
||||||
|
"VXLAN Control Service",
|
||||||
|
"Martian",
|
||||||
|
"DHCP client installed default route",
|
||||||
|
"Dynamic Policy Route",
|
||||||
|
"VRF Leaked",
|
||||||
|
"gRIBI",
|
||||||
|
"Route Cache Route",
|
||||||
|
"CBF Leaked Route",
|
||||||
|
]
|
||||||
|
|
|
@ -17,7 +17,8 @@ if TYPE_CHECKING:
|
||||||
F = TypeVar("F", bound=Callable[..., Any])
|
F = TypeVar("F", bound=Callable[..., Any])
|
||||||
|
|
||||||
|
|
||||||
def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]:
|
# TODO: Remove this decorator in ANTA v2.0.0 in favor of deprecated_test_class
|
||||||
|
def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]: # pragma: no cover
|
||||||
"""Return a decorator to log a message of WARNING severity when a test is deprecated.
|
"""Return a decorator to log a message of WARNING severity when a test is deprecated.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -62,6 +63,57 @@ def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated_test_class(new_tests: list[str] | None = None, removal_in_version: str | None = None) -> Callable[[type[AntaTest]], type[AntaTest]]:
|
||||||
|
"""Return a decorator to log a message of WARNING severity when a test is deprecated.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
new_tests
|
||||||
|
A list of new test classes that should replace the deprecated test.
|
||||||
|
removal_in_version
|
||||||
|
A string indicating the version in which the test will be removed.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Callable[[type], type]
|
||||||
|
A decorator that can be used to wrap test functions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(cls: type[AntaTest]) -> type[AntaTest]:
|
||||||
|
"""Actual decorator that logs the message.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cls
|
||||||
|
The cls to be decorated.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
cls
|
||||||
|
The decorated cls.
|
||||||
|
"""
|
||||||
|
orig_init = cls.__init__
|
||||||
|
|
||||||
|
def new_init(*args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Overload __init__ to generate a warning message for deprecation."""
|
||||||
|
if new_tests:
|
||||||
|
new_test_names = ", ".join(new_tests)
|
||||||
|
logger.warning("%s test is deprecated. Consider using the following new tests: %s.", cls.name, new_test_names)
|
||||||
|
else:
|
||||||
|
logger.warning("%s test is deprecated.", cls.name)
|
||||||
|
orig_init(*args, **kwargs)
|
||||||
|
|
||||||
|
if removal_in_version is not None:
|
||||||
|
cls.__removal_in_version = removal_in_version
|
||||||
|
|
||||||
|
# NOTE: we are ignoring mypy warning as we want to assign to a method here
|
||||||
|
cls.__init__ = new_init # type: ignore[method-assign]
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def skip_on_platforms(platforms: list[str]) -> Callable[[F], F]:
|
def skip_on_platforms(platforms: list[str]) -> Callable[[F], F]:
|
||||||
"""Return a decorator to skip a test based on the device's hardware model.
|
"""Return a decorator to skip a test based on the device's hardware model.
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,7 @@ class AsyncEOSDevice(AntaDevice):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
host: str,
|
host: str,
|
||||||
username: str,
|
username: str,
|
||||||
|
@ -372,7 +372,7 @@ class AsyncEOSDevice(AntaDevice):
|
||||||
"""
|
"""
|
||||||
return (self._session.host, self._session.port)
|
return (self._session.host, self._session.port)
|
||||||
|
|
||||||
async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks
|
async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:
|
||||||
"""Collect device command output from EOS using aio-eapi.
|
"""Collect device command output from EOS using aio-eapi.
|
||||||
|
|
||||||
Supports outformat `json` and `text` as output structure.
|
Supports outformat `json` and `text` as output structure.
|
||||||
|
@ -409,15 +409,7 @@ class AsyncEOSDevice(AntaDevice):
|
||||||
command.output = response[-1]
|
command.output = response[-1]
|
||||||
except asynceapi.EapiCommandError as e:
|
except asynceapi.EapiCommandError as e:
|
||||||
# This block catches exceptions related to EOS issuing an error.
|
# This block catches exceptions related to EOS issuing an error.
|
||||||
command.errors = e.errors
|
self._log_eapi_command_error(command, e)
|
||||||
if command.requires_privileges:
|
|
||||||
logger.error(
|
|
||||||
"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.", command.command, self.name
|
|
||||||
)
|
|
||||||
if command.supported:
|
|
||||||
logger.error("Command '%s' failed on %s: %s", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)
|
|
||||||
else:
|
|
||||||
logger.debug("Command '%s' is not supported on '%s' (%s)", command.command, self.name, self.hw_model)
|
|
||||||
except TimeoutException as e:
|
except TimeoutException as e:
|
||||||
# This block catches Timeout exceptions.
|
# This block catches Timeout exceptions.
|
||||||
command.errors = [exc_to_str(e)]
|
command.errors = [exc_to_str(e)]
|
||||||
|
@ -446,6 +438,18 @@ class AsyncEOSDevice(AntaDevice):
|
||||||
anta_log_exception(e, f"An error occurred while issuing an eAPI request to {self.name}", logger)
|
anta_log_exception(e, f"An error occurred while issuing an eAPI request to {self.name}", logger)
|
||||||
logger.debug("%s: %s", self.name, command)
|
logger.debug("%s: %s", self.name, command)
|
||||||
|
|
||||||
|
def _log_eapi_command_error(self, command: AntaCommand, e: asynceapi.EapiCommandError) -> None:
|
||||||
|
"""Appropriately log the eapi command error."""
|
||||||
|
command.errors = e.errors
|
||||||
|
if command.requires_privileges:
|
||||||
|
logger.error("Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.", command.command, self.name)
|
||||||
|
if not command.supported:
|
||||||
|
logger.debug("Command '%s' is not supported on '%s' (%s)", command.command, self.name, self.hw_model)
|
||||||
|
elif command.returned_known_eos_error:
|
||||||
|
logger.debug("Command '%s' returned a known error '%s': %s", command.command, self.name, command.errors)
|
||||||
|
else:
|
||||||
|
logger.error("Command '%s' failed on %s: %s", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)
|
||||||
|
|
||||||
async def refresh(self) -> None:
|
async def refresh(self) -> None:
|
||||||
"""Update attributes of an AsyncEOSDevice instance.
|
"""Update attributes of an AsyncEOSDevice instance.
|
||||||
|
|
||||||
|
|
4
anta/input_models/__init__.py
Normal file
4
anta/input_models/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Package related to all ANTA tests input models."""
|
36
anta/input_models/avt.py
Normal file
36
anta/input_models/avt.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for AVT tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class AVTPath(BaseModel):
|
||||||
|
"""AVT (Adaptive Virtual Topology) model representing path details and associated information."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
vrf: str = "default"
|
||||||
|
"""VRF context. Defaults to `default`."""
|
||||||
|
avt_name: str
|
||||||
|
"""The name of the Adaptive Virtual Topology (AVT)."""
|
||||||
|
destination: IPv4Address
|
||||||
|
"""The IPv4 address of the destination peer in the AVT."""
|
||||||
|
next_hop: IPv4Address
|
||||||
|
"""The IPv4 address of the next hop used to reach the AVT peer."""
|
||||||
|
path_type: str | None = None
|
||||||
|
"""Specifies the type of path for the AVT. If not specified, both types 'direct' and 'multihop' are considered."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the AVTPath for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
AVT CONTROL-PLANE-PROFILE VRF: default (Destination: 10.101.255.2, Next-hop: 10.101.255.1)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return f"AVT {self.avt_name} VRF: {self.vrf} (Destination: {self.destination}, Next-hop: {self.next_hop})"
|
37
anta/input_models/bfd.py
Normal file
37
anta/input_models/bfd.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for BFD tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from anta.custom_types import BfdInterval, BfdMultiplier, BfdProtocol
|
||||||
|
|
||||||
|
|
||||||
|
class BFDPeer(BaseModel):
|
||||||
|
"""BFD (Bidirectional Forwarding Detection) model representing the peer details.
|
||||||
|
|
||||||
|
Only IPv4 peers are supported for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
peer_address: IPv4Address
|
||||||
|
"""IPv4 address of a BFD peer."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""Optional VRF for the BFD peer. Defaults to `default`."""
|
||||||
|
tx_interval: BfdInterval | None = None
|
||||||
|
"""Tx interval of BFD peer in milliseconds. Required field in the `VerifyBFDPeersIntervals` test."""
|
||||||
|
rx_interval: BfdInterval | None = None
|
||||||
|
"""Rx interval of BFD peer in milliseconds. Required field in the `VerifyBFDPeersIntervals` test."""
|
||||||
|
multiplier: BfdMultiplier | None = None
|
||||||
|
"""Multiplier of BFD peer. Required field in the `VerifyBFDPeersIntervals` test."""
|
||||||
|
protocols: list[BfdProtocol] | None = None
|
||||||
|
"""List of protocols to be verified. Required field in the `VerifyBFDPeersRegProtocols` test."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the BFDPeer for reporting."""
|
||||||
|
return f"Peer: {self.peer_address} VRF: {self.vrf}"
|
83
anta/input_models/connectivity.py
Normal file
83
anta/input_models/connectivity.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for connectivity tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
from typing import Any
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from anta.custom_types import Interface
|
||||||
|
|
||||||
|
|
||||||
|
class Host(BaseModel):
|
||||||
|
"""Model for a remote host to ping."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
destination: IPv4Address
|
||||||
|
"""IPv4 address to ping."""
|
||||||
|
source: IPv4Address | Interface
|
||||||
|
"""IPv4 address source IP or egress interface to use."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""VRF context. Defaults to `default`."""
|
||||||
|
repeat: int = 2
|
||||||
|
"""Number of ping repetition. Defaults to 2."""
|
||||||
|
size: int = 100
|
||||||
|
"""Specify datagram size. Defaults to 100."""
|
||||||
|
df_bit: bool = False
|
||||||
|
"""Enable do not fragment bit in IP header. Defaults to False."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the Host for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)
|
||||||
|
|
||||||
|
"""
|
||||||
|
df_status = ", df-bit: enabled" if self.df_bit else ""
|
||||||
|
return f"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})"
|
||||||
|
|
||||||
|
|
||||||
|
class LLDPNeighbor(BaseModel):
|
||||||
|
"""LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
port: Interface
|
||||||
|
"""The LLDP port for the local device."""
|
||||||
|
neighbor_device: str
|
||||||
|
"""The system name of the LLDP neighbor device."""
|
||||||
|
neighbor_port: Interface
|
||||||
|
"""The LLDP port on the neighboring device."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the LLDPNeighbor for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return f"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})"
|
||||||
|
|
||||||
|
|
||||||
|
class Neighbor(LLDPNeighbor): # pragma: no cover
|
||||||
|
"""Alias for the LLDPNeighbor model to maintain backward compatibility.
|
||||||
|
|
||||||
|
When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.
|
||||||
|
|
||||||
|
TODO: Remove this class in ANTA v2.0.0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **data: Any) -> None: # noqa: ANN401
|
||||||
|
"""Initialize the LLDPNeighbor class, emitting a depreciation warning."""
|
||||||
|
warn(
|
||||||
|
message="Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
super().__init__(**data)
|
19
anta/input_models/cvx.py
Normal file
19
anta/input_models/cvx.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for CVX tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from anta.custom_types import Hostname
|
||||||
|
|
||||||
|
|
||||||
|
class CVXPeers(BaseModel):
|
||||||
|
"""Model for a CVX Cluster Peer."""
|
||||||
|
|
||||||
|
peer_name: Hostname
|
||||||
|
registration_state: Literal["Connecting", "Connected", "Registration error", "Registration complete", "Unexpected peer state"] = "Registration complete"
|
48
anta/input_models/interfaces.py
Normal file
48
anta/input_models/interfaces.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for interface tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from anta.custom_types import Interface, PortChannelInterface
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceState(BaseModel):
|
||||||
|
"""Model for an interface state."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
name: Interface
|
||||||
|
"""Interface to validate."""
|
||||||
|
status: Literal["up", "down", "adminDown"] | None = None
|
||||||
|
"""Expected status of the interface. Required field in the `VerifyInterfacesStatus` test."""
|
||||||
|
line_protocol_status: Literal["up", "down", "testing", "unknown", "dormant", "notPresent", "lowerLayerDown"] | None = None
|
||||||
|
"""Expected line protocol status of the interface. Optional field in the `VerifyInterfacesStatus` test."""
|
||||||
|
portchannel: PortChannelInterface | None = None
|
||||||
|
"""Port-Channel in which the interface is bundled. Required field in the `VerifyLACPInterfacesStatus` test."""
|
||||||
|
lacp_rate_fast: bool = False
|
||||||
|
"""Specifies the LACP timeout mode for the link aggregation group.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- True: Also referred to as fast mode.
|
||||||
|
- False: The default mode, also known as slow mode.
|
||||||
|
|
||||||
|
Can be enabled in the `VerifyLACPInterfacesStatus` tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the InterfaceState for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
- Interface: Ethernet1 Port-Channel: Port-Channel100
|
||||||
|
- Interface: Ethernet1
|
||||||
|
"""
|
||||||
|
base_string = f"Interface: {self.name}"
|
||||||
|
if self.portchannel is not None:
|
||||||
|
base_string += f" Port-Channel: {self.portchannel}"
|
||||||
|
return base_string
|
4
anta/input_models/routing/__init__.py
Normal file
4
anta/input_models/routing/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Package related to routing tests input models."""
|
209
anta/input_models/routing/bgp.py
Normal file
209
anta/input_models/routing/bgp.py
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for routing BGP tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address, IPv4Network, IPv6Address
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator
|
||||||
|
from pydantic_extra_types.mac_address import MacAddress
|
||||||
|
|
||||||
|
from anta.custom_types import Afi, BgpDropStats, BgpUpdateError, MultiProtocolCaps, Safi, Vni
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
from typing import Self
|
||||||
|
else:
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
AFI_SAFI_EOS_KEY = {
|
||||||
|
("ipv4", "unicast"): "ipv4Unicast",
|
||||||
|
("ipv4", "multicast"): "ipv4Multicast",
|
||||||
|
("ipv4", "labeled-unicast"): "ipv4MplsLabels",
|
||||||
|
("ipv4", "sr-te"): "ipv4SrTe",
|
||||||
|
("ipv6", "unicast"): "ipv6Unicast",
|
||||||
|
("ipv6", "multicast"): "ipv6Multicast",
|
||||||
|
("ipv6", "labeled-unicast"): "ipv6MplsLabels",
|
||||||
|
("ipv6", "sr-te"): "ipv6SrTe",
|
||||||
|
("vpn-ipv4", None): "ipv4MplsVpn",
|
||||||
|
("vpn-ipv6", None): "ipv6MplsVpn",
|
||||||
|
("evpn", None): "l2VpnEvpn",
|
||||||
|
("rt-membership", None): "rtMembership",
|
||||||
|
("path-selection", None): "dps",
|
||||||
|
("link-state", None): "linkState",
|
||||||
|
}
|
||||||
|
"""Dictionary mapping AFI/SAFI to EOS key representation."""
|
||||||
|
|
||||||
|
|
||||||
|
class BgpAddressFamily(BaseModel):
|
||||||
|
"""Model for a BGP address family."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
afi: Afi
|
||||||
|
"""BGP Address Family Identifier (AFI)."""
|
||||||
|
safi: Safi | None = None
|
||||||
|
"""BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.
|
||||||
|
|
||||||
|
If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.
|
||||||
|
|
||||||
|
These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.
|
||||||
|
"""
|
||||||
|
num_peers: PositiveInt | None = None
|
||||||
|
"""Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test."""
|
||||||
|
peers: list[IPv4Address | IPv6Address] | None = None
|
||||||
|
"""List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test."""
|
||||||
|
check_tcp_queues: bool = True
|
||||||
|
"""Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.
|
||||||
|
|
||||||
|
Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.
|
||||||
|
"""
|
||||||
|
check_peer_state: bool = False
|
||||||
|
"""Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.
|
||||||
|
|
||||||
|
Can be enabled in the `VerifyBGPPeerCount` tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_inputs(self) -> Self:
|
||||||
|
"""Validate the inputs provided to the BgpAddressFamily class.
|
||||||
|
|
||||||
|
If `afi` is either `ipv4` or `ipv6`, `safi` must be provided.
|
||||||
|
|
||||||
|
If `afi` is not `ipv4` or `ipv6`, `safi` must NOT be provided and `vrf` must be `default`.
|
||||||
|
"""
|
||||||
|
if self.afi in ["ipv4", "ipv6"]:
|
||||||
|
if self.safi is None:
|
||||||
|
msg = "'safi' must be provided when afi is ipv4 or ipv6"
|
||||||
|
raise ValueError(msg)
|
||||||
|
elif self.safi is not None:
|
||||||
|
msg = "'safi' must not be provided when afi is not ipv4 or ipv6"
|
||||||
|
raise ValueError(msg)
|
||||||
|
elif self.vrf != "default":
|
||||||
|
msg = "'vrf' must be default when afi is not ipv4 or ipv6"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eos_key(self) -> str:
|
||||||
|
"""AFI/SAFI EOS key representation."""
|
||||||
|
# Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.
|
||||||
|
return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the BgpAddressFamily for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
- AFI:ipv4 SAFI:unicast VRF:default
|
||||||
|
- AFI:evpn
|
||||||
|
"""
|
||||||
|
base_string = f"AFI: {self.afi}"
|
||||||
|
if self.safi is not None:
|
||||||
|
base_string += f" SAFI: {self.safi}"
|
||||||
|
if self.afi in ["ipv4", "ipv6"]:
|
||||||
|
base_string += f" VRF: {self.vrf}"
|
||||||
|
return base_string
|
||||||
|
|
||||||
|
|
||||||
|
class BgpAfi(BgpAddressFamily): # pragma: no cover
|
||||||
|
"""Alias for the BgpAddressFamily model to maintain backward compatibility.
|
||||||
|
|
||||||
|
When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.
|
||||||
|
|
||||||
|
TODO: Remove this class in ANTA v2.0.0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **data: Any) -> None: # noqa: ANN401
|
||||||
|
"""Initialize the BgpAfi class, emitting a deprecation warning."""
|
||||||
|
warn(
|
||||||
|
message="BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
super().__init__(**data)
|
||||||
|
|
||||||
|
|
||||||
|
class BgpPeer(BaseModel):
|
||||||
|
"""Model for a BGP peer.
|
||||||
|
|
||||||
|
Only IPv4 peers are supported for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
peer_address: IPv4Address
|
||||||
|
"""IPv4 address of the BGP peer."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""Optional VRF for the BGP peer. Defaults to `default`."""
|
||||||
|
advertised_routes: list[IPv4Network] | None = None
|
||||||
|
"""List of advertised routes in CIDR format. Required field in the `VerifyBGPExchangedRoutes` test."""
|
||||||
|
received_routes: list[IPv4Network] | None = None
|
||||||
|
"""List of received routes in CIDR format. Required field in the `VerifyBGPExchangedRoutes` test."""
|
||||||
|
capabilities: list[MultiProtocolCaps] | None = None
|
||||||
|
"""List of BGP multiprotocol capabilities. Required field in the `VerifyBGPPeerMPCaps` test."""
|
||||||
|
strict: bool = False
|
||||||
|
"""If True, requires exact match of the provided BGP multiprotocol capabilities.
|
||||||
|
|
||||||
|
Optional field in the `VerifyBGPPeerMPCaps` test. Defaults to False."""
|
||||||
|
hold_time: int | None = Field(default=None, ge=3, le=7200)
|
||||||
|
"""BGP hold time in seconds. Required field in the `VerifyBGPTimers` test."""
|
||||||
|
keep_alive_time: int | None = Field(default=None, ge=0, le=3600)
|
||||||
|
"""BGP keepalive time in seconds. Required field in the `VerifyBGPTimers` test."""
|
||||||
|
drop_stats: list[BgpDropStats] | None = None
|
||||||
|
"""List of drop statistics to be verified.
|
||||||
|
|
||||||
|
Optional field in the `VerifyBGPPeerDropStats` test. If not provided, the test will verifies all drop statistics."""
|
||||||
|
update_errors: list[BgpUpdateError] | None = None
|
||||||
|
"""List of update error counters to be verified.
|
||||||
|
|
||||||
|
Optional field in the `VerifyBGPPeerUpdateErrors` test. If not provided, the test will verifies all the update error counters."""
|
||||||
|
inbound_route_map: str | None = None
|
||||||
|
"""Inbound route map applied, defaults to None. Required field in the `VerifyBgpRouteMaps` test."""
|
||||||
|
outbound_route_map: str | None = None
|
||||||
|
"""Outbound route map applied, defaults to None. Required field in the `VerifyBgpRouteMaps` test."""
|
||||||
|
maximum_routes: int | None = Field(default=None, ge=0, le=4294967294)
|
||||||
|
"""The maximum allowable number of BGP routes, `0` means unlimited. Required field in the `VerifyBGPPeerRouteLimit` test"""
|
||||||
|
warning_limit: int | None = Field(default=None, ge=0, le=4294967294)
|
||||||
|
"""Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the BgpPeer for reporting."""
|
||||||
|
return f"Peer: {self.peer_address} VRF: {self.vrf}"
|
||||||
|
|
||||||
|
|
||||||
|
class BgpNeighbor(BgpPeer): # pragma: no cover
|
||||||
|
"""Alias for the BgpPeer model to maintain backward compatibility.
|
||||||
|
|
||||||
|
When initialised, it will emit a deprecation warning and call the BgpPeer model.
|
||||||
|
|
||||||
|
TODO: Remove this class in ANTA v2.0.0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **data: Any) -> None: # noqa: ANN401
|
||||||
|
"""Initialize the BgpPeer class, emitting a depreciation warning."""
|
||||||
|
warn(
|
||||||
|
message="BgpNeighbor model is deprecated and will be removed in ANTA v2.0.0. Use the BgpPeer model instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
super().__init__(**data)
|
||||||
|
|
||||||
|
|
||||||
|
class VxlanEndpoint(BaseModel):
|
||||||
|
"""Model for a VXLAN endpoint."""
|
||||||
|
|
||||||
|
address: IPv4Address | MacAddress
|
||||||
|
"""IPv4 or MAC address of the VXLAN endpoint."""
|
||||||
|
vni: Vni
|
||||||
|
"""VNI of the VXLAN endpoint."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the VxlanEndpoint for reporting."""
|
||||||
|
return f"Address: {self.address} VNI: {self.vni}"
|
28
anta/input_models/routing/generic.py
Normal file
28
anta/input_models/routing/generic.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for generic routing tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Network
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from anta.custom_types import IPv4RouteType
|
||||||
|
|
||||||
|
|
||||||
|
class IPv4Routes(BaseModel):
|
||||||
|
"""Model for a list of IPV4 route entries."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
prefix: IPv4Network
|
||||||
|
"""The IPV4 network to validate the route type."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""VRF context. Defaults to `default` VRF."""
|
||||||
|
route_type: IPv4RouteType
|
||||||
|
"""List of IPV4 Route type to validate the valid rout type."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the IPv4RouteType for reporting."""
|
||||||
|
return f"Prefix: {self.prefix} VRF: {self.vrf}"
|
61
anta/input_models/security.py
Normal file
61
anta/input_models/security.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for security tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
from typing import Any
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class IPSecPeer(BaseModel):
|
||||||
|
"""IPSec (Internet Protocol Security) model represents the details of an IPv4 security peer."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
peer: IPv4Address
|
||||||
|
"""The IPv4 address of the security peer."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""VRF context. Defaults to `default`."""
|
||||||
|
connections: list[IPSecConn] | None = None
|
||||||
|
"""A list of IPv4 security connections associated with the peer. Defaults to None."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a string representation of the IPSecPeer model. Used in failure messages.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
- Peer: 1.1.1.1 VRF: default
|
||||||
|
"""
|
||||||
|
return f"Peer: {self.peer} VRF: {self.vrf}"
|
||||||
|
|
||||||
|
|
||||||
|
class IPSecConn(BaseModel):
|
||||||
|
"""Details of an IPv4 security connection for a peer."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
source_address: IPv4Address
|
||||||
|
"""The IPv4 address of the source in the security connection."""
|
||||||
|
destination_address: IPv4Address
|
||||||
|
"""The IPv4 address of the destination in the security connection."""
|
||||||
|
|
||||||
|
|
||||||
|
class IPSecPeers(IPSecPeer): # pragma: no cover
|
||||||
|
"""Alias for the IPSecPeers model to maintain backward compatibility.
|
||||||
|
|
||||||
|
When initialized, it will emit a deprecation warning and call the IPSecPeer model.
|
||||||
|
|
||||||
|
TODO: Remove this class in ANTA v2.0.0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **data: Any) -> None: # noqa: ANN401
|
||||||
|
"""Initialize the IPSecPeer class, emitting a deprecation warning."""
|
||||||
|
warn(
|
||||||
|
message="IPSecPeers model is deprecated and will be removed in ANTA v2.0.0. Use the IPSecPeer model instead.",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
super().__init__(**data)
|
31
anta/input_models/services.py
Normal file
31
anta/input_models/services.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for services tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class DnsServer(BaseModel):
|
||||||
|
"""Model for a DNS server configuration."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
server_address: IPv4Address | IPv6Address
|
||||||
|
"""The IPv4 or IPv6 address of the DNS server."""
|
||||||
|
vrf: str = "default"
|
||||||
|
"""The VRF instance in which the DNS server resides. Defaults to 'default'."""
|
||||||
|
priority: int = Field(ge=0, le=4)
|
||||||
|
"""The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the DnsServer for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Server 10.0.0.1 (VRF: default, Priority: 1)
|
||||||
|
"""
|
||||||
|
return f"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})"
|
35
anta/input_models/stun.py
Normal file
35
anta/input_models/stun.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for services tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from anta.custom_types import Port
|
||||||
|
|
||||||
|
|
||||||
|
class StunClientTranslation(BaseModel):
|
||||||
|
"""STUN (Session Traversal Utilities for NAT) model represents the configuration of an IPv4-based client translations."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
source_address: IPv4Address
|
||||||
|
"""The IPv4 address of the STUN client"""
|
||||||
|
source_port: Port = 4500
|
||||||
|
"""The port number used by the STUN client for communication. Defaults to 4500."""
|
||||||
|
public_address: IPv4Address | None = None
|
||||||
|
"""The public-facing IPv4 address of the STUN client, discovered via the STUN server."""
|
||||||
|
public_port: Port | None = None
|
||||||
|
"""The public-facing port number of the STUN client, discovered via the STUN server."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return a human-readable string representation of the StunClientTranslation for reporting.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
Client 10.0.0.1 Port: 4500
|
||||||
|
"""
|
||||||
|
return f"Client {self.source_address} Port: {self.source_port}"
|
31
anta/input_models/system.py
Normal file
31
anta/input_models/system.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module containing input models for system tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from anta.custom_types import Hostname
|
||||||
|
|
||||||
|
|
||||||
|
class NTPServer(BaseModel):
|
||||||
|
"""Model for a NTP server."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
server_address: Hostname | IPv4Address
|
||||||
|
"""The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration
|
||||||
|
of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.
|
||||||
|
For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output."""
|
||||||
|
preferred: bool = False
|
||||||
|
"""Optional preferred for NTP server. If not provided, it defaults to `False`."""
|
||||||
|
stratum: int = Field(ge=0, le=16)
|
||||||
|
"""NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.
|
||||||
|
Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Representation of the NTPServer model."""
|
||||||
|
return f"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})"
|
|
@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
|
||||||
from pydantic import BaseModel, ConfigDict, ValidationError, create_model
|
from pydantic import BaseModel, ConfigDict, ValidationError, create_model
|
||||||
|
|
||||||
from anta import GITHUB_SUGGESTION
|
from anta import GITHUB_SUGGESTION
|
||||||
|
from anta.constants import KNOWN_EOS_ERRORS
|
||||||
from anta.custom_types import REGEXP_EOS_BLACKLIST_CMDS, Revision
|
from anta.custom_types import REGEXP_EOS_BLACKLIST_CMDS, Revision
|
||||||
from anta.logger import anta_log_exception, exc_to_str
|
from anta.logger import anta_log_exception, exc_to_str
|
||||||
from anta.result_manager.models import AntaTestStatus, TestResult
|
from anta.result_manager.models import AntaTestStatus, TestResult
|
||||||
|
@ -240,7 +241,12 @@ class AntaCommand(BaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported(self) -> bool:
|
def supported(self) -> bool:
|
||||||
"""Return True if the command is supported on the device hardware platform, False otherwise.
|
"""Indicates if the command is supported on the device.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True if the command is supported on the device hardware platform, False otherwise.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
|
@ -250,8 +256,22 @@ class AntaCommand(BaseModel):
|
||||||
"""
|
"""
|
||||||
if not self.collected and not self.error:
|
if not self.collected and not self.error:
|
||||||
msg = f"Command '{self.command}' has not been collected and has not returned an error. Call AntaDevice.collect()."
|
msg = f"Command '{self.command}' has not been collected and has not returned an error. Call AntaDevice.collect()."
|
||||||
|
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
return not any("not supported on this hardware platform" in e for e in self.errors)
|
return all("not supported on this hardware platform" not in e for e in self.errors)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def returned_known_eos_error(self) -> bool:
|
||||||
|
"""Return True if the command returned a known_eos_error on the device, False otherwise.
|
||||||
|
|
||||||
|
RuntimeError
|
||||||
|
If the command has not been collected and has not returned an error.
|
||||||
|
AntaDevice.collect() must be called before this property.
|
||||||
|
"""
|
||||||
|
if not self.collected and not self.error:
|
||||||
|
msg = f"Command '{self.command}' has not been collected and has not returned an error. Call AntaDevice.collect()."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
return any(any(re.match(pattern, e) for e in self.errors) for pattern in KNOWN_EOS_ERRORS)
|
||||||
|
|
||||||
|
|
||||||
class AntaTemplateRenderError(RuntimeError):
|
class AntaTemplateRenderError(RuntimeError):
|
||||||
|
@ -284,8 +304,7 @@ class AntaTest(ABC):
|
||||||
The following is an example of an AntaTest subclass implementation:
|
The following is an example of an AntaTest subclass implementation:
|
||||||
```python
|
```python
|
||||||
class VerifyReachability(AntaTest):
|
class VerifyReachability(AntaTest):
|
||||||
name = "VerifyReachability"
|
'''Test the network reachability to one or many destination IP(s).'''
|
||||||
description = "Test the network reachability to one or many destination IP(s)."
|
|
||||||
categories = ["connectivity"]
|
categories = ["connectivity"]
|
||||||
commands = [AntaTemplate(template="ping vrf {vrf} {dst} source {src} repeat 2")]
|
commands = [AntaTemplate(template="ping vrf {vrf} {dst} source {src} repeat 2")]
|
||||||
|
|
||||||
|
@ -326,12 +345,19 @@ class AntaTest(ABC):
|
||||||
Python logger for this test instance.
|
Python logger for this test instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Mandatory class attributes
|
# Optional class attributes
|
||||||
# TODO: find a way to tell mypy these are mandatory for child classes - maybe Protocol
|
|
||||||
name: ClassVar[str]
|
name: ClassVar[str]
|
||||||
description: ClassVar[str]
|
description: ClassVar[str]
|
||||||
|
__removal_in_version: ClassVar[str]
|
||||||
|
"""Internal class variable set by the `deprecated_test_class` decorator."""
|
||||||
|
|
||||||
|
# Mandatory class attributes
|
||||||
|
# TODO: find a way to tell mypy these are mandatory for child classes
|
||||||
|
# follow this https://discuss.python.org/t/provide-a-canonical-way-to-declare-an-abstract-class-variable/69416
|
||||||
|
# for now only enforced at runtime with __init_subclass__
|
||||||
categories: ClassVar[list[str]]
|
categories: ClassVar[list[str]]
|
||||||
commands: ClassVar[list[AntaTemplate | AntaCommand]]
|
commands: ClassVar[list[AntaTemplate | AntaCommand]]
|
||||||
|
|
||||||
# Class attributes to handle the progress bar of ANTA CLI
|
# Class attributes to handle the progress bar of ANTA CLI
|
||||||
progress: Progress | None = None
|
progress: Progress | None = None
|
||||||
nrfu_task: TaskID | None = None
|
nrfu_task: TaskID | None = None
|
||||||
|
@ -505,12 +531,19 @@ class AntaTest(ABC):
|
||||||
self.instance_commands[index].output = data
|
self.instance_commands[index].output = data
|
||||||
|
|
||||||
def __init_subclass__(cls) -> None:
|
def __init_subclass__(cls) -> None:
|
||||||
"""Verify that the mandatory class attributes are defined."""
|
"""Verify that the mandatory class attributes are defined and set name and description if not set."""
|
||||||
mandatory_attributes = ["name", "description", "categories", "commands"]
|
mandatory_attributes = ["categories", "commands"]
|
||||||
for attr in mandatory_attributes:
|
if missing_attrs := [attr for attr in mandatory_attributes if not hasattr(cls, attr)]:
|
||||||
if not hasattr(cls, attr):
|
msg = f"Class {cls.__module__}.{cls.__name__} is missing required class attribute(s): {', '.join(missing_attrs)}"
|
||||||
msg = f"Class {cls.__module__}.{cls.__name__} is missing required class attribute {attr}"
|
raise AttributeError(msg)
|
||||||
raise NotImplementedError(msg)
|
|
||||||
|
cls.name = getattr(cls, "name", cls.__name__)
|
||||||
|
if not hasattr(cls, "description"):
|
||||||
|
if not cls.__doc__ or cls.__doc__.strip() == "":
|
||||||
|
# No doctsring or empty doctsring - raise
|
||||||
|
msg = f"Cannot set the description for class {cls.name}, either set it in the class definition or add a docstring to the class."
|
||||||
|
raise AttributeError(msg)
|
||||||
|
cls.description = cls.__doc__.split(sep="\n", maxsplit=1)[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self) -> str:
|
def module(self) -> str:
|
||||||
|
@ -617,14 +650,9 @@ class AntaTest(ABC):
|
||||||
AntaTest.update_progress()
|
AntaTest.update_progress()
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
if cmds := self.failed_commands:
|
if self.failed_commands:
|
||||||
unsupported_commands = [f"'{c.command}' is not supported on {self.device.hw_model}" for c in cmds if not c.supported]
|
self._handle_failed_commands()
|
||||||
if unsupported_commands:
|
|
||||||
msg = f"Test {self.name} has been skipped because it is not supported on {self.device.hw_model}: {GITHUB_SUGGESTION}"
|
|
||||||
self.logger.warning(msg)
|
|
||||||
self.result.is_skipped("\n".join(unsupported_commands))
|
|
||||||
else:
|
|
||||||
self.result.is_error(message="\n".join([f"{c.command} has failed: {', '.join(c.errors)}" for c in cmds]))
|
|
||||||
AntaTest.update_progress()
|
AntaTest.update_progress()
|
||||||
return self.result
|
return self.result
|
||||||
|
|
||||||
|
@ -644,6 +672,28 @@ class AntaTest(ABC):
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def _handle_failed_commands(self) -> None:
|
||||||
|
"""Handle failed commands inside a test.
|
||||||
|
|
||||||
|
There can be 3 types:
|
||||||
|
* unsupported on hardware commands which set the test status to 'skipped'
|
||||||
|
* known EOS error which set the test status to 'failure'
|
||||||
|
* unknown failure which set the test status to 'error'
|
||||||
|
"""
|
||||||
|
cmds = self.failed_commands
|
||||||
|
unsupported_commands = [f"'{c.command}' is not supported on {self.device.hw_model}" for c in cmds if not c.supported]
|
||||||
|
if unsupported_commands:
|
||||||
|
msg = f"Test {self.name} has been skipped because it is not supported on {self.device.hw_model}: {GITHUB_SUGGESTION}"
|
||||||
|
self.logger.warning(msg)
|
||||||
|
self.result.is_skipped("\n".join(unsupported_commands))
|
||||||
|
return
|
||||||
|
returned_known_eos_error = [f"'{c.command}' failed on {self.device.name}: {', '.join(c.errors)}" for c in cmds if c.returned_known_eos_error]
|
||||||
|
if returned_known_eos_error:
|
||||||
|
self.result.is_failure("\n".join(returned_known_eos_error))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.result.is_error(message="\n".join([f"{c.command} has failed: {', '.join(c.errors)}" for c in cmds]))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_progress(cls: type[AntaTest]) -> None:
|
def update_progress(cls: type[AntaTest]) -> None:
|
||||||
"""Update progress bar for all AntaTest objects if it exists."""
|
"""Update progress bar for all AntaTest objects if it exists."""
|
||||||
|
|
0
anta/py.typed
Normal file
0
anta/py.typed
Normal file
|
@ -58,8 +58,7 @@ class ReportCsv:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_to_list(cls, result: TestResult) -> list[str]:
|
def convert_to_list(cls, result: TestResult) -> list[str]:
|
||||||
"""
|
"""Convert a TestResult into a list of string for creating file content.
|
||||||
Convert a TestResult into a list of string for creating file content.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -108,7 +107,7 @@ class ReportCsv:
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with csv_filename.open(mode="w", encoding="utf-8") as csvfile:
|
with csv_filename.open(mode="w", encoding="utf-8", newline="") as csvfile:
|
||||||
csvwriter = csv.writer(
|
csvwriter = csv.writer(
|
||||||
csvfile,
|
csvfile,
|
||||||
delimiter=",",
|
delimiter=",",
|
||||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, ClassVar
|
from typing import TYPE_CHECKING, ClassVar, TextIO
|
||||||
|
|
||||||
from anta.constants import MD_REPORT_TOC
|
from anta.constants import MD_REPORT_TOC
|
||||||
from anta.logger import anta_log_exception
|
from anta.logger import anta_log_exception
|
||||||
|
@ -17,7 +17,6 @@ from anta.tools import convert_categories
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from io import TextIOWrapper
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from anta.result_manager import ResultManager
|
from anta.result_manager import ResultManager
|
||||||
|
@ -72,7 +71,7 @@ class MDReportBase(ABC):
|
||||||
to generate and write content to the provided markdown file.
|
to generate and write content to the provided markdown file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mdfile: TextIOWrapper, results: ResultManager) -> None:
|
def __init__(self, mdfile: TextIO, results: ResultManager) -> None:
|
||||||
"""Initialize the MDReportBase with an open markdown file object to write to and a ResultManager instance.
|
"""Initialize the MDReportBase with an open markdown file object to write to and a ResultManager instance.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|
|
@ -6,15 +6,20 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from anta.result_manager.models import AntaTestStatus, TestResult
|
from anta.result_manager.models import AntaTestStatus, TestResult
|
||||||
|
|
||||||
from .models import CategoryStats, DeviceStats, TestStats
|
from .models import CategoryStats, DeviceStats, TestStats
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
class ResultManager:
|
class ResultManager:
|
||||||
"""Helper to manage Test Results and generate reports.
|
"""Helper to manage Test Results and generate reports.
|
||||||
|
|
||||||
|
@ -68,6 +73,15 @@ class ResultManager:
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_result_entries: list[TestResult]
|
||||||
|
status: AntaTestStatus
|
||||||
|
error_status: bool
|
||||||
|
|
||||||
|
_device_stats: defaultdict[str, DeviceStats]
|
||||||
|
_category_stats: defaultdict[str, CategoryStats]
|
||||||
|
_test_stats: defaultdict[str, TestStats]
|
||||||
|
_stats_in_sync: bool
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Class constructor.
|
"""Class constructor.
|
||||||
|
|
||||||
|
@ -89,13 +103,16 @@ class ResultManager:
|
||||||
If the status of the added test is error, the status is untouched and the
|
If the status of the added test is error, the status is untouched and the
|
||||||
error_status is set to True.
|
error_status is set to True.
|
||||||
"""
|
"""
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Create or reset the attributes of the ResultManager instance."""
|
||||||
self._result_entries: list[TestResult] = []
|
self._result_entries: list[TestResult] = []
|
||||||
self.status: AntaTestStatus = AntaTestStatus.UNSET
|
self.status: AntaTestStatus = AntaTestStatus.UNSET
|
||||||
self.error_status = False
|
self.error_status = False
|
||||||
|
|
||||||
self.device_stats: defaultdict[str, DeviceStats] = defaultdict(DeviceStats)
|
# Initialize the statistics attributes
|
||||||
self.category_stats: defaultdict[str, CategoryStats] = defaultdict(CategoryStats)
|
self._reset_stats()
|
||||||
self.test_stats: defaultdict[str, TestStats] = defaultdict(TestStats)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Implement __len__ method to count number of results."""
|
"""Implement __len__ method to count number of results."""
|
||||||
|
@ -110,26 +127,43 @@ class ResultManager:
|
||||||
def results(self, value: list[TestResult]) -> None:
|
def results(self, value: list[TestResult]) -> None:
|
||||||
"""Set the list of TestResult."""
|
"""Set the list of TestResult."""
|
||||||
# When setting the results, we need to reset the state of the current instance
|
# When setting the results, we need to reset the state of the current instance
|
||||||
self._result_entries = []
|
self.reset()
|
||||||
self.status = AntaTestStatus.UNSET
|
|
||||||
self.error_status = False
|
|
||||||
|
|
||||||
# Also reset the stats attributes
|
|
||||||
self.device_stats = defaultdict(DeviceStats)
|
|
||||||
self.category_stats = defaultdict(CategoryStats)
|
|
||||||
self.test_stats = defaultdict(TestStats)
|
|
||||||
|
|
||||||
for result in value:
|
for result in value:
|
||||||
self.add(result)
|
self.add(result)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dump(self) -> list[dict[str, Any]]:
|
||||||
|
"""Get a list of dictionary of the results."""
|
||||||
|
return [result.model_dump() for result in self._result_entries]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self) -> str:
|
def json(self) -> str:
|
||||||
"""Get a JSON representation of the results."""
|
"""Get a JSON representation of the results."""
|
||||||
return json.dumps([result.model_dump() for result in self._result_entries], indent=4)
|
return json.dumps(self.dump, indent=4)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_stats(self) -> defaultdict[str, DeviceStats]:
|
||||||
|
"""Get the device statistics."""
|
||||||
|
self._ensure_stats_in_sync()
|
||||||
|
return self._device_stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category_stats(self) -> defaultdict[str, CategoryStats]:
|
||||||
|
"""Get the category statistics."""
|
||||||
|
self._ensure_stats_in_sync()
|
||||||
|
return self._category_stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def test_stats(self) -> defaultdict[str, TestStats]:
|
||||||
|
"""Get the test statistics."""
|
||||||
|
self._ensure_stats_in_sync()
|
||||||
|
return self._test_stats
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_category_stats(self) -> dict[str, CategoryStats]:
|
def sorted_category_stats(self) -> dict[str, CategoryStats]:
|
||||||
"""A property that returns the category_stats dictionary sorted by key name."""
|
"""A property that returns the category_stats dictionary sorted by key name."""
|
||||||
|
self._ensure_stats_in_sync()
|
||||||
return dict(sorted(self.category_stats.items()))
|
return dict(sorted(self.category_stats.items()))
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
|
@ -148,11 +182,18 @@ class ResultManager:
|
||||||
if test_status == "error":
|
if test_status == "error":
|
||||||
self.error_status = True
|
self.error_status = True
|
||||||
return
|
return
|
||||||
if self.status == "unset" or self.status == "skipped" and test_status in {"success", "failure"}:
|
if self.status == "unset" or (self.status == "skipped" and test_status in {"success", "failure"}):
|
||||||
self.status = test_status
|
self.status = test_status
|
||||||
elif self.status == "success" and test_status == "failure":
|
elif self.status == "success" and test_status == "failure":
|
||||||
self.status = AntaTestStatus.FAILURE
|
self.status = AntaTestStatus.FAILURE
|
||||||
|
|
||||||
|
def _reset_stats(self) -> None:
|
||||||
|
"""Create or reset the statistics attributes."""
|
||||||
|
self._device_stats = defaultdict(DeviceStats)
|
||||||
|
self._category_stats = defaultdict(CategoryStats)
|
||||||
|
self._test_stats = defaultdict(TestStats)
|
||||||
|
self._stats_in_sync = False
|
||||||
|
|
||||||
def _update_stats(self, result: TestResult) -> None:
|
def _update_stats(self, result: TestResult) -> None:
|
||||||
"""Update the statistics based on the test result.
|
"""Update the statistics based on the test result.
|
||||||
|
|
||||||
|
@ -164,7 +205,7 @@ class ResultManager:
|
||||||
count_attr = f"tests_{result.result}_count"
|
count_attr = f"tests_{result.result}_count"
|
||||||
|
|
||||||
# Update device stats
|
# Update device stats
|
||||||
device_stats: DeviceStats = self.device_stats[result.name]
|
device_stats: DeviceStats = self._device_stats[result.name]
|
||||||
setattr(device_stats, count_attr, getattr(device_stats, count_attr) + 1)
|
setattr(device_stats, count_attr, getattr(device_stats, count_attr) + 1)
|
||||||
if result.result in ("failure", "error"):
|
if result.result in ("failure", "error"):
|
||||||
device_stats.tests_failure.add(result.test)
|
device_stats.tests_failure.add(result.test)
|
||||||
|
@ -174,16 +215,34 @@ class ResultManager:
|
||||||
|
|
||||||
# Update category stats
|
# Update category stats
|
||||||
for category in result.categories:
|
for category in result.categories:
|
||||||
category_stats: CategoryStats = self.category_stats[category]
|
category_stats: CategoryStats = self._category_stats[category]
|
||||||
setattr(category_stats, count_attr, getattr(category_stats, count_attr) + 1)
|
setattr(category_stats, count_attr, getattr(category_stats, count_attr) + 1)
|
||||||
|
|
||||||
# Update test stats
|
# Update test stats
|
||||||
count_attr = f"devices_{result.result}_count"
|
count_attr = f"devices_{result.result}_count"
|
||||||
test_stats: TestStats = self.test_stats[result.test]
|
test_stats: TestStats = self._test_stats[result.test]
|
||||||
setattr(test_stats, count_attr, getattr(test_stats, count_attr) + 1)
|
setattr(test_stats, count_attr, getattr(test_stats, count_attr) + 1)
|
||||||
if result.result in ("failure", "error"):
|
if result.result in ("failure", "error"):
|
||||||
test_stats.devices_failure.add(result.name)
|
test_stats.devices_failure.add(result.name)
|
||||||
|
|
||||||
|
def _compute_stats(self) -> None:
|
||||||
|
"""Compute all statistics from the current results."""
|
||||||
|
logger.info("Computing statistics for all results.")
|
||||||
|
|
||||||
|
# Reset all stats
|
||||||
|
self._reset_stats()
|
||||||
|
|
||||||
|
# Recompute stats for all results
|
||||||
|
for result in self._result_entries:
|
||||||
|
self._update_stats(result)
|
||||||
|
|
||||||
|
self._stats_in_sync = True
|
||||||
|
|
||||||
|
def _ensure_stats_in_sync(self) -> None:
|
||||||
|
"""Ensure statistics are in sync with current results."""
|
||||||
|
if not self._stats_in_sync:
|
||||||
|
self._compute_stats()
|
||||||
|
|
||||||
def add(self, result: TestResult) -> None:
|
def add(self, result: TestResult) -> None:
|
||||||
"""Add a result to the ResultManager instance.
|
"""Add a result to the ResultManager instance.
|
||||||
|
|
||||||
|
@ -197,7 +256,7 @@ class ResultManager:
|
||||||
"""
|
"""
|
||||||
self._result_entries.append(result)
|
self._result_entries.append(result)
|
||||||
self._update_status(result.result)
|
self._update_status(result.result)
|
||||||
self._update_stats(result)
|
self._stats_in_sync = False
|
||||||
|
|
||||||
# Every time a new result is added, we need to clear the cached property
|
# Every time a new result is added, we need to clear the cached property
|
||||||
self.__dict__.pop("results_by_status", None)
|
self.__dict__.pop("results_by_status", None)
|
||||||
|
|
109
anta/runner.py
109
anta/runner.py
|
@ -8,7 +8,7 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import resource
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
@ -26,36 +26,39 @@ if TYPE_CHECKING:
|
||||||
from anta.result_manager import ResultManager
|
from anta.result_manager import ResultManager
|
||||||
from anta.result_manager.models import TestResult
|
from anta.result_manager.models import TestResult
|
||||||
|
|
||||||
|
if os.name == "posix":
|
||||||
|
import resource
|
||||||
|
|
||||||
|
DEFAULT_NOFILE = 16384
|
||||||
|
|
||||||
|
def adjust_rlimit_nofile() -> tuple[int, int]:
|
||||||
|
"""Adjust the maximum number of open file descriptors for the ANTA process.
|
||||||
|
|
||||||
|
The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.
|
||||||
|
|
||||||
|
If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple[int, int]
|
||||||
|
The new soft and hard limits for open file descriptors.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
nofile = int(os.environ.get("ANTA_NOFILE", DEFAULT_NOFILE))
|
||||||
|
except ValueError as exception:
|
||||||
|
logger.warning("The ANTA_NOFILE environment variable value is invalid: %s\nDefault to %s.", exc_to_str(exception), DEFAULT_NOFILE)
|
||||||
|
nofile = DEFAULT_NOFILE
|
||||||
|
|
||||||
|
limits = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
logger.debug("Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1])
|
||||||
|
nofile = min(limits[1], nofile)
|
||||||
|
logger.debug("Setting soft limit for open file descriptors for the current ANTA process to %s", nofile)
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))
|
||||||
|
return resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NOFILE = 16384
|
|
||||||
|
|
||||||
|
|
||||||
def adjust_rlimit_nofile() -> tuple[int, int]:
|
|
||||||
"""Adjust the maximum number of open file descriptors for the ANTA process.
|
|
||||||
|
|
||||||
The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.
|
|
||||||
|
|
||||||
If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
tuple[int, int]
|
|
||||||
The new soft and hard limits for open file descriptors.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
nofile = int(os.environ.get("ANTA_NOFILE", DEFAULT_NOFILE))
|
|
||||||
except ValueError as exception:
|
|
||||||
logger.warning("The ANTA_NOFILE environment variable value is invalid: %s\nDefault to %s.", exc_to_str(exception), DEFAULT_NOFILE)
|
|
||||||
nofile = DEFAULT_NOFILE
|
|
||||||
|
|
||||||
limits = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
||||||
logger.debug("Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1])
|
|
||||||
nofile = min(limits[1], nofile)
|
|
||||||
logger.debug("Setting soft limit for open file descriptors for the current ANTA process to %s", nofile)
|
|
||||||
resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))
|
|
||||||
return resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
||||||
|
|
||||||
|
|
||||||
def log_cache_statistics(devices: list[AntaDevice]) -> None:
|
def log_cache_statistics(devices: list[AntaDevice]) -> None:
|
||||||
"""Log cache statistics for each device in the inventory.
|
"""Log cache statistics for each device in the inventory.
|
||||||
|
@ -146,22 +149,29 @@ def prepare_tests(
|
||||||
# Using a set to avoid inserting duplicate tests
|
# Using a set to avoid inserting duplicate tests
|
||||||
device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
|
device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
|
||||||
|
|
||||||
|
total_test_count = 0
|
||||||
|
|
||||||
# Create the device to tests mapping from the tags
|
# Create the device to tests mapping from the tags
|
||||||
for device in inventory.devices:
|
for device in inventory.devices:
|
||||||
if tags:
|
if tags:
|
||||||
if not any(tag in device.tags for tag in tags):
|
# If there are CLI tags, execute tests with matching tags for this device
|
||||||
|
if not (matching_tags := tags.intersection(device.tags)):
|
||||||
# The device does not have any selected tag, skipping
|
# The device does not have any selected tag, skipping
|
||||||
continue
|
continue
|
||||||
|
device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))
|
||||||
else:
|
else:
|
||||||
# If there is no CLI tags, execute all tests that do not have any tags
|
# If there is no CLI tags, execute all tests that do not have any tags
|
||||||
device_to_tests[device].update(catalog.tag_to_tests[None])
|
device_to_tests[device].update(catalog.tag_to_tests[None])
|
||||||
|
|
||||||
# Add the tests with matching tags from device tags
|
# Then add the tests with matching tags from device tags
|
||||||
device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))
|
device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))
|
||||||
|
|
||||||
if len(device_to_tests.values()) == 0:
|
total_test_count += len(device_to_tests[device])
|
||||||
|
|
||||||
|
if total_test_count == 0:
|
||||||
msg = (
|
msg = (
|
||||||
f"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs."
|
f"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current "
|
||||||
|
"test catalog and device inventory, please verify your inputs."
|
||||||
)
|
)
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
return None
|
return None
|
||||||
|
@ -169,7 +179,7 @@ def prepare_tests(
|
||||||
return device_to_tests
|
return device_to_tests
|
||||||
|
|
||||||
|
|
||||||
def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:
|
def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager | None = None) -> list[Coroutine[Any, Any, TestResult]]:
|
||||||
"""Get the coroutines for the ANTA run.
|
"""Get the coroutines for the ANTA run.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -177,7 +187,7 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
|
||||||
selected_tests
|
selected_tests
|
||||||
A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.
|
A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.
|
||||||
manager
|
manager
|
||||||
A ResultManager
|
An optional ResultManager object to pre-populate with the test results. Used in dry-run mode.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
@ -189,7 +199,8 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
|
||||||
for test in test_definitions:
|
for test in test_definitions:
|
||||||
try:
|
try:
|
||||||
test_instance = test.test(device=device, inputs=test.inputs)
|
test_instance = test.test(device=device, inputs=test.inputs)
|
||||||
manager.add(test_instance.result)
|
if manager is not None:
|
||||||
|
manager.add(test_instance.result)
|
||||||
coros.append(test_instance.test())
|
coros.append(test_instance.test())
|
||||||
except Exception as e: # noqa: PERF203, BLE001
|
except Exception as e: # noqa: PERF203, BLE001
|
||||||
# An AntaTest instance is potentially user-defined code.
|
# An AntaTest instance is potentially user-defined code.
|
||||||
|
@ -205,7 +216,7 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
|
||||||
|
|
||||||
|
|
||||||
@cprofile()
|
@cprofile()
|
||||||
async def main( # noqa: PLR0913
|
async def main(
|
||||||
manager: ResultManager,
|
manager: ResultManager,
|
||||||
inventory: AntaInventory,
|
inventory: AntaInventory,
|
||||||
catalog: AntaCatalog,
|
catalog: AntaCatalog,
|
||||||
|
@ -240,9 +251,6 @@ async def main( # noqa: PLR0913
|
||||||
dry_run
|
dry_run
|
||||||
Build the list of coroutine to run and stop before test execution.
|
Build the list of coroutine to run and stop before test execution.
|
||||||
"""
|
"""
|
||||||
# Adjust the maximum number of open file descriptors for the ANTA process
|
|
||||||
limits = adjust_rlimit_nofile()
|
|
||||||
|
|
||||||
if not catalog.tests:
|
if not catalog.tests:
|
||||||
logger.info("The list of tests is empty, exiting")
|
logger.info("The list of tests is empty, exiting")
|
||||||
return
|
return
|
||||||
|
@ -263,10 +271,19 @@ async def main( # noqa: PLR0913
|
||||||
"--- ANTA NRFU Run Information ---\n"
|
"--- ANTA NRFU Run Information ---\n"
|
||||||
f"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\n"
|
f"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\n"
|
||||||
f"Total number of selected tests: {final_tests_count}\n"
|
f"Total number of selected tests: {final_tests_count}\n"
|
||||||
f"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\n"
|
|
||||||
"---------------------------------"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if os.name == "posix":
|
||||||
|
# Adjust the maximum number of open file descriptors for the ANTA process
|
||||||
|
limits = adjust_rlimit_nofile()
|
||||||
|
run_info += f"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\n"
|
||||||
|
else:
|
||||||
|
# Running on non-Posix system, cannot manage the resource.
|
||||||
|
limits = (sys.maxsize, sys.maxsize)
|
||||||
|
run_info += "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.\n"
|
||||||
|
|
||||||
|
run_info += "---------------------------------"
|
||||||
|
|
||||||
logger.info(run_info)
|
logger.info(run_info)
|
||||||
|
|
||||||
if final_tests_count > limits[0]:
|
if final_tests_count > limits[0]:
|
||||||
|
@ -276,7 +293,7 @@ async def main( # noqa: PLR0913
|
||||||
"Please consult the ANTA FAQ."
|
"Please consult the ANTA FAQ."
|
||||||
)
|
)
|
||||||
|
|
||||||
coroutines = get_coroutines(selected_tests, manager)
|
coroutines = get_coroutines(selected_tests, manager if dry_run else None)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
logger.info("Dry-run mode, exiting before running the tests.")
|
logger.info("Dry-run mode, exiting before running the tests.")
|
||||||
|
@ -288,6 +305,8 @@ async def main( # noqa: PLR0913
|
||||||
AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=len(coroutines))
|
AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=len(coroutines))
|
||||||
|
|
||||||
with Catchtime(logger=logger, message="Running ANTA tests"):
|
with Catchtime(logger=logger, message="Running ANTA tests"):
|
||||||
await asyncio.gather(*coroutines)
|
results = await asyncio.gather(*coroutines)
|
||||||
|
for result in results:
|
||||||
|
manager.add(result)
|
||||||
|
|
||||||
log_cache_statistics(selected_inventory.devices)
|
log_cache_statistics(selected_inventory.devices)
|
||||||
|
|
|
@ -35,8 +35,6 @@ class VerifyTacacsSourceIntf(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTacacsSourceIntf"
|
|
||||||
description = "Verifies TACACS source-interface for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
||||||
|
|
||||||
|
@ -81,8 +79,6 @@ class VerifyTacacsServers(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTacacsServers"
|
|
||||||
description = "Verifies TACACS servers are configured for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
||||||
|
|
||||||
|
@ -134,8 +130,6 @@ class VerifyTacacsServerGroups(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTacacsServerGroups"
|
|
||||||
description = "Verifies if the provided TACACS server group(s) are configured."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
|
||||||
|
|
||||||
|
@ -173,19 +167,17 @@ class VerifyAuthenMethods(AntaTest):
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.aaa:
|
anta.tests.aaa:
|
||||||
- VerifyAuthenMethods:
|
- VerifyAuthenMethods:
|
||||||
methods:
|
methods:
|
||||||
- local
|
- local
|
||||||
- none
|
- none
|
||||||
- logging
|
- logging
|
||||||
types:
|
types:
|
||||||
- login
|
- login
|
||||||
- enable
|
- enable
|
||||||
- dot1x
|
- dot1x
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAuthenMethods"
|
|
||||||
description = "Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x)."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods authentication", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods authentication", revision=1)]
|
||||||
|
|
||||||
|
@ -245,8 +237,6 @@ class VerifyAuthzMethods(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAuthzMethods"
|
|
||||||
description = "Verifies the AAA authorization method lists for different authorization types (commands, exec)."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods authorization", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods authorization", revision=1)]
|
||||||
|
|
||||||
|
@ -301,8 +291,6 @@ class VerifyAcctDefaultMethods(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAcctDefaultMethods"
|
|
||||||
description = "Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x)."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods accounting", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods accounting", revision=1)]
|
||||||
|
|
||||||
|
@ -364,8 +352,6 @@ class VerifyAcctConsoleMethods(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAcctConsoleMethods"
|
|
||||||
description = "Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x)."
|
|
||||||
categories: ClassVar[list[str]] = ["aaa"]
|
categories: ClassVar[list[str]] = ["aaa"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods accounting", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods accounting", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,16 @@
|
||||||
# mypy: disable-error-code=attr-defined
|
# mypy: disable-error-code=attr-defined
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from anta.decorators import skip_on_platforms
|
from anta.decorators import skip_on_platforms
|
||||||
|
from anta.input_models.avt import AVTPath
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
from anta.tools import get_value
|
from anta.tools import get_value
|
||||||
|
|
||||||
|
|
||||||
class VerifyAVTPathHealth(AntaTest):
|
class VerifyAVTPathHealth(AntaTest):
|
||||||
"""
|
"""Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.
|
||||||
Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -34,7 +31,6 @@ class VerifyAVTPathHealth(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAVTPathHealth"
|
|
||||||
description = "Verifies the status of all AVT paths for all VRFs."
|
description = "Verifies the status of all AVT paths for all VRFs."
|
||||||
categories: ClassVar[list[str]] = ["avt"]
|
categories: ClassVar[list[str]] = ["avt"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]
|
||||||
|
@ -73,15 +69,22 @@ class VerifyAVTPathHealth(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyAVTSpecificPath(AntaTest):
|
class VerifyAVTSpecificPath(AntaTest):
|
||||||
"""
|
"""Verifies the Adaptive Virtual Topology (AVT) path.
|
||||||
Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.
|
|
||||||
|
This test performs the following checks for each specified LLDP neighbor:
|
||||||
|
|
||||||
|
1. Confirming that the AVT paths are associated with the specified VRF.
|
||||||
|
2. Verifying that each AVT path is active and valid.
|
||||||
|
3. Ensuring that the AVT path matches the specified type (direct/multihop) if provided.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.
|
* Success: The test will pass if all of the following conditions are met:
|
||||||
If multiple paths are configured, the test will pass only if all the paths are valid and active.
|
- All AVT paths for the specified VRF are active, valid, and match the specified path type (direct/multihop), if provided.
|
||||||
* Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,
|
- If multiple paths are configured, the test will pass only if all paths meet these criteria.
|
||||||
or does not match the specified type.
|
* Failure: The test will fail if any of the following conditions are met:
|
||||||
|
- No AVT paths are configured for the specified VRF.
|
||||||
|
- Any configured path is inactive, invalid, or does not match the specified type.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -97,36 +100,16 @@ class VerifyAVTSpecificPath(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAVTSpecificPath"
|
|
||||||
description = "Verifies the status and type of an AVT path for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["avt"]
|
categories: ClassVar[list[str]] = ["avt"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path", revision=1)]
|
||||||
AntaTemplate(template="show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}")
|
|
||||||
]
|
|
||||||
|
|
||||||
class Input(AntaTest.Input):
|
class Input(AntaTest.Input):
|
||||||
"""Input model for the VerifyAVTSpecificPath test."""
|
"""Input model for the VerifyAVTSpecificPath test."""
|
||||||
|
|
||||||
avt_paths: list[AVTPaths]
|
avt_paths: list[AVTPath]
|
||||||
"""List of AVT paths to verify."""
|
"""List of AVT paths to verify."""
|
||||||
|
AVTPaths: ClassVar[type[AVTPath]] = AVTPath
|
||||||
class AVTPaths(BaseModel):
|
"""To maintain backward compatibility."""
|
||||||
"""Model for the details of AVT paths."""
|
|
||||||
|
|
||||||
vrf: str = "default"
|
|
||||||
"""The VRF for the AVT path. Defaults to 'default' if not provided."""
|
|
||||||
avt_name: str
|
|
||||||
"""Name of the adaptive virtual topology."""
|
|
||||||
destination: IPv4Address
|
|
||||||
"""The IPv4 address of the AVT peer."""
|
|
||||||
next_hop: IPv4Address
|
|
||||||
"""The IPv4 address of the next hop for the AVT peer."""
|
|
||||||
path_type: str | None = None
|
|
||||||
"""The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered."""
|
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
|
||||||
"""Render the template for each input AVT path/peer."""
|
|
||||||
return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]
|
|
||||||
|
|
||||||
@skip_on_platforms(["cEOSLab", "vEOS-lab"])
|
@skip_on_platforms(["cEOSLab", "vEOS-lab"])
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
|
@ -135,64 +118,43 @@ class VerifyAVTSpecificPath(AntaTest):
|
||||||
# Assume the test is successful until a failure is detected
|
# Assume the test is successful until a failure is detected
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
|
|
||||||
# Process each command in the instance
|
command_output = self.instance_commands[0].json_output
|
||||||
for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):
|
for avt_path in self.inputs.avt_paths:
|
||||||
# Extract the command output and parameters
|
if (path_output := get_value(command_output, f"vrfs.{avt_path.vrf}.avts.{avt_path.avt_name}.avtPaths")) is None:
|
||||||
vrf = command.params.vrf
|
self.result.is_failure(f"{avt_path} - No AVT path configured")
|
||||||
avt_name = command.params.avt_name
|
return
|
||||||
peer = str(command.params.destination)
|
|
||||||
|
|
||||||
command_output = command.json_output.get("vrfs", {})
|
path_found = path_type_found = False
|
||||||
|
|
||||||
# If no AVT is configured, mark the test as failed and skip to the next command
|
|
||||||
if not command_output:
|
|
||||||
self.result.is_failure(f"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Extract the AVT paths
|
|
||||||
avt_paths = get_value(command_output, f"{vrf}.avts.{avt_name}.avtPaths")
|
|
||||||
next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type
|
|
||||||
|
|
||||||
nexthop_path_found = path_type_found = False
|
|
||||||
|
|
||||||
# Check each AVT path
|
# Check each AVT path
|
||||||
for path, path_data in avt_paths.items():
|
for path, path_data in path_output.items():
|
||||||
# If the path does not match the expected next hop, skip to the next path
|
dest = path_data.get("destination")
|
||||||
if path_data.get("nexthopAddr") != next_hop:
|
nexthop = path_data.get("nexthopAddr")
|
||||||
continue
|
|
||||||
|
|
||||||
nexthop_path_found = True
|
|
||||||
path_type = "direct" if get_value(path_data, "flags.directPath") else "multihop"
|
path_type = "direct" if get_value(path_data, "flags.directPath") else "multihop"
|
||||||
|
|
||||||
# If the path type does not match the expected path type, skip to the next path
|
if not avt_path.path_type:
|
||||||
if input_path_type and path_type != input_path_type:
|
path_found = all([dest == str(avt_path.destination), nexthop == str(avt_path.next_hop)])
|
||||||
continue
|
|
||||||
|
|
||||||
path_type_found = True
|
else:
|
||||||
valid = get_value(path_data, "flags.valid")
|
path_type_found = all([dest == str(avt_path.destination), nexthop == str(avt_path.next_hop), path_type == avt_path.path_type])
|
||||||
active = get_value(path_data, "flags.active")
|
if path_type_found:
|
||||||
|
path_found = True
|
||||||
|
# Check the path status and type against the expected values
|
||||||
|
valid = get_value(path_data, "flags.valid")
|
||||||
|
active = get_value(path_data, "flags.active")
|
||||||
|
if not all([valid, active]):
|
||||||
|
self.result.is_failure(f"{avt_path} - Incorrect path {path} - Valid: {valid}, Active: {active}")
|
||||||
|
|
||||||
# Check the path status and type against the expected values
|
# If no matching path found, mark the test as failed
|
||||||
if not all([valid, active]):
|
if not path_found:
|
||||||
failure_reasons = []
|
if avt_path.path_type and not path_type_found:
|
||||||
if not get_value(path_data, "flags.active"):
|
self.result.is_failure(f"{avt_path} Path Type: {avt_path.path_type} - Path not found")
|
||||||
failure_reasons.append("inactive")
|
else:
|
||||||
if not get_value(path_data, "flags.valid"):
|
self.result.is_failure(f"{avt_path} - Path not found")
|
||||||
failure_reasons.append("invalid")
|
|
||||||
# Construct the failure message prefix
|
|
||||||
failed_log = f"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'"
|
|
||||||
self.result.is_failure(f"{failed_log} is {', '.join(failure_reasons)}.")
|
|
||||||
|
|
||||||
# If no matching next hop or path type was found, mark the test as failed
|
|
||||||
if not nexthop_path_found or not path_type_found:
|
|
||||||
self.result.is_failure(
|
|
||||||
f"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyAVTRole(AntaTest):
|
class VerifyAVTRole(AntaTest):
|
||||||
"""
|
"""Verifies the Adaptive Virtual Topology (AVT) role of a device.
|
||||||
Verifies the Adaptive Virtual Topology (AVT) role of a device.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -208,7 +170,6 @@ class VerifyAVTRole(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAVTRole"
|
|
||||||
description = "Verifies the AVT role of a device."
|
description = "Verifies the AVT role of a device."
|
||||||
categories: ClassVar[list[str]] = ["avt"]
|
categories: ClassVar[list[str]] = ["avt"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]
|
||||||
|
|
|
@ -8,12 +8,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from ipaddress import IPv4Address
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
from typing import TYPE_CHECKING, Any, ClassVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import Field
|
||||||
|
|
||||||
from anta.custom_types import BfdInterval, BfdMultiplier, BfdProtocol
|
from anta.input_models.bfd import BFDPeer
|
||||||
from anta.models import AntaCommand, AntaTest
|
from anta.models import AntaCommand, AntaTest
|
||||||
from anta.tools import get_value
|
from anta.tools import get_value
|
||||||
|
|
||||||
|
@ -22,12 +21,24 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class VerifyBFDSpecificPeers(AntaTest):
|
class VerifyBFDSpecificPeers(AntaTest):
|
||||||
"""Verifies if the IPv4 BFD peer's sessions are UP and remote disc is non-zero in the specified VRF.
|
"""Verifies the state of IPv4 BFD peer sessions.
|
||||||
|
|
||||||
|
This test performs the following checks for each specified peer:
|
||||||
|
|
||||||
|
1. Confirms that the specified VRF is configured.
|
||||||
|
2. Verifies that the peer exists in the BFD configuration.
|
||||||
|
3. For each specified BFD peer:
|
||||||
|
- Validates that the state is `up`
|
||||||
|
- Confirms that the remote discriminator identifier (disc) is non-zero.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.
|
* Success: If all of the following conditions are met:
|
||||||
* Failure: The test will fail if IPv4 BFD peers are not found, the status is not UP or remote disc is zero in the specified VRF.
|
- All specified peers are found in the BFD configuration within the specified VRF.
|
||||||
|
- All BFD peers are `up` and remote disc is non-zero.
|
||||||
|
* Failure: If any of the following occur:
|
||||||
|
- A specified peer is not found in the BFD configuration within the specified VRF.
|
||||||
|
- Any BFD peer session is not `up` or the remote discriminator identifier is zero.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -42,8 +53,6 @@ class VerifyBFDSpecificPeers(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBFDSpecificPeers"
|
|
||||||
description = "Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["bfd"]
|
categories: ClassVar[list[str]] = ["bfd"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers", revision=1)]
|
||||||
|
|
||||||
|
@ -51,20 +60,14 @@ class VerifyBFDSpecificPeers(AntaTest):
|
||||||
"""Input model for the VerifyBFDSpecificPeers test."""
|
"""Input model for the VerifyBFDSpecificPeers test."""
|
||||||
|
|
||||||
bfd_peers: list[BFDPeer]
|
bfd_peers: list[BFDPeer]
|
||||||
"""List of IPv4 BFD peers."""
|
"""List of IPv4 BFD"""
|
||||||
|
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
|
||||||
class BFDPeer(BaseModel):
|
"""To maintain backward compatibility."""
|
||||||
"""Model for an IPv4 BFD peer."""
|
|
||||||
|
|
||||||
peer_address: IPv4Address
|
|
||||||
"""IPv4 address of a BFD peer."""
|
|
||||||
vrf: str = "default"
|
|
||||||
"""Optional VRF for BFD peer. If not provided, it defaults to `default`."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyBFDSpecificPeers."""
|
"""Main test function for VerifyBFDSpecificPeers."""
|
||||||
failures: dict[Any, Any] = {}
|
self.result.is_success()
|
||||||
|
|
||||||
# Iterating over BFD peers
|
# Iterating over BFD peers
|
||||||
for bfd_peer in self.inputs.bfd_peers:
|
for bfd_peer in self.inputs.bfd_peers:
|
||||||
|
@ -78,31 +81,33 @@ class VerifyBFDSpecificPeers(AntaTest):
|
||||||
|
|
||||||
# Check if BFD peer configured
|
# Check if BFD peer configured
|
||||||
if not bfd_output:
|
if not bfd_output:
|
||||||
failures[peer] = {vrf: "Not Configured"}
|
self.result.is_failure(f"{bfd_peer} - Not found")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check BFD peer status and remote disc
|
# Check BFD peer status and remote disc
|
||||||
if not (bfd_output.get("status") == "up" and bfd_output.get("remoteDisc") != 0):
|
state = bfd_output.get("status")
|
||||||
failures[peer] = {
|
remote_disc = bfd_output.get("remoteDisc")
|
||||||
vrf: {
|
if not (state == "up" and remote_disc != 0):
|
||||||
"status": bfd_output.get("status"),
|
self.result.is_failure(f"{bfd_peer} - Session not properly established - State: {state} Remote Discriminator: {remote_disc}")
|
||||||
"remote_disc": bfd_output.get("remoteDisc"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if not failures:
|
|
||||||
self.result.is_success()
|
|
||||||
else:
|
|
||||||
self.result.is_failure(f"Following BFD peers are not configured, status is not up or remote disc is zero:\n{failures}")
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyBFDPeersIntervals(AntaTest):
|
class VerifyBFDPeersIntervals(AntaTest):
|
||||||
"""Verifies the timers of the IPv4 BFD peers in the specified VRF.
|
"""Verifies the timers of IPv4 BFD peer sessions.
|
||||||
|
|
||||||
|
This test performs the following checks for each specified peer:
|
||||||
|
|
||||||
|
1. Confirms that the specified VRF is configured.
|
||||||
|
2. Verifies that the peer exists in the BFD configuration.
|
||||||
|
3. Confirms that BFD peer is correctly configured with the `Transmit interval, Receive interval and Multiplier`.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.
|
* Success: If all of the following conditions are met:
|
||||||
* Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.
|
- All specified peers are found in the BFD configuration within the specified VRF.
|
||||||
|
- All BFD peers are correctly configured with the `Transmit interval, Receive interval and Multiplier`.
|
||||||
|
* Failure: If any of the following occur:
|
||||||
|
- A specified peer is not found in the BFD configuration within the specified VRF.
|
||||||
|
- Any BFD peer not correctly configured with the `Transmit interval, Receive interval and Multiplier`.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -123,8 +128,6 @@ class VerifyBFDPeersIntervals(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBFDPeersIntervals"
|
|
||||||
description = "Verifies the timers of the IPv4 BFD peers in the specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["bfd"]
|
categories: ClassVar[list[str]] = ["bfd"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers detail", revision=1)]
|
||||||
|
|
||||||
|
@ -132,34 +135,22 @@ class VerifyBFDPeersIntervals(AntaTest):
|
||||||
"""Input model for the VerifyBFDPeersIntervals test."""
|
"""Input model for the VerifyBFDPeersIntervals test."""
|
||||||
|
|
||||||
bfd_peers: list[BFDPeer]
|
bfd_peers: list[BFDPeer]
|
||||||
"""List of BFD peers."""
|
"""List of IPv4 BFD"""
|
||||||
|
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
|
||||||
class BFDPeer(BaseModel):
|
"""To maintain backward compatibility"""
|
||||||
"""Model for an IPv4 BFD peer."""
|
|
||||||
|
|
||||||
peer_address: IPv4Address
|
|
||||||
"""IPv4 address of a BFD peer."""
|
|
||||||
vrf: str = "default"
|
|
||||||
"""Optional VRF for BFD peer. If not provided, it defaults to `default`."""
|
|
||||||
tx_interval: BfdInterval
|
|
||||||
"""Tx interval of BFD peer in milliseconds."""
|
|
||||||
rx_interval: BfdInterval
|
|
||||||
"""Rx interval of BFD peer in milliseconds."""
|
|
||||||
multiplier: BfdMultiplier
|
|
||||||
"""Multiplier of BFD peer."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyBFDPeersIntervals."""
|
"""Main test function for VerifyBFDPeersIntervals."""
|
||||||
failures: dict[Any, Any] = {}
|
self.result.is_success()
|
||||||
|
|
||||||
# Iterating over BFD peers
|
# Iterating over BFD peers
|
||||||
for bfd_peers in self.inputs.bfd_peers:
|
for bfd_peer in self.inputs.bfd_peers:
|
||||||
peer = str(bfd_peers.peer_address)
|
peer = str(bfd_peer.peer_address)
|
||||||
vrf = bfd_peers.vrf
|
vrf = bfd_peer.vrf
|
||||||
tx_interval = bfd_peers.tx_interval
|
tx_interval = bfd_peer.tx_interval
|
||||||
rx_interval = bfd_peers.rx_interval
|
rx_interval = bfd_peer.rx_interval
|
||||||
multiplier = bfd_peers.multiplier
|
multiplier = bfd_peer.multiplier
|
||||||
|
|
||||||
# Check if BFD peer configured
|
# Check if BFD peer configured
|
||||||
bfd_output = get_value(
|
bfd_output = get_value(
|
||||||
|
@ -168,7 +159,7 @@ class VerifyBFDPeersIntervals(AntaTest):
|
||||||
separator="..",
|
separator="..",
|
||||||
)
|
)
|
||||||
if not bfd_output:
|
if not bfd_output:
|
||||||
failures[peer] = {vrf: "Not Configured"}
|
self.result.is_failure(f"{bfd_peer} - Not found")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Convert interval timer(s) into milliseconds to be consistent with the inputs.
|
# Convert interval timer(s) into milliseconds to be consistent with the inputs.
|
||||||
|
@ -176,38 +167,34 @@ class VerifyBFDPeersIntervals(AntaTest):
|
||||||
op_tx_interval = bfd_details.get("operTxInterval") // 1000
|
op_tx_interval = bfd_details.get("operTxInterval") // 1000
|
||||||
op_rx_interval = bfd_details.get("operRxInterval") // 1000
|
op_rx_interval = bfd_details.get("operRxInterval") // 1000
|
||||||
detect_multiplier = bfd_details.get("detectMult")
|
detect_multiplier = bfd_details.get("detectMult")
|
||||||
intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier
|
|
||||||
|
|
||||||
# Check timers of BFD peer
|
if op_tx_interval != tx_interval:
|
||||||
if not intervals_ok:
|
self.result.is_failure(f"{bfd_peer} - Incorrect Transmit interval - Expected: {tx_interval} Actual: {op_tx_interval}")
|
||||||
failures[peer] = {
|
|
||||||
vrf: {
|
|
||||||
"tx_interval": op_tx_interval,
|
|
||||||
"rx_interval": op_rx_interval,
|
|
||||||
"multiplier": detect_multiplier,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if any failures
|
if op_rx_interval != rx_interval:
|
||||||
if not failures:
|
self.result.is_failure(f"{bfd_peer} - Incorrect Receive interval - Expected: {rx_interval} Actual: {op_rx_interval}")
|
||||||
self.result.is_success()
|
|
||||||
else:
|
if detect_multiplier != multiplier:
|
||||||
self.result.is_failure(f"Following BFD peers are not configured or timers are not correct:\n{failures}")
|
self.result.is_failure(f"{bfd_peer} - Incorrect Multiplier - Expected: {multiplier} Actual: {detect_multiplier}")
|
||||||
|
|
||||||
|
|
||||||
class VerifyBFDPeersHealth(AntaTest):
|
class VerifyBFDPeersHealth(AntaTest):
|
||||||
"""Verifies the health of IPv4 BFD peers across all VRFs.
|
"""Verifies the health of IPv4 BFD peers across all VRFs.
|
||||||
|
|
||||||
It checks that no BFD peer is in the down state and that the discriminator value of the remote system is not zero.
|
This test performs the following checks for BFD peers across all VRFs:
|
||||||
|
|
||||||
Optionally, it can also verify that BFD peers have not been down before a specified threshold of hours.
|
1. Validates that the state is `up`.
|
||||||
|
2. Confirms that the remote discriminator identifier (disc) is non-zero.
|
||||||
|
3. Optionally verifies that the peer have not been down before a specified threshold of hours.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero,
|
* Success: If all of the following conditions are met:
|
||||||
and the last downtime of each peer is above the defined threshold.
|
- All BFD peers across the VRFs are up and remote disc is non-zero.
|
||||||
* Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero,
|
- Last downtime of each peer is above the defined threshold, if specified.
|
||||||
or the last downtime of any peer is below the defined threshold.
|
* Failure: If any of the following occur:
|
||||||
|
- Any BFD peer session is not up or the remote discriminator identifier is zero.
|
||||||
|
- Last downtime of any peer is below the defined threshold, if specified.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -218,8 +205,6 @@ class VerifyBFDPeersHealth(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBFDPeersHealth"
|
|
||||||
description = "Verifies the health of all IPv4 BFD peers."
|
|
||||||
categories: ClassVar[list[str]] = ["bfd"]
|
categories: ClassVar[list[str]] = ["bfd"]
|
||||||
# revision 1 as later revision introduces additional nesting for type
|
# revision 1 as later revision introduces additional nesting for type
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
|
@ -236,18 +221,13 @@ class VerifyBFDPeersHealth(AntaTest):
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyBFDPeersHealth."""
|
"""Main test function for VerifyBFDPeersHealth."""
|
||||||
# Initialize failure strings
|
self.result.is_success()
|
||||||
down_failures = []
|
|
||||||
up_failures = []
|
|
||||||
|
|
||||||
# Extract the current timestamp and command output
|
# Extract the current timestamp and command output
|
||||||
clock_output = self.instance_commands[1].json_output
|
clock_output = self.instance_commands[1].json_output
|
||||||
current_timestamp = clock_output["utcTime"]
|
current_timestamp = clock_output["utcTime"]
|
||||||
bfd_output = self.instance_commands[0].json_output
|
bfd_output = self.instance_commands[0].json_output
|
||||||
|
|
||||||
# set the initial result
|
|
||||||
self.result.is_success()
|
|
||||||
|
|
||||||
# Check if any IPv4 BFD peer is configured
|
# Check if any IPv4 BFD peer is configured
|
||||||
ipv4_neighbors_exist = any(vrf_data["ipv4Neighbors"] for vrf_data in bfd_output["vrfs"].values())
|
ipv4_neighbors_exist = any(vrf_data["ipv4Neighbors"] for vrf_data in bfd_output["vrfs"].values())
|
||||||
if not ipv4_neighbors_exist:
|
if not ipv4_neighbors_exist:
|
||||||
|
@ -260,40 +240,40 @@ class VerifyBFDPeersHealth(AntaTest):
|
||||||
for peer_data in neighbor_data["peerStats"].values():
|
for peer_data in neighbor_data["peerStats"].values():
|
||||||
peer_status = peer_data["status"]
|
peer_status = peer_data["status"]
|
||||||
remote_disc = peer_data["remoteDisc"]
|
remote_disc = peer_data["remoteDisc"]
|
||||||
remote_disc_info = f" with remote disc {remote_disc}" if remote_disc == 0 else ""
|
|
||||||
last_down = peer_data["lastDown"]
|
last_down = peer_data["lastDown"]
|
||||||
hours_difference = (
|
hours_difference = (
|
||||||
datetime.fromtimestamp(current_timestamp, tz=timezone.utc) - datetime.fromtimestamp(last_down, tz=timezone.utc)
|
datetime.fromtimestamp(current_timestamp, tz=timezone.utc) - datetime.fromtimestamp(last_down, tz=timezone.utc)
|
||||||
).total_seconds() / 3600
|
).total_seconds() / 3600
|
||||||
|
|
||||||
# Check if peer status is not up
|
if not (peer_status == "up" and remote_disc != 0):
|
||||||
if peer_status != "up":
|
self.result.is_failure(
|
||||||
down_failures.append(f"{peer} is {peer_status} in {vrf} VRF{remote_disc_info}.")
|
f"Peer: {peer} VRF: {vrf} - Session not properly established - State: {peer_status} Remote Discriminator: {remote_disc}"
|
||||||
|
)
|
||||||
|
|
||||||
# Check if the last down is within the threshold
|
# Check if the last down is within the threshold
|
||||||
elif self.inputs.down_threshold and hours_difference < self.inputs.down_threshold:
|
if self.inputs.down_threshold and hours_difference < self.inputs.down_threshold:
|
||||||
up_failures.append(f"{peer} in {vrf} VRF was down {round(hours_difference)} hours ago{remote_disc_info}.")
|
self.result.is_failure(
|
||||||
|
f"Peer: {peer} VRF: {vrf} - Session failure detected within the expected uptime threshold ({round(hours_difference)} hours ago)"
|
||||||
# Check if remote disc is 0
|
)
|
||||||
elif remote_disc == 0:
|
|
||||||
up_failures.append(f"{peer} in {vrf} VRF has remote disc {remote_disc}.")
|
|
||||||
|
|
||||||
# Check if there are any failures
|
|
||||||
if down_failures:
|
|
||||||
down_failures_str = "\n".join(down_failures)
|
|
||||||
self.result.is_failure(f"Following BFD peers are not up:\n{down_failures_str}")
|
|
||||||
if up_failures:
|
|
||||||
up_failures_str = "\n".join(up_failures)
|
|
||||||
self.result.is_failure(f"\nFollowing BFD peers were down:\n{up_failures_str}")
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyBFDPeersRegProtocols(AntaTest):
|
class VerifyBFDPeersRegProtocols(AntaTest):
|
||||||
"""Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.
|
"""Verifies the registered routing protocol of IPv4 BFD peer sessions.
|
||||||
|
|
||||||
|
This test performs the following checks for each specified peer:
|
||||||
|
|
||||||
|
1. Confirms that the specified VRF is configured.
|
||||||
|
2. Verifies that the peer exists in the BFD configuration.
|
||||||
|
3. Confirms that BFD peer is correctly configured with the `routing protocol`.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).
|
* Success: If all of the following conditions are met:
|
||||||
* Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).
|
- All specified peers are found in the BFD configuration within the specified VRF.
|
||||||
|
- All BFD peers are correctly configured with the `routing protocol`.
|
||||||
|
* Failure: If any of the following occur:
|
||||||
|
- A specified peer is not found in the BFD configuration within the specified VRF.
|
||||||
|
- Any BFD peer not correctly configured with the `routing protocol`.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -308,8 +288,6 @@ class VerifyBFDPeersRegProtocols(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBFDPeersRegProtocols"
|
|
||||||
description = "Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered."
|
|
||||||
categories: ClassVar[list[str]] = ["bfd"]
|
categories: ClassVar[list[str]] = ["bfd"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show bfd peers detail", revision=1)]
|
||||||
|
|
||||||
|
@ -317,23 +295,14 @@ class VerifyBFDPeersRegProtocols(AntaTest):
|
||||||
"""Input model for the VerifyBFDPeersRegProtocols test."""
|
"""Input model for the VerifyBFDPeersRegProtocols test."""
|
||||||
|
|
||||||
bfd_peers: list[BFDPeer]
|
bfd_peers: list[BFDPeer]
|
||||||
"""List of IPv4 BFD peers."""
|
"""List of IPv4 BFD"""
|
||||||
|
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
|
||||||
class BFDPeer(BaseModel):
|
"""To maintain backward compatibility"""
|
||||||
"""Model for an IPv4 BFD peer."""
|
|
||||||
|
|
||||||
peer_address: IPv4Address
|
|
||||||
"""IPv4 address of a BFD peer."""
|
|
||||||
vrf: str = "default"
|
|
||||||
"""Optional VRF for BFD peer. If not provided, it defaults to `default`."""
|
|
||||||
protocols: list[BfdProtocol]
|
|
||||||
"""List of protocols to be verified."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyBFDPeersRegProtocols."""
|
"""Main test function for VerifyBFDPeersRegProtocols."""
|
||||||
# Initialize failure messages
|
self.result.is_success()
|
||||||
failures: dict[Any, Any] = {}
|
|
||||||
|
|
||||||
# Iterating over BFD peers, extract the parameters and command output
|
# Iterating over BFD peers, extract the parameters and command output
|
||||||
for bfd_peer in self.inputs.bfd_peers:
|
for bfd_peer in self.inputs.bfd_peers:
|
||||||
|
@ -348,16 +317,11 @@ class VerifyBFDPeersRegProtocols(AntaTest):
|
||||||
|
|
||||||
# Check if BFD peer configured
|
# Check if BFD peer configured
|
||||||
if not bfd_output:
|
if not bfd_output:
|
||||||
failures[peer] = {vrf: "Not Configured"}
|
self.result.is_failure(f"{bfd_peer} - Not found")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check registered protocols
|
# Check registered protocols
|
||||||
difference = set(protocols) - set(get_value(bfd_output, "peerStatsDetail.apps"))
|
difference = sorted(set(protocols) - set(get_value(bfd_output, "peerStatsDetail.apps")))
|
||||||
|
|
||||||
if difference:
|
if difference:
|
||||||
failures[peer] = {vrf: sorted(difference)}
|
failures = " ".join(f"`{item}`" for item in difference)
|
||||||
|
self.result.is_failure(f"{bfd_peer} - {failures} routing protocol(s) not configured")
|
||||||
if not failures:
|
|
||||||
self.result.is_success()
|
|
||||||
else:
|
|
||||||
self.result.is_failure(f"The following BFD peers are not configured or have non-registered protocol(s):\n{failures}")
|
|
||||||
|
|
|
@ -33,8 +33,6 @@ class VerifyZeroTouch(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyZeroTouch"
|
|
||||||
description = "Verifies ZeroTouch is disabled"
|
|
||||||
categories: ClassVar[list[str]] = ["configuration"]
|
categories: ClassVar[list[str]] = ["configuration"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show zerotouch", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show zerotouch", revision=1)]
|
||||||
|
|
||||||
|
@ -64,8 +62,6 @@ class VerifyRunningConfigDiffs(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyRunningConfigDiffs"
|
|
||||||
description = "Verifies there is no difference between the running-config and the startup-config"
|
|
||||||
categories: ClassVar[list[str]] = ["configuration"]
|
categories: ClassVar[list[str]] = ["configuration"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config diffs", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config diffs", ofmt="text")]
|
||||||
|
|
||||||
|
@ -98,13 +94,12 @@ class VerifyRunningConfigLines(AntaTest):
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.configuration:
|
anta.tests.configuration:
|
||||||
- VerifyRunningConfigLines:
|
- VerifyRunningConfigLines:
|
||||||
regex_patterns:
|
regex_patterns:
|
||||||
- "^enable password.*$"
|
- "^enable password.*$"
|
||||||
- "bla bla"
|
- "bla bla"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyRunningConfigLines"
|
|
||||||
description = "Search the Running-Config for the given RegEx patterns."
|
description = "Search the Running-Config for the given RegEx patterns."
|
||||||
categories: ClassVar[list[str]] = ["configuration"]
|
categories: ClassVar[list[str]] = ["configuration"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config", ofmt="text")]
|
||||||
|
|
|
@ -7,12 +7,9 @@
|
||||||
# mypy: disable-error-code=attr-defined
|
# mypy: disable-error-code=attr-defined
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from anta.input_models.connectivity import Host, LLDPNeighbor, Neighbor
|
||||||
|
|
||||||
from anta.custom_types import Interface
|
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,11 +40,8 @@ class VerifyReachability(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyReachability"
|
|
||||||
description = "Test the network reachability to one or many destination IP(s)."
|
|
||||||
categories: ClassVar[list[str]] = ["connectivity"]
|
categories: ClassVar[list[str]] = ["connectivity"]
|
||||||
# Removing the <space> between '{size}' and '{df_bit}' to compensate the df-bit set default value
|
# Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled
|
||||||
# i.e if df-bit kept disable then it will add redundant space in between the command
|
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaTemplate(template="ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}", revision=1)
|
AntaTemplate(template="ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}", revision=1)
|
||||||
]
|
]
|
||||||
|
@ -57,62 +51,43 @@ class VerifyReachability(AntaTest):
|
||||||
|
|
||||||
hosts: list[Host]
|
hosts: list[Host]
|
||||||
"""List of host to ping."""
|
"""List of host to ping."""
|
||||||
|
Host: ClassVar[type[Host]] = Host
|
||||||
class Host(BaseModel):
|
"""To maintain backward compatibility."""
|
||||||
"""Model for a remote host to ping."""
|
|
||||||
|
|
||||||
destination: IPv4Address
|
|
||||||
"""IPv4 address to ping."""
|
|
||||||
source: IPv4Address | Interface
|
|
||||||
"""IPv4 address source IP or egress interface to use."""
|
|
||||||
vrf: str = "default"
|
|
||||||
"""VRF context. Defaults to `default`."""
|
|
||||||
repeat: int = 2
|
|
||||||
"""Number of ping repetition. Defaults to 2."""
|
|
||||||
size: int = 100
|
|
||||||
"""Specify datagram size. Defaults to 100."""
|
|
||||||
df_bit: bool = False
|
|
||||||
"""Enable do not fragment bit in IP header. Defaults to False."""
|
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
||||||
"""Render the template for each host in the input list."""
|
"""Render the template for each host in the input list."""
|
||||||
commands = []
|
return [
|
||||||
for host in self.inputs.hosts:
|
template.render(
|
||||||
# Enables do not fragment bit in IP header if needed else keeping disable.
|
destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=" df-bit" if host.df_bit else ""
|
||||||
# Adding the <space> at start to compensate change in AntaTemplate
|
)
|
||||||
df_bit = " df-bit" if host.df_bit else ""
|
for host in self.inputs.hosts
|
||||||
command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)
|
]
|
||||||
commands.append(command)
|
|
||||||
return commands
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyReachability."""
|
"""Main test function for VerifyReachability."""
|
||||||
failures = []
|
self.result.is_success()
|
||||||
|
|
||||||
for command in self.instance_commands:
|
for command, host in zip(self.instance_commands, self.inputs.hosts):
|
||||||
src = command.params.source
|
if f"{host.repeat} received" not in command.json_output["messages"][0]:
|
||||||
dst = command.params.destination
|
self.result.is_failure(f"{host} - Unreachable")
|
||||||
repeat = command.params.repeat
|
|
||||||
|
|
||||||
if f"{repeat} 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}")
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyLLDPNeighbors(AntaTest):
|
class VerifyLLDPNeighbors(AntaTest):
|
||||||
"""Verifies that the provided LLDP neighbors are present and connected with the correct configuration.
|
"""Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.
|
||||||
|
|
||||||
|
This test performs the following checks for each specified LLDP neighbor:
|
||||||
|
|
||||||
|
1. Confirming matching ports on both local and neighboring devices.
|
||||||
|
2. Ensuring compatibility of device names and interface identifiers.
|
||||||
|
3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if each of the provided LLDP neighbors is present and connected to the specified port and device.
|
* Success: The test will pass if all the provided LLDP neighbors are present and correctly connected to the specified port and device.
|
||||||
* Failure: The test will fail if any of the following conditions are met:
|
* Failure: The test will fail if any of the following conditions are met:
|
||||||
- The provided LLDP neighbor is not found.
|
- The provided LLDP neighbor is not found in the LLDP table.
|
||||||
- The system name or port of the LLDP neighbor does not match the provided information.
|
- The system name or port of the LLDP neighbor does not match the expected information.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -129,60 +104,37 @@ class VerifyLLDPNeighbors(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLLDPNeighbors"
|
|
||||||
description = "Verifies that the provided LLDP neighbors are connected properly."
|
|
||||||
categories: ClassVar[list[str]] = ["connectivity"]
|
categories: ClassVar[list[str]] = ["connectivity"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lldp neighbors detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lldp neighbors detail", revision=1)]
|
||||||
|
|
||||||
class Input(AntaTest.Input):
|
class Input(AntaTest.Input):
|
||||||
"""Input model for the VerifyLLDPNeighbors test."""
|
"""Input model for the VerifyLLDPNeighbors test."""
|
||||||
|
|
||||||
neighbors: list[Neighbor]
|
neighbors: list[LLDPNeighbor]
|
||||||
"""List of LLDP neighbors."""
|
"""List of LLDP neighbors."""
|
||||||
|
Neighbor: ClassVar[type[Neighbor]] = Neighbor
|
||||||
class Neighbor(BaseModel):
|
"""To maintain backward compatibility."""
|
||||||
"""Model for an LLDP neighbor."""
|
|
||||||
|
|
||||||
port: Interface
|
|
||||||
"""LLDP port."""
|
|
||||||
neighbor_device: str
|
|
||||||
"""LLDP neighbor device."""
|
|
||||||
neighbor_port: Interface
|
|
||||||
"""LLDP neighbor port."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyLLDPNeighbors."""
|
"""Main test function for VerifyLLDPNeighbors."""
|
||||||
failures: dict[str, list[str]] = {}
|
self.result.is_success()
|
||||||
|
|
||||||
output = self.instance_commands[0].json_output["lldpNeighbors"]
|
output = self.instance_commands[0].json_output["lldpNeighbors"]
|
||||||
|
|
||||||
for neighbor in self.inputs.neighbors:
|
for neighbor in self.inputs.neighbors:
|
||||||
if neighbor.port not in output:
|
if neighbor.port not in output:
|
||||||
failures.setdefault("Port(s) not configured", []).append(neighbor.port)
|
self.result.is_failure(f"{neighbor} - Port not found")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(lldp_neighbor_info := output[neighbor.port]["lldpNeighborInfo"]) == 0:
|
if len(lldp_neighbor_info := output[neighbor.port]["lldpNeighborInfo"]) == 0:
|
||||||
failures.setdefault("No LLDP neighbor(s) on port(s)", []).append(neighbor.port)
|
self.result.is_failure(f"{neighbor} - No LLDP neighbors")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not any(
|
# Check if the system name and neighbor port matches
|
||||||
|
match_found = any(
|
||||||
info["systemName"] == neighbor.neighbor_device and info["neighborInterfaceInfo"]["interfaceId_v2"] == neighbor.neighbor_port
|
info["systemName"] == neighbor.neighbor_device and info["neighborInterfaceInfo"]["interfaceId_v2"] == neighbor.neighbor_port
|
||||||
for info in lldp_neighbor_info
|
for info in lldp_neighbor_info
|
||||||
):
|
)
|
||||||
neighbors = "\n ".join(
|
if not match_found:
|
||||||
[
|
failure_msg = [f"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}" for info in lldp_neighbor_info]
|
||||||
f"{neighbor[0]}_{neighbor[1]}"
|
self.result.is_failure(f"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}")
|
||||||
for neighbor in [(info["systemName"], info["neighborInterfaceInfo"]["interfaceId_v2"]) for info in lldp_neighbor_info]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
failures.setdefault("Wrong LLDP neighbor(s) on port(s)", []).append(f"{neighbor.port}\n {neighbors}")
|
|
||||||
|
|
||||||
if not failures:
|
|
||||||
self.result.is_success()
|
|
||||||
else:
|
|
||||||
failure_messages = []
|
|
||||||
for failure_type, ports in failures.items():
|
|
||||||
ports_str = "\n ".join(ports)
|
|
||||||
failure_messages.append(f"{failure_type}:\n {ports_str}")
|
|
||||||
self.result.is_failure("\n".join(failure_messages))
|
|
||||||
|
|
283
anta/tests/cvx.py
Normal file
283
anta/tests/cvx.py
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Module related to the CVX tests."""
|
||||||
|
|
||||||
|
# Mypy does not understand AntaTest.Input typing
|
||||||
|
# mypy: disable-error-code=attr-defined
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
||||||
|
|
||||||
|
from anta.custom_types import PositiveInteger
|
||||||
|
from anta.models import AntaCommand, AntaTest
|
||||||
|
from anta.tools import get_value
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from anta.models import AntaTemplate
|
||||||
|
from anta.input_models.cvx import CVXPeers
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyMcsClientMounts(AntaTest):
|
||||||
|
"""Verify if all MCS client mounts are in mountStateMountComplete.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.
|
||||||
|
* Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.cvx:
|
||||||
|
- VerifyMcsClientMounts:
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["cvx"]
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management cvx mounts", revision=1)]
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Main test function for VerifyMcsClientMounts."""
|
||||||
|
command_output = self.instance_commands[0].json_output
|
||||||
|
self.result.is_success()
|
||||||
|
mount_states = command_output["mountStates"]
|
||||||
|
mcs_mount_state_detected = False
|
||||||
|
for mount_state in mount_states:
|
||||||
|
if not mount_state["type"].startswith("Mcs"):
|
||||||
|
continue
|
||||||
|
mcs_mount_state_detected = True
|
||||||
|
if (state := mount_state["state"]) != "mountStateMountComplete":
|
||||||
|
self.result.is_failure(f"MCS Client mount states are not valid: {state}")
|
||||||
|
|
||||||
|
if not mcs_mount_state_detected:
|
||||||
|
self.result.is_failure("MCS Client mount states are not present")
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyManagementCVX(AntaTest):
|
||||||
|
"""Verifies the management CVX global status.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: The test will pass if the management CVX global status matches the expected status.
|
||||||
|
* Failure: The test will fail if the management CVX global status does not match the expected status.
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.cvx:
|
||||||
|
- VerifyManagementCVX:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["cvx"]
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management cvx", revision=3)]
|
||||||
|
|
||||||
|
class Input(AntaTest.Input):
|
||||||
|
"""Input model for the VerifyManagementCVX test."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
"""Whether management CVX must be enabled (True) or disabled (False)."""
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Main test function for VerifyManagementCVX."""
|
||||||
|
command_output = self.instance_commands[0].json_output
|
||||||
|
self.result.is_success()
|
||||||
|
if (cluster_state := get_value(command_output, "clusterStatus.enabled")) != self.inputs.enabled:
|
||||||
|
self.result.is_failure(f"Management CVX status is not valid: {cluster_state}")
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyMcsServerMounts(AntaTest):
|
||||||
|
"""Verify if all MCS server mounts are in a MountComplete state.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: The test will pass if all the MCS mount status on MCS server are mountStateMountComplete.
|
||||||
|
* Failure: The test will fail even if any MCS server mount status is not mountStateMountComplete.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.cvx:
|
||||||
|
|
||||||
|
- VerifyMcsServerMounts:
|
||||||
|
connections_count: 100
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["cvx"]
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show cvx mounts", revision=1)]
|
||||||
|
|
||||||
|
mcs_path_types: ClassVar[list[str]] = ["Mcs::ApiConfigRedundancyStatus", "Mcs::ActiveFlows", "Mcs::Client::Status"]
|
||||||
|
"""The list of expected MCS path types to verify."""
|
||||||
|
|
||||||
|
class Input(AntaTest.Input):
|
||||||
|
"""Input model for the VerifyMcsServerMounts test."""
|
||||||
|
|
||||||
|
connections_count: int
|
||||||
|
"""The expected number of active CVX Connections with mountStateMountComplete"""
|
||||||
|
|
||||||
|
def validate_mount_states(self, mount: dict[str, Any], hostname: str) -> None:
|
||||||
|
"""Validate the mount states of a given mount."""
|
||||||
|
mount_states = mount["mountStates"][0]
|
||||||
|
|
||||||
|
if (num_path_states := len(mount_states["pathStates"])) != (expected_num := len(self.mcs_path_types)):
|
||||||
|
self.result.is_failure(f"Incorrect number of mount path states for {hostname} - Expected: {expected_num}, Actual: {num_path_states}")
|
||||||
|
|
||||||
|
for path in mount_states["pathStates"]:
|
||||||
|
if (path_type := path.get("type")) not in self.mcs_path_types:
|
||||||
|
self.result.is_failure(f"Unexpected MCS path type for {hostname}: '{path_type}'.")
|
||||||
|
if (path_state := path.get("state")) != "mountStateMountComplete":
|
||||||
|
self.result.is_failure(f"MCS server mount state for path '{path_type}' is not valid is for {hostname}: '{path_state}'.")
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Main test function for VerifyMcsServerMounts."""
|
||||||
|
command_output = self.instance_commands[0].json_output
|
||||||
|
self.result.is_success()
|
||||||
|
active_count = 0
|
||||||
|
|
||||||
|
if not (connections := command_output.get("connections")):
|
||||||
|
self.result.is_failure("CVX connections are not available.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for connection in connections:
|
||||||
|
mounts = connection.get("mounts", [])
|
||||||
|
hostname = connection["hostname"]
|
||||||
|
|
||||||
|
mcs_mounts = [mount for mount in mounts if mount["service"] == "Mcs"]
|
||||||
|
|
||||||
|
if not mounts:
|
||||||
|
self.result.is_failure(f"No mount status for {hostname}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not mcs_mounts:
|
||||||
|
self.result.is_failure(f"MCS mount state not detected for {hostname}")
|
||||||
|
else:
|
||||||
|
for mount in mcs_mounts:
|
||||||
|
self.validate_mount_states(mount, hostname)
|
||||||
|
active_count += 1
|
||||||
|
|
||||||
|
if active_count != self.inputs.connections_count:
|
||||||
|
self.result.is_failure(f"Incorrect CVX successful connections count. Expected: {self.inputs.connections_count}, Actual : {active_count}")
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyActiveCVXConnections(AntaTest):
|
||||||
|
"""Verifies the number of active CVX Connections.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: The test will pass if number of connections is equal to the expected number of connections.
|
||||||
|
* Failure: The test will fail otherwise.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.cvx:
|
||||||
|
- VerifyActiveCVXConnections:
|
||||||
|
connections_count: 100
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["cvx"]
|
||||||
|
# TODO: @gmuloc - cover "% Unavailable command (controller not ready)"
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show cvx connections brief", revision=1)]
|
||||||
|
|
||||||
|
class Input(AntaTest.Input):
|
||||||
|
"""Input model for the VerifyActiveCVXConnections test."""
|
||||||
|
|
||||||
|
connections_count: PositiveInteger
|
||||||
|
"""The expected number of active CVX Connections."""
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Main test function for VerifyActiveCVXConnections."""
|
||||||
|
command_output = self.instance_commands[0].json_output
|
||||||
|
self.result.is_success()
|
||||||
|
|
||||||
|
if not (connections := command_output.get("connections")):
|
||||||
|
self.result.is_failure("CVX connections are not available.")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_count = len([connection for connection in connections if connection.get("oobConnectionActive")])
|
||||||
|
|
||||||
|
if self.inputs.connections_count != active_count:
|
||||||
|
self.result.is_failure(f"CVX active connections count. Expected: {self.inputs.connections_count}, Actual : {active_count}")
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyCVXClusterStatus(AntaTest):
|
||||||
|
"""Verifies the CVX Server Cluster status.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: The test will pass if all of the following conditions is met:
|
||||||
|
- CVX Enabled state is true
|
||||||
|
- Cluster Mode is true
|
||||||
|
- Role is either Master or Standby.
|
||||||
|
- peer_status matches defined state
|
||||||
|
* Failure: The test will fail if any of the success conditions is not met.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.cvx:
|
||||||
|
- VerifyCVXClusterStatus:
|
||||||
|
role: Master
|
||||||
|
peer_status:
|
||||||
|
- peer_name : cvx-red-2
|
||||||
|
registration_state: Registration complete
|
||||||
|
- peer_name: cvx-red-3
|
||||||
|
registration_state: Registration error
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["cvx"]
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show cvx", revision=1)]
|
||||||
|
|
||||||
|
class Input(AntaTest.Input):
|
||||||
|
"""Input model for the VerifyCVXClusterStatus test."""
|
||||||
|
|
||||||
|
role: Literal["Master", "Standby", "Disconnected"] = "Master"
|
||||||
|
peer_status: list[CVXPeers]
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Run the main test for VerifyCVXClusterStatus."""
|
||||||
|
command_output = self.instance_commands[0].json_output
|
||||||
|
self.result.is_success()
|
||||||
|
|
||||||
|
# Validate Server enabled status
|
||||||
|
if not command_output.get("enabled"):
|
||||||
|
self.result.is_failure("CVX Server status is not enabled")
|
||||||
|
|
||||||
|
# Validate cluster status and mode
|
||||||
|
if not (cluster_status := command_output.get("clusterStatus")) or not command_output.get("clusterMode"):
|
||||||
|
self.result.is_failure("CVX Server is not a cluster")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check cluster role
|
||||||
|
if (cluster_role := cluster_status.get("role")) != self.inputs.role:
|
||||||
|
self.result.is_failure(f"CVX Role is not valid: {cluster_role}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate peer status
|
||||||
|
peer_cluster = cluster_status.get("peerStatus", {})
|
||||||
|
|
||||||
|
# Check peer count
|
||||||
|
if (num_of_peers := len(peer_cluster)) != (expected_num_of_peers := len(self.inputs.peer_status)):
|
||||||
|
self.result.is_failure(f"Unexpected number of peers {num_of_peers} vs {expected_num_of_peers}")
|
||||||
|
|
||||||
|
# Check each peer
|
||||||
|
for peer in self.inputs.peer_status:
|
||||||
|
# Retrieve the peer status from the peer cluster
|
||||||
|
if (eos_peer_status := get_value(peer_cluster, peer.peer_name, separator="..")) is None:
|
||||||
|
self.result.is_failure(f"{peer.peer_name} is not present")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Validate the registration state of the peer
|
||||||
|
if (peer_reg_state := eos_peer_status.get("registrationState")) != peer.registration_state:
|
||||||
|
self.result.is_failure(f"{peer.peer_name} registration state is not complete: {peer_reg_state}")
|
|
@ -34,7 +34,6 @@ class VerifyFieldNotice44Resolution(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyFieldNotice44Resolution"
|
|
||||||
description = "Verifies that the device is using the correct Aboot version per FN0044."
|
description = "Verifies that the device is using the correct Aboot version per FN0044."
|
||||||
categories: ClassVar[list[str]] = ["field notices"]
|
categories: ClassVar[list[str]] = ["field notices"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
||||||
|
@ -110,15 +109,11 @@ class VerifyFieldNotice44Resolution(AntaTest):
|
||||||
|
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
incorrect_aboot_version = (
|
incorrect_aboot_version = (
|
||||||
aboot_version.startswith("4.0.")
|
(aboot_version.startswith("4.0.") and int(aboot_version.split(".")[2]) < 7)
|
||||||
and int(aboot_version.split(".")[2]) < 7
|
or (aboot_version.startswith("4.1.") and int(aboot_version.split(".")[2]) < 1)
|
||||||
or aboot_version.startswith("4.1.")
|
|
||||||
and int(aboot_version.split(".")[2]) < 1
|
|
||||||
or (
|
or (
|
||||||
aboot_version.startswith("6.0.")
|
(aboot_version.startswith("6.0.") and int(aboot_version.split(".")[2]) < 9)
|
||||||
and int(aboot_version.split(".")[2]) < 9
|
or (aboot_version.startswith("6.1.") and int(aboot_version.split(".")[2]) < 7)
|
||||||
or aboot_version.startswith("6.1.")
|
|
||||||
and int(aboot_version.split(".")[2]) < 7
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if incorrect_aboot_version:
|
if incorrect_aboot_version:
|
||||||
|
@ -143,7 +138,6 @@ class VerifyFieldNotice72Resolution(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyFieldNotice72Resolution"
|
|
||||||
description = "Verifies if the device is exposed to FN0072, and if the issue has been mitigated."
|
description = "Verifies if the device is exposed to FN0072, and if the issue has been mitigated."
|
||||||
categories: ClassVar[list[str]] = ["field notices"]
|
categories: ClassVar[list[str]] = ["field notices"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
||||||
|
|
|
@ -17,8 +17,7 @@ from anta.tools import get_failed_logs
|
||||||
|
|
||||||
|
|
||||||
def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:
|
def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:
|
||||||
"""
|
"""Validate the record export configuration against the tracker info.
|
||||||
Validate the record export configuration against the tracker info.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -41,8 +40,7 @@ def validate_record_export(record_export: dict[str, str], tracker_info: dict[str
|
||||||
|
|
||||||
|
|
||||||
def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:
|
def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:
|
||||||
"""
|
"""Validate the exporter configurations against the tracker info.
|
||||||
Validate the exporter configurations against the tracker info.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -74,8 +72,7 @@ def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str,
|
||||||
|
|
||||||
|
|
||||||
class VerifyHardwareFlowTrackerStatus(AntaTest):
|
class VerifyHardwareFlowTrackerStatus(AntaTest):
|
||||||
"""
|
"""Verifies if hardware flow tracking is running and an input tracker is active.
|
||||||
Verifies if hardware flow tracking is running and an input tracker is active.
|
|
||||||
|
|
||||||
This test optionally verifies the tracker interval/timeout and exporter configuration.
|
This test optionally verifies the tracker interval/timeout and exporter configuration.
|
||||||
|
|
||||||
|
@ -89,7 +86,7 @@ class VerifyHardwareFlowTrackerStatus(AntaTest):
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.flow_tracking:
|
anta.tests.flow_tracking:
|
||||||
- VerifyFlowTrackingHardware:
|
- VerifyHardwareFlowTrackerStatus:
|
||||||
trackers:
|
trackers:
|
||||||
- name: FLOW-TRACKER
|
- name: FLOW-TRACKER
|
||||||
record_export:
|
record_export:
|
||||||
|
@ -102,7 +99,6 @@ class VerifyHardwareFlowTrackerStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyHardwareFlowTrackerStatus"
|
|
||||||
description = (
|
description = (
|
||||||
"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration."
|
"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration."
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,11 +25,11 @@ class VerifyGreenTCounters(AntaTest):
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.greent:
|
anta.tests.greent:
|
||||||
- VerifyGreenT:
|
- VerifyGreenTCounters:
|
||||||
```
|
```
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyGreenTCounters"
|
|
||||||
description = "Verifies if the GreenT counters are incremented."
|
description = "Verifies if the GreenT counters are incremented."
|
||||||
categories: ClassVar[list[str]] = ["greent"]
|
categories: ClassVar[list[str]] = ["greent"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard counters", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard counters", revision=1)]
|
||||||
|
@ -57,12 +57,12 @@ class VerifyGreenT(AntaTest):
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.greent:
|
anta.tests.greent:
|
||||||
- VerifyGreenTCounters:
|
- VerifyGreenT:
|
||||||
```
|
```
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyGreenT"
|
description = "Verifies if a GreenT policy other than the default is created."
|
||||||
description = "Verifies if a GreenT policy is created."
|
|
||||||
categories: ClassVar[list[str]] = ["greent"]
|
categories: ClassVar[list[str]] = ["greent"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard policy profile", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard policy profile", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,6 @@ class VerifyTransceiversManufacturers(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTransceiversManufacturers"
|
|
||||||
description = "Verifies if all transceivers come from approved manufacturers."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show inventory", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show inventory", revision=2)]
|
||||||
|
|
||||||
|
@ -77,8 +75,6 @@ class VerifyTemperature(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTemperature"
|
|
||||||
description = "Verifies the device temperature."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)]
|
||||||
|
|
||||||
|
@ -110,8 +106,6 @@ class VerifyTransceiversTemperature(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTransceiversTemperature"
|
|
||||||
description = "Verifies the transceivers temperature."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature transceiver", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature transceiver", revision=1)]
|
||||||
|
|
||||||
|
@ -151,8 +145,6 @@ class VerifyEnvironmentSystemCooling(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyEnvironmentSystemCooling"
|
|
||||||
description = "Verifies the system cooling status."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment cooling", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment cooling", revision=1)]
|
||||||
|
|
||||||
|
@ -232,8 +224,6 @@ class VerifyEnvironmentPower(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyEnvironmentPower"
|
|
||||||
description = "Verifies the power supplies status."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment power", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment power", revision=1)]
|
||||||
|
|
||||||
|
@ -274,7 +264,6 @@ class VerifyAdverseDrops(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAdverseDrops"
|
|
||||||
description = "Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches."
|
description = "Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches."
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware counter drop", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware counter drop", revision=1)]
|
||||||
|
|
|
@ -8,17 +8,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from ipaddress import IPv4Network
|
from ipaddress import IPv4Interface
|
||||||
from typing import Any, ClassVar, Literal
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_extra_types.mac_address import MacAddress
|
from pydantic_extra_types.mac_address import MacAddress
|
||||||
|
|
||||||
from anta import GITHUB_SUGGESTION
|
from anta import GITHUB_SUGGESTION
|
||||||
from anta.custom_types import EthernetInterface, Interface, Percent, PortChannelInterface, PositiveInteger
|
from anta.custom_types import EthernetInterface, Interface, Percent, PositiveInteger
|
||||||
from anta.decorators import skip_on_platforms
|
from anta.decorators import skip_on_platforms
|
||||||
|
from anta.input_models.interfaces import InterfaceState
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
from anta.tools import custom_division, get_failed_logs, get_item, get_value
|
from anta.tools import custom_division, format_data, get_failed_logs, get_item, get_value
|
||||||
|
|
||||||
BPS_GBPS_CONVERSIONS = 1000000000
|
BPS_GBPS_CONVERSIONS = 1000000000
|
||||||
|
|
||||||
|
@ -44,8 +45,6 @@ class VerifyInterfaceUtilization(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfaceUtilization"
|
|
||||||
description = "Verifies that the utilization of interfaces is below a certain threshold."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="show interfaces counters rates", revision=1),
|
AntaCommand(command="show interfaces counters rates", revision=1),
|
||||||
|
@ -105,8 +104,6 @@ class VerifyInterfaceErrors(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfaceErrors"
|
|
||||||
description = "Verifies there are no interface error counters."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters errors", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters errors", revision=1)]
|
||||||
|
|
||||||
|
@ -140,8 +137,6 @@ class VerifyInterfaceDiscards(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfaceDiscards"
|
|
||||||
description = "Verifies there are no interface discard counters."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters discards", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces counters discards", revision=1)]
|
||||||
|
|
||||||
|
@ -174,8 +169,6 @@ class VerifyInterfaceErrDisabled(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfaceErrDisabled"
|
|
||||||
description = "Verifies there are no interfaces in the errdisabled state."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces status", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces status", revision=1)]
|
||||||
|
|
||||||
|
@ -191,16 +184,20 @@ class VerifyInterfaceErrDisabled(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyInterfacesStatus(AntaTest):
|
class VerifyInterfacesStatus(AntaTest):
|
||||||
"""Verifies if the provided list of interfaces are all in the expected state.
|
"""Verifies the operational states of specified interfaces to ensure they match expected configurations.
|
||||||
|
|
||||||
- If line protocol status is provided, prioritize checking against both status and line protocol status
|
This test performs the following checks for each specified interface:
|
||||||
- If line protocol status is not provided and interface status is "up", expect both status and line protocol to be "up"
|
|
||||||
- If interface status is not "up", check only the interface status without considering line protocol status
|
1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.
|
||||||
|
2. If `line_protocol_status` is not provided but the `status` is "up", it is assumed that both the status and line protocol should be "up".
|
||||||
|
3. If the interface `status` is not "up", only the interface's status is validated, with no line protocol check performed.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if the provided interfaces are all in the expected state.
|
* Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.
|
||||||
* Failure: The test will fail if any interface is not in the expected state.
|
* Failure: If any of the following occur:
|
||||||
|
- The specified interface is not configured.
|
||||||
|
- The specified interface status and line protocol status does not match the expected operational state for any interface.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -219,8 +216,6 @@ class VerifyInterfacesStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfacesStatus"
|
|
||||||
description = "Verifies the status of the provided interfaces."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
|
||||||
|
|
||||||
|
@ -229,30 +224,17 @@ class VerifyInterfacesStatus(AntaTest):
|
||||||
|
|
||||||
interfaces: list[InterfaceState]
|
interfaces: list[InterfaceState]
|
||||||
"""List of interfaces with their expected state."""
|
"""List of interfaces with their expected state."""
|
||||||
|
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
|
||||||
class InterfaceState(BaseModel):
|
|
||||||
"""Model for an interface state."""
|
|
||||||
|
|
||||||
name: Interface
|
|
||||||
"""Interface to validate."""
|
|
||||||
status: Literal["up", "down", "adminDown"]
|
|
||||||
"""Expected status of the interface."""
|
|
||||||
line_protocol_status: Literal["up", "down", "testing", "unknown", "dormant", "notPresent", "lowerLayerDown"] | None = None
|
|
||||||
"""Expected line protocol status of the interface."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyInterfacesStatus."""
|
"""Main test function for VerifyInterfacesStatus."""
|
||||||
command_output = self.instance_commands[0].json_output
|
|
||||||
|
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
|
|
||||||
intf_not_configured = []
|
command_output = self.instance_commands[0].json_output
|
||||||
intf_wrong_state = []
|
|
||||||
|
|
||||||
for interface in self.inputs.interfaces:
|
for interface in self.inputs.interfaces:
|
||||||
if (intf_status := get_value(command_output["interfaceDescriptions"], interface.name, separator="..")) is None:
|
if (intf_status := get_value(command_output["interfaceDescriptions"], interface.name, separator="..")) is None:
|
||||||
intf_not_configured.append(interface.name)
|
self.result.is_failure(f"{interface.name} - Not configured")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
status = "up" if intf_status["interfaceStatus"] in {"up", "connected"} else intf_status["interfaceStatus"]
|
status = "up" if intf_status["interfaceStatus"] in {"up", "connected"} else intf_status["interfaceStatus"]
|
||||||
|
@ -261,18 +243,15 @@ class VerifyInterfacesStatus(AntaTest):
|
||||||
# If line protocol status is provided, prioritize checking against both status and line protocol status
|
# If line protocol status is provided, prioritize checking against both status and line protocol status
|
||||||
if interface.line_protocol_status:
|
if interface.line_protocol_status:
|
||||||
if interface.status != status or interface.line_protocol_status != proto:
|
if interface.status != status or interface.line_protocol_status != proto:
|
||||||
intf_wrong_state.append(f"{interface.name} is {status}/{proto}")
|
actual_state = f"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}"
|
||||||
|
self.result.is_failure(f"{interface.name} - {actual_state}")
|
||||||
|
|
||||||
# If line protocol status is not provided and interface status is "up", expect both status and proto to be "up"
|
# If line protocol status is not provided and interface status is "up", expect both status and proto to be "up"
|
||||||
# If interface status is not "up", check only the interface status without considering line protocol status
|
# If interface status is not "up", check only the interface status without considering line protocol status
|
||||||
elif (interface.status == "up" and (status != "up" or proto != "up")) or (interface.status != status):
|
elif interface.status == "up" and (status != "up" or proto != "up"):
|
||||||
intf_wrong_state.append(f"{interface.name} is {status}/{proto}")
|
self.result.is_failure(f"{interface.name} - Expected: up/up, Actual: {status}/{proto}")
|
||||||
|
elif interface.status != status:
|
||||||
if intf_not_configured:
|
self.result.is_failure(f"{interface.name} - Expected: {interface.status}, Actual: {status}")
|
||||||
self.result.is_failure(f"The following interface(s) are not configured: {intf_not_configured}")
|
|
||||||
|
|
||||||
if intf_wrong_state:
|
|
||||||
self.result.is_failure(f"The following interface(s) are not in the expected state: {intf_wrong_state}")
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyStormControlDrops(AntaTest):
|
class VerifyStormControlDrops(AntaTest):
|
||||||
|
@ -291,8 +270,6 @@ class VerifyStormControlDrops(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyStormControlDrops"
|
|
||||||
description = "Verifies there are no interface storm-control drop counters."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show storm-control", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show storm-control", revision=1)]
|
||||||
|
|
||||||
|
@ -329,8 +306,6 @@ class VerifyPortChannels(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPortChannels"
|
|
||||||
description = "Verifies there are no inactive ports in all port channels."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show port-channel", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show port-channel", revision=1)]
|
||||||
|
|
||||||
|
@ -364,8 +339,6 @@ class VerifyIllegalLACP(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIllegalLACP"
|
|
||||||
description = "Verifies there are no illegal LACP packets in all port channels."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lacp counters all-ports", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lacp counters all-ports", revision=1)]
|
||||||
|
|
||||||
|
@ -401,7 +374,6 @@ class VerifyLoopbackCount(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoopbackCount"
|
|
||||||
description = "Verifies the number of loopback interfaces and their status."
|
description = "Verifies the number of loopback interfaces and their status."
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip interface brief", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip interface brief", revision=1)]
|
||||||
|
@ -450,8 +422,6 @@ class VerifySVI(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySVI"
|
|
||||||
description = "Verifies the status of all SVIs."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip interface brief", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip interface brief", revision=1)]
|
||||||
|
|
||||||
|
@ -495,7 +465,6 @@ class VerifyL3MTU(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyL3MTU"
|
|
||||||
description = "Verifies the global L3 MTU of all L3 interfaces."
|
description = "Verifies the global L3 MTU of all L3 interfaces."
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
||||||
|
@ -553,7 +522,6 @@ class VerifyIPProxyARP(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIPProxyARP"
|
|
||||||
description = "Verifies if Proxy ARP is enabled."
|
description = "Verifies if Proxy ARP is enabled."
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip interface {intf}", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip interface {intf}", revision=2)]
|
||||||
|
@ -607,7 +575,6 @@ class VerifyL2MTU(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyL2MTU"
|
|
||||||
description = "Verifies the global L2 MTU of all L2 interfaces."
|
description = "Verifies the global L2 MTU of all L2 interfaces."
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
||||||
|
@ -662,14 +629,13 @@ class VerifyInterfaceIPv4(AntaTest):
|
||||||
- VerifyInterfaceIPv4:
|
- VerifyInterfaceIPv4:
|
||||||
interfaces:
|
interfaces:
|
||||||
- name: Ethernet2
|
- name: Ethernet2
|
||||||
primary_ip: 172.30.11.0/31
|
primary_ip: 172.30.11.1/31
|
||||||
secondary_ips:
|
secondary_ips:
|
||||||
- 10.10.10.0/31
|
- 10.10.10.1/31
|
||||||
- 10.10.10.10/31
|
- 10.10.10.10/31
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfaceIPv4"
|
|
||||||
description = "Verifies the interface IPv4 addresses."
|
description = "Verifies the interface IPv4 addresses."
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip interface {interface}", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip interface {interface}", revision=2)]
|
||||||
|
@ -685,9 +651,9 @@ class VerifyInterfaceIPv4(AntaTest):
|
||||||
|
|
||||||
name: Interface
|
name: Interface
|
||||||
"""Name of the interface."""
|
"""Name of the interface."""
|
||||||
primary_ip: IPv4Network
|
primary_ip: IPv4Interface
|
||||||
"""Primary IPv4 address in CIDR notation."""
|
"""Primary IPv4 address in CIDR notation."""
|
||||||
secondary_ips: list[IPv4Network] | None = None
|
secondary_ips: list[IPv4Interface] | None = None
|
||||||
"""Optional list of secondary IPv4 addresses in CIDR notation."""
|
"""Optional list of secondary IPv4 addresses in CIDR notation."""
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
||||||
|
@ -765,8 +731,6 @@ class VerifyIpVirtualRouterMac(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIpVirtualRouterMac"
|
|
||||||
description = "Verifies the IP virtual router MAC address."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip virtual-router", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip virtual-router", revision=2)]
|
||||||
|
|
||||||
|
@ -818,8 +782,6 @@ class VerifyInterfacesSpeed(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyInterfacesSpeed"
|
|
||||||
description = "Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces")]
|
||||||
|
|
||||||
|
@ -886,17 +848,27 @@ class VerifyInterfacesSpeed(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyLACPInterfacesStatus(AntaTest):
|
class VerifyLACPInterfacesStatus(AntaTest):
|
||||||
"""Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.
|
"""Verifies the Link Aggregation Control Protocol (LACP) status of the interface.
|
||||||
|
|
||||||
- Verifies that the interface is a member of the LACP port channel.
|
This test performs the following checks for each specified interface:
|
||||||
- Ensures that the synchronization is established.
|
|
||||||
- Ensures the interfaces are in the correct state for collecting and distributing traffic.
|
1. Verifies that the interface is a member of the LACP port channel.
|
||||||
- Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as "slow" mode, is the default setting.)
|
2. Verifies LACP port states and operational status:
|
||||||
|
- Activity: Active LACP mode (initiates)
|
||||||
|
- Timeout: Short (Fast Mode), Long (Slow Mode - default)
|
||||||
|
- Aggregation: Port aggregable
|
||||||
|
- Synchronization: Port in sync with partner
|
||||||
|
- Collecting: Incoming frames aggregating
|
||||||
|
- Distributing: Outgoing frames aggregating
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.
|
* Success: Interface is bundled and all LACP states match expected values for both actor and partner
|
||||||
* Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.
|
* Failure: If any of the following occur:
|
||||||
|
- Interface or port channel is not configured.
|
||||||
|
- Interface is not bundled in port channel.
|
||||||
|
- Actor or partner port LACP states don't match expected configuration.
|
||||||
|
- LACP rate (timeout) mismatch when fast mode is configured.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -909,28 +881,15 @@ class VerifyLACPInterfacesStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLACPInterfacesStatus"
|
|
||||||
description = "Verifies the Link Aggregation Control Protocol(LACP) status of the provided interfaces."
|
|
||||||
categories: ClassVar[list[str]] = ["interfaces"]
|
categories: ClassVar[list[str]] = ["interfaces"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show lacp interface {interface}", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lacp interface", revision=1)]
|
||||||
|
|
||||||
class Input(AntaTest.Input):
|
class Input(AntaTest.Input):
|
||||||
"""Input model for the VerifyLACPInterfacesStatus test."""
|
"""Input model for the VerifyLACPInterfacesStatus test."""
|
||||||
|
|
||||||
interfaces: list[LACPInterface]
|
interfaces: list[InterfaceState]
|
||||||
"""List of LACP member interface."""
|
"""List of interfaces with their expected state."""
|
||||||
|
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
|
||||||
class LACPInterface(BaseModel):
|
|
||||||
"""Model for an LACP member interface."""
|
|
||||||
|
|
||||||
name: EthernetInterface
|
|
||||||
"""Ethernet interface to validate."""
|
|
||||||
portchannel: PortChannelInterface
|
|
||||||
"""Port Channel in which the interface is bundled."""
|
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
|
||||||
"""Render the template for each interface in the input list."""
|
|
||||||
return [template.render(interface=interface.name) for interface in self.inputs.interfaces]
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
|
@ -940,21 +899,17 @@ class VerifyLACPInterfacesStatus(AntaTest):
|
||||||
# Member port verification parameters.
|
# Member port verification parameters.
|
||||||
member_port_details = ["activity", "aggregation", "synchronization", "collecting", "distributing", "timeout"]
|
member_port_details = ["activity", "aggregation", "synchronization", "collecting", "distributing", "timeout"]
|
||||||
|
|
||||||
# Iterating over command output for different interfaces
|
command_output = self.instance_commands[0].json_output
|
||||||
for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):
|
for interface in self.inputs.interfaces:
|
||||||
interface = input_entry.name
|
|
||||||
portchannel = input_entry.portchannel
|
|
||||||
|
|
||||||
# Verify if a PortChannel is configured with the provided interface
|
# Verify if a PortChannel is configured with the provided interface
|
||||||
if not (interface_details := get_value(command.json_output, f"portChannels.{portchannel}.interfaces.{interface}")):
|
if not (interface_details := get_value(command_output, f"portChannels..{interface.portchannel}..interfaces..{interface.name}", separator="..")):
|
||||||
self.result.is_failure(f"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.")
|
self.result.is_failure(f"{interface} - Not configured")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Verify the interface is bundled in port channel.
|
# Verify the interface is bundled in port channel.
|
||||||
actor_port_status = interface_details.get("actorPortStatus")
|
actor_port_status = interface_details.get("actorPortStatus")
|
||||||
if actor_port_status != "bundled":
|
if actor_port_status != "bundled":
|
||||||
message = f"For Interface {interface}:\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\n"
|
self.result.is_failure(f"{interface} - Not bundled - Port Status: {actor_port_status}")
|
||||||
self.result.is_failure(message)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Collecting actor and partner port details
|
# Collecting actor and partner port details
|
||||||
|
@ -969,21 +924,12 @@ class VerifyLACPInterfacesStatus(AntaTest):
|
||||||
|
|
||||||
# Forming expected interface details
|
# Forming expected interface details
|
||||||
expected_details = {param: param != "timeout" for param in member_port_details}
|
expected_details = {param: param != "timeout" for param in member_port_details}
|
||||||
expected_interface_output = {"actor_port_details": expected_details, "partner_port_details": expected_details}
|
# Updating the short LACP timeout, if expected.
|
||||||
|
if interface.lacp_rate_fast:
|
||||||
|
expected_details["timeout"] = True
|
||||||
|
|
||||||
# Forming failure message
|
if (act_port_details := actual_interface_output["actor_port_details"]) != expected_details:
|
||||||
if actual_interface_output != expected_interface_output:
|
self.result.is_failure(f"{interface} - Actor port details mismatch - {format_data(act_port_details)}")
|
||||||
message = f"For Interface {interface}:\n"
|
|
||||||
actor_port_failed_log = get_failed_logs(
|
|
||||||
expected_interface_output.get("actor_port_details", {}), actual_interface_output.get("actor_port_details", {})
|
|
||||||
)
|
|
||||||
partner_port_failed_log = get_failed_logs(
|
|
||||||
expected_interface_output.get("partner_port_details", {}), actual_interface_output.get("partner_port_details", {})
|
|
||||||
)
|
|
||||||
|
|
||||||
if actor_port_failed_log:
|
if (part_port_details := actual_interface_output["partner_port_details"]) != expected_details:
|
||||||
message += f"Actor port details:{actor_port_failed_log}\n"
|
self.result.is_failure(f"{interface} - Partner port details mismatch - {format_data(part_port_details)}")
|
||||||
if partner_port_failed_log:
|
|
||||||
message += f"Partner port details:{partner_port_failed_log}\n"
|
|
||||||
|
|
||||||
self.result.is_failure(message)
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ class VerifyLANZ(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLANZ"
|
|
||||||
description = "Verifies if LANZ is enabled."
|
description = "Verifies if LANZ is enabled."
|
||||||
categories: ClassVar[list[str]] = ["lanz"]
|
categories: ClassVar[list[str]] = ["lanz"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show queue-monitor length status", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show queue-monitor length status", revision=1)]
|
||||||
|
|
|
@ -59,8 +59,6 @@ class VerifyLoggingPersistent(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingPersistent"
|
|
||||||
description = "Verifies if logging persistent is enabled and logs are saved in flash."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="show logging", ofmt="text"),
|
AntaCommand(command="show logging", ofmt="text"),
|
||||||
|
@ -100,8 +98,6 @@ class VerifyLoggingSourceIntf(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingSourceIntf"
|
|
||||||
description = "Verifies logging source-interface for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging", ofmt="text")]
|
||||||
|
|
||||||
|
@ -144,8 +140,6 @@ class VerifyLoggingHosts(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingHosts"
|
|
||||||
description = "Verifies logging hosts (syslog servers) for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging", ofmt="text")]
|
||||||
|
|
||||||
|
@ -176,10 +170,22 @@ class VerifyLoggingHosts(AntaTest):
|
||||||
class VerifyLoggingLogsGeneration(AntaTest):
|
class VerifyLoggingLogsGeneration(AntaTest):
|
||||||
"""Verifies if logs are generated.
|
"""Verifies if logs are generated.
|
||||||
|
|
||||||
|
This test performs the following checks:
|
||||||
|
|
||||||
|
1. Sends a test log message at the **informational** level
|
||||||
|
2. Retrieves the most recent logs (last 30 seconds)
|
||||||
|
3. Verifies that the test message was successfully logged
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if logs are generated.
|
* Success: If logs are being generated and the test message is found in recent logs.
|
||||||
* Failure: The test will fail if logs are NOT generated.
|
* Failure: If any of the following occur:
|
||||||
|
- The test message is not found in recent logs
|
||||||
|
- The logging system is not capturing new messages
|
||||||
|
- No logs are being generated
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -189,8 +195,6 @@ class VerifyLoggingLogsGeneration(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingLogsGeneration"
|
|
||||||
description = "Verifies if logs are generated."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="send log level informational message ANTA VerifyLoggingLogsGeneration validation", ofmt="text"),
|
AntaCommand(command="send log level informational message ANTA VerifyLoggingLogsGeneration validation", ofmt="text"),
|
||||||
|
@ -213,10 +217,23 @@ class VerifyLoggingLogsGeneration(AntaTest):
|
||||||
class VerifyLoggingHostname(AntaTest):
|
class VerifyLoggingHostname(AntaTest):
|
||||||
"""Verifies if logs are generated with the device FQDN.
|
"""Verifies if logs are generated with the device FQDN.
|
||||||
|
|
||||||
|
This test performs the following checks:
|
||||||
|
|
||||||
|
1. Retrieves the device's configured FQDN
|
||||||
|
2. Sends a test log message at the **informational** level
|
||||||
|
3. Retrieves the most recent logs (last 30 seconds)
|
||||||
|
4. Verifies that the test message includes the complete FQDN of the device
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if logs are generated with the device FQDN.
|
* Success: If logs are generated with the device's complete FQDN.
|
||||||
* Failure: The test will fail if logs are NOT generated with the device FQDN.
|
* Failure: If any of the following occur:
|
||||||
|
- The test message is not found in recent logs
|
||||||
|
- The log message does not include the device's FQDN
|
||||||
|
- The FQDN in the log message doesn't match the configured FQDN
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -226,8 +243,6 @@ class VerifyLoggingHostname(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingHostname"
|
|
||||||
description = "Verifies if logs are generated with the device FQDN."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="show hostname", revision=1),
|
AntaCommand(command="show hostname", revision=1),
|
||||||
|
@ -257,10 +272,24 @@ class VerifyLoggingHostname(AntaTest):
|
||||||
class VerifyLoggingTimestamp(AntaTest):
|
class VerifyLoggingTimestamp(AntaTest):
|
||||||
"""Verifies if logs are generated with the appropriate timestamp.
|
"""Verifies if logs are generated with the appropriate timestamp.
|
||||||
|
|
||||||
|
This test performs the following checks:
|
||||||
|
|
||||||
|
1. Sends a test log message at the **informational** level
|
||||||
|
2. Retrieves the most recent logs (last 30 seconds)
|
||||||
|
3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format
|
||||||
|
- Example format: `2024-01-25T15:30:45.123456+00:00`
|
||||||
|
- Includes microsecond precision
|
||||||
|
- Contains timezone offset
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if logs are generated with the appropriate timestamp.
|
* Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.
|
||||||
* Failure: The test will fail if logs are NOT generated with the appropriate timestamp.
|
* Failure: If any of the following occur:
|
||||||
|
- The test message is not found in recent logs
|
||||||
|
- The timestamp format does not match the expected RFC3339 format
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -270,8 +299,6 @@ class VerifyLoggingTimestamp(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingTimestamp"
|
|
||||||
description = "Verifies if logs are generated with the appropriate timestamp."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="send log level informational message ANTA VerifyLoggingTimestamp validation", ofmt="text"),
|
AntaCommand(command="send log level informational message ANTA VerifyLoggingTimestamp validation", ofmt="text"),
|
||||||
|
@ -312,8 +339,6 @@ class VerifyLoggingAccounting(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingAccounting"
|
|
||||||
description = "Verifies if AAA accounting logs are generated."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa accounting logs | tail", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa accounting logs | tail", ofmt="text")]
|
||||||
|
|
||||||
|
@ -344,8 +369,6 @@ class VerifyLoggingErrors(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyLoggingErrors"
|
|
||||||
description = "Verifies there are no syslog messages with a severity of ERRORS or higher."
|
|
||||||
categories: ClassVar[list[str]] = ["logging"]
|
categories: ClassVar[list[str]] = ["logging"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging threshold errors", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging threshold errors", ofmt="text")]
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,6 @@ class VerifyMlagStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagStatus"
|
|
||||||
description = "Verifies the health status of the MLAG configuration."
|
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
||||||
|
|
||||||
|
@ -78,8 +76,6 @@ class VerifyMlagInterfaces(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagInterfaces"
|
|
||||||
description = "Verifies there are no inactive or active-partial MLAG ports."
|
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
||||||
|
|
||||||
|
@ -114,8 +110,6 @@ class VerifyMlagConfigSanity(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagConfigSanity"
|
|
||||||
description = "Verifies there are no MLAG config-sanity inconsistencies."
|
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag config-sanity", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag config-sanity", revision=1)]
|
||||||
|
|
||||||
|
@ -153,8 +147,6 @@ class VerifyMlagReloadDelay(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagReloadDelay"
|
|
||||||
description = "Verifies the MLAG reload-delay parameters."
|
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag", revision=2)]
|
||||||
|
|
||||||
|
@ -203,7 +195,6 @@ class VerifyMlagDualPrimary(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagDualPrimary"
|
|
||||||
description = "Verifies the MLAG dual-primary detection parameters."
|
description = "Verifies the MLAG dual-primary detection parameters."
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag detail", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag detail", revision=2)]
|
||||||
|
@ -262,7 +253,6 @@ class VerifyMlagPrimaryPriority(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMlagPrimaryPriority"
|
|
||||||
description = "Verifies the configuration of the MLAG primary priority."
|
description = "Verifies the configuration of the MLAG primary priority."
|
||||||
categories: ClassVar[list[str]] = ["mlag"]
|
categories: ClassVar[list[str]] = ["mlag"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag detail", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag detail", revision=2)]
|
||||||
|
|
|
@ -35,8 +35,6 @@ class VerifyIGMPSnoopingVlans(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIGMPSnoopingVlans"
|
|
||||||
description = "Verifies the IGMP snooping status for the provided VLANs."
|
|
||||||
categories: ClassVar[list[str]] = ["multicast"]
|
categories: ClassVar[list[str]] = ["multicast"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip igmp snooping", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip igmp snooping", revision=1)]
|
||||||
|
|
||||||
|
@ -78,8 +76,6 @@ class VerifyIGMPSnoopingGlobal(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIGMPSnoopingGlobal"
|
|
||||||
description = "Verifies the IGMP snooping global configuration."
|
|
||||||
categories: ClassVar[list[str]] = ["multicast"]
|
categories: ClassVar[list[str]] = ["multicast"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip igmp snooping", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip igmp snooping", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,7 @@ from anta.tools import get_value
|
||||||
|
|
||||||
|
|
||||||
class VerifyPathsHealth(AntaTest):
|
class VerifyPathsHealth(AntaTest):
|
||||||
"""
|
"""Verifies the path and telemetry state of all paths under router path-selection.
|
||||||
Verifies the path and telemetry state of all paths under router path-selection.
|
|
||||||
|
|
||||||
The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.
|
The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.
|
||||||
|
|
||||||
|
@ -38,8 +37,6 @@ class VerifyPathsHealth(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPathsHealth"
|
|
||||||
description = "Verifies the path and telemetry state of all paths under router path-selection."
|
|
||||||
categories: ClassVar[list[str]] = ["path-selection"]
|
categories: ClassVar[list[str]] = ["path-selection"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show path-selection paths", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show path-selection paths", revision=1)]
|
||||||
|
|
||||||
|
@ -73,8 +70,7 @@ class VerifyPathsHealth(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifySpecificPath(AntaTest):
|
class VerifySpecificPath(AntaTest):
|
||||||
"""
|
"""Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.
|
||||||
Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.
|
|
||||||
|
|
||||||
The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.
|
The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.
|
||||||
|
|
||||||
|
@ -98,8 +94,6 @@ class VerifySpecificPath(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySpecificPath"
|
|
||||||
description = "Verifies the path and telemetry state of a specific path under router path-selection."
|
|
||||||
categories: ClassVar[list[str]] = ["path-selection"]
|
categories: ClassVar[list[str]] = ["path-selection"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaTemplate(template="show path-selection paths peer {peer} path-group {group} source {source} destination {destination}", revision=1)
|
AntaTemplate(template="show path-selection paths peer {peer} path-group {group} source {source} destination {destination}", revision=1)
|
||||||
|
|
|
@ -33,7 +33,6 @@ class VerifyUnifiedForwardingTableMode(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyUnifiedForwardingTableMode"
|
|
||||||
description = "Verifies the device is using the expected UFT mode."
|
description = "Verifies the device is using the expected UFT mode."
|
||||||
categories: ClassVar[list[str]] = ["profiles"]
|
categories: ClassVar[list[str]] = ["profiles"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show platform trident forwarding-table partition", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show platform trident forwarding-table partition", revision=1)]
|
||||||
|
@ -72,7 +71,6 @@ class VerifyTcamProfile(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTcamProfile"
|
|
||||||
description = "Verifies the device TCAM profile."
|
description = "Verifies the device TCAM profile."
|
||||||
categories: ClassVar[list[str]] = ["profiles"]
|
categories: ClassVar[list[str]] = ["profiles"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware tcam profile", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware tcam profile", revision=1)]
|
||||||
|
|
|
@ -33,7 +33,6 @@ class VerifyPtpModeStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPtpModeStatus"
|
|
||||||
description = "Verifies that the device is configured as a PTP Boundary Clock."
|
description = "Verifies that the device is configured as a PTP Boundary Clock."
|
||||||
categories: ClassVar[list[str]] = ["ptp"]
|
categories: ClassVar[list[str]] = ["ptp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
||||||
|
@ -80,7 +79,6 @@ class VerifyPtpGMStatus(AntaTest):
|
||||||
gmid: str
|
gmid: str
|
||||||
"""Identifier of the Grandmaster to which the device should be locked."""
|
"""Identifier of the Grandmaster to which the device should be locked."""
|
||||||
|
|
||||||
name = "VerifyPtpGMStatus"
|
|
||||||
description = "Verifies that the device is locked to a valid PTP Grandmaster."
|
description = "Verifies that the device is locked to a valid PTP Grandmaster."
|
||||||
categories: ClassVar[list[str]] = ["ptp"]
|
categories: ClassVar[list[str]] = ["ptp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
||||||
|
@ -120,7 +118,6 @@ class VerifyPtpLockStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPtpLockStatus"
|
|
||||||
description = "Verifies that the device was locked to the upstream PTP GM in the last minute."
|
description = "Verifies that the device was locked to the upstream PTP GM in the last minute."
|
||||||
categories: ClassVar[list[str]] = ["ptp"]
|
categories: ClassVar[list[str]] = ["ptp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
||||||
|
@ -161,7 +158,6 @@ class VerifyPtpOffset(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPtpOffset"
|
|
||||||
description = "Verifies that the PTP timing offset is within +/- 1000ns from the master clock."
|
description = "Verifies that the PTP timing offset is within +/- 1000ns from the master clock."
|
||||||
categories: ClassVar[list[str]] = ["ptp"]
|
categories: ClassVar[list[str]] = ["ptp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp monitor", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp monitor", revision=1)]
|
||||||
|
@ -206,7 +202,6 @@ class VerifyPtpPortModeStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyPtpPortModeStatus"
|
|
||||||
description = "Verifies the PTP interfaces state."
|
description = "Verifies the PTP interfaces state."
|
||||||
categories: ClassVar[list[str]] = ["ptp"]
|
categories: ClassVar[list[str]] = ["ptp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,9 @@ from typing import TYPE_CHECKING, ClassVar, Literal
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
|
|
||||||
from anta.custom_types import PositiveInteger
|
from anta.custom_types import PositiveInteger
|
||||||
|
from anta.input_models.routing.generic import IPv4Routes
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
|
from anta.tools import get_value
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import sys
|
import sys
|
||||||
|
@ -26,7 +28,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class VerifyRoutingProtocolModel(AntaTest):
|
class VerifyRoutingProtocolModel(AntaTest):
|
||||||
"""Verifies the configured routing protocol model is the one we expect.
|
"""Verifies the configured routing protocol model.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -43,8 +45,6 @@ class VerifyRoutingProtocolModel(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyRoutingProtocolModel"
|
|
||||||
description = "Verifies the configured routing protocol model."
|
|
||||||
categories: ClassVar[list[str]] = ["routing"]
|
categories: ClassVar[list[str]] = ["routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route summary", revision=3)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route summary", revision=3)]
|
||||||
|
|
||||||
|
@ -85,8 +85,6 @@ class VerifyRoutingTableSize(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyRoutingTableSize"
|
|
||||||
description = "Verifies the size of the IP routing table of the default VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["routing"]
|
categories: ClassVar[list[str]] = ["routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route summary", revision=3)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route summary", revision=3)]
|
||||||
|
|
||||||
|
@ -138,8 +136,6 @@ class VerifyRoutingTableEntry(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyRoutingTableEntry"
|
|
||||||
description = "Verifies that the provided routes are present in the routing table of a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["routing"]
|
categories: ClassVar[list[str]] = ["routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaTemplate(template="show ip route vrf {vrf} {route}", revision=4),
|
AntaTemplate(template="show ip route vrf {vrf} {route}", revision=4),
|
||||||
|
@ -187,3 +183,76 @@ class VerifyRoutingTableEntry(AntaTest):
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
else:
|
else:
|
||||||
self.result.is_failure(f"The following route(s) are missing from the routing table of VRF {self.inputs.vrf}: {missing_routes}")
|
self.result.is_failure(f"The following route(s) are missing from the routing table of VRF {self.inputs.vrf}: {missing_routes}")
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyIPv4RouteType(AntaTest):
|
||||||
|
"""Verifies the route-type of the IPv4 prefixes.
|
||||||
|
|
||||||
|
This test performs the following checks for each IPv4 route:
|
||||||
|
1. Verifies that the specified VRF is configured.
|
||||||
|
2. Verifies that the specified IPv4 route is exists in the configuration.
|
||||||
|
3. Verifies that the the specified IPv4 route is of the expected type.
|
||||||
|
|
||||||
|
Expected Results
|
||||||
|
----------------
|
||||||
|
* Success: If all of the following conditions are met:
|
||||||
|
- All the specified VRFs are configured.
|
||||||
|
- All the specified IPv4 routes are found.
|
||||||
|
- All the specified IPv4 routes are of the expected type.
|
||||||
|
* Failure: If any of the following occur:
|
||||||
|
- A specified VRF is not configured.
|
||||||
|
- A specified IPv4 route is not found.
|
||||||
|
- Any specified IPv4 route is not of the expected type.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.routing:
|
||||||
|
generic:
|
||||||
|
- VerifyIPv4RouteType:
|
||||||
|
routes_entries:
|
||||||
|
- prefix: 10.10.0.1/32
|
||||||
|
vrf: default
|
||||||
|
route_type: eBGP
|
||||||
|
- prefix: 10.100.0.12/31
|
||||||
|
vrf: default
|
||||||
|
route_type: connected
|
||||||
|
- prefix: 10.100.1.5/32
|
||||||
|
vrf: default
|
||||||
|
route_type: iBGP
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories: ClassVar[list[str]] = ["routing"]
|
||||||
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip route vrf all", revision=4)]
|
||||||
|
|
||||||
|
class Input(AntaTest.Input):
|
||||||
|
"""Input model for the VerifyIPv4RouteType test."""
|
||||||
|
|
||||||
|
routes_entries: list[IPv4Routes]
|
||||||
|
|
||||||
|
@AntaTest.anta_test
|
||||||
|
def test(self) -> None:
|
||||||
|
"""Main test function for VerifyIPv4RouteType."""
|
||||||
|
self.result.is_success()
|
||||||
|
output = self.instance_commands[0].json_output
|
||||||
|
|
||||||
|
# Iterating over the all routes entries mentioned in the inputs.
|
||||||
|
for entry in self.inputs.routes_entries:
|
||||||
|
prefix = str(entry.prefix)
|
||||||
|
vrf = entry.vrf
|
||||||
|
expected_route_type = entry.route_type
|
||||||
|
|
||||||
|
# Verifying that on device, expected VRF is configured.
|
||||||
|
if (routes_details := get_value(output, f"vrfs.{vrf}.routes")) is None:
|
||||||
|
self.result.is_failure(f"{entry} - VRF not configured")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Verifying that the expected IPv4 route is present or not on the device
|
||||||
|
if (route_data := routes_details.get(prefix)) is None:
|
||||||
|
self.result.is_failure(f"{entry} - Route not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Verifying that the specified IPv4 routes are of the expected type.
|
||||||
|
if expected_route_type != (actual_route_type := route_data.get("routeType")):
|
||||||
|
self.result.is_failure(f"{entry} - Incorrect route type - Expected: {expected_route_type} Actual: {actual_route_type}")
|
||||||
|
|
|
@ -158,8 +158,6 @@ class VerifyISISNeighborState(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISNeighborState"
|
|
||||||
description = "Verifies all IS-IS neighbors are in UP state."
|
|
||||||
categories: ClassVar[list[str]] = ["isis"]
|
categories: ClassVar[list[str]] = ["isis"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis neighbors", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis neighbors", revision=1)]
|
||||||
|
|
||||||
|
@ -204,8 +202,6 @@ class VerifyISISNeighborCount(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISNeighborCount"
|
|
||||||
description = "Verifies count of IS-IS interface per level"
|
|
||||||
categories: ClassVar[list[str]] = ["isis"]
|
categories: ClassVar[list[str]] = ["isis"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)]
|
||||||
|
|
||||||
|
@ -277,7 +273,6 @@ class VerifyISISInterfaceMode(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISInterfaceMode"
|
|
||||||
description = "Verifies interface mode for IS-IS"
|
description = "Verifies interface mode for IS-IS"
|
||||||
categories: ClassVar[list[str]] = ["isis"]
|
categories: ClassVar[list[str]] = ["isis"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)]
|
||||||
|
@ -333,9 +328,7 @@ class VerifyISISInterfaceMode(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
|
class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
|
||||||
"""Verifies ISIS Segment Routing Adjacency Segments.
|
"""Verify that all expected Adjacency segments are correctly visible for each interface.
|
||||||
|
|
||||||
Verify that all expected Adjacency segments are correctly visible for each interface.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -356,12 +349,9 @@ class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
|
||||||
- interface: Ethernet2
|
- interface: Ethernet2
|
||||||
address: 10.0.1.3
|
address: 10.0.1.3
|
||||||
sid_origin: dynamic
|
sid_origin: dynamic
|
||||||
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISSegmentRoutingAdjacencySegments"
|
|
||||||
description = "Verify expected Adjacency segments are correctly visible for each interface."
|
|
||||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing adjacency-segments", ofmt="json")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing adjacency-segments", ofmt="json")]
|
||||||
|
|
||||||
|
@ -446,8 +436,7 @@ class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyISISSegmentRoutingDataplane(AntaTest):
|
class VerifyISISSegmentRoutingDataplane(AntaTest):
|
||||||
"""
|
"""Verify dataplane of a list of ISIS-SR instances.
|
||||||
Verify dataplane of a list of ISIS-SR instances.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -468,8 +457,6 @@ class VerifyISISSegmentRoutingDataplane(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISSegmentRoutingDataplane"
|
|
||||||
description = "Verify dataplane of a list of ISIS-SR instances"
|
|
||||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing", ofmt="json")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing", ofmt="json")]
|
||||||
|
|
||||||
|
@ -530,8 +517,7 @@ class VerifyISISSegmentRoutingDataplane(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyISISSegmentRoutingTunnels(AntaTest):
|
class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
"""
|
"""Verify ISIS-SR tunnels computed by device.
|
||||||
Verify ISIS-SR tunnels computed by device.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -543,26 +529,24 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.routing:
|
anta.tests.routing:
|
||||||
isis:
|
isis:
|
||||||
- VerifyISISSegmentRoutingTunnels:
|
- VerifyISISSegmentRoutingTunnels:
|
||||||
entries:
|
entries:
|
||||||
# Check only endpoint
|
# Check only endpoint
|
||||||
- endpoint: 1.0.0.122/32
|
- endpoint: 1.0.0.122/32
|
||||||
# Check endpoint and via TI-LFA
|
# Check endpoint and via TI-LFA
|
||||||
- endpoint: 1.0.0.13/32
|
- endpoint: 1.0.0.13/32
|
||||||
vias:
|
vias:
|
||||||
- type: tunnel
|
- type: tunnel
|
||||||
tunnel_id: ti-lfa
|
tunnel_id: ti-lfa
|
||||||
# Check endpoint and via IP routers
|
# Check endpoint and via IP routers
|
||||||
- endpoint: 1.0.0.14/32
|
- endpoint: 1.0.0.14/32
|
||||||
vias:
|
vias:
|
||||||
- type: ip
|
- type: ip
|
||||||
nexthop: 1.1.1.1
|
nexthop: 1.1.1.1
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyISISSegmentRoutingTunnels"
|
|
||||||
description = "Verify ISIS-SR tunnels computed by device"
|
|
||||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing tunnel", ofmt="json")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing tunnel", ofmt="json")]
|
||||||
|
|
||||||
|
@ -638,8 +622,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
self.result.is_failure("\n".join(failure_message))
|
self.result.is_failure("\n".join(failure_message))
|
||||||
|
|
||||||
def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||||
"""
|
"""Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.
|
||||||
Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -666,8 +649,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||||
"""
|
"""Check if the tunnel nexthop matches the given input.
|
||||||
Check if the tunnel nexthop matches the given input.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -694,8 +676,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||||
"""
|
"""Check if the tunnel interface exists in the given EOS entry.
|
||||||
Check if the tunnel interface exists in the given EOS entry.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -722,8 +703,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||||
"""
|
"""Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.
|
||||||
Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -109,8 +109,6 @@ class VerifyOSPFNeighborState(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyOSPFNeighborState"
|
|
||||||
description = "Verifies all OSPF neighbors are in FULL state."
|
|
||||||
categories: ClassVar[list[str]] = ["ospf"]
|
categories: ClassVar[list[str]] = ["ospf"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf neighbor", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf neighbor", revision=1)]
|
||||||
|
|
||||||
|
@ -146,8 +144,6 @@ class VerifyOSPFNeighborCount(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyOSPFNeighborCount"
|
|
||||||
description = "Verifies the number of OSPF neighbors in FULL state is the one we expect."
|
|
||||||
categories: ClassVar[list[str]] = ["ospf"]
|
categories: ClassVar[list[str]] = ["ospf"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf neighbor", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf neighbor", revision=1)]
|
||||||
|
|
||||||
|
@ -190,7 +186,6 @@ class VerifyOSPFMaxLSA(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyOSPFMaxLSA"
|
|
||||||
description = "Verifies all OSPF instances did not cross the maximum LSA threshold."
|
description = "Verifies all OSPF instances did not cross the maximum LSA threshold."
|
||||||
categories: ClassVar[list[str]] = ["ospf"]
|
categories: ClassVar[list[str]] = ["ospf"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf", revision=1)]
|
||||||
|
|
|
@ -8,12 +8,12 @@ from __future__ import annotations
|
||||||
# Mypy does not understand AntaTest.Input typing
|
# Mypy does not understand AntaTest.Input typing
|
||||||
# mypy: disable-error-code=attr-defined
|
# mypy: disable-error-code=attr-defined
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from typing import TYPE_CHECKING, ClassVar, get_args
|
from typing import TYPE_CHECKING, ClassVar, get_args
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
from anta.custom_types import EcdsaKeySize, EncryptionAlgorithm, PositiveInteger, RsaKeySize
|
from anta.custom_types import EcdsaKeySize, EncryptionAlgorithm, PositiveInteger, RsaKeySize
|
||||||
|
from anta.input_models.security import IPSecPeer, IPSecPeers
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
from anta.tools import get_failed_logs, get_item, get_value
|
from anta.tools import get_failed_logs, get_item, get_value
|
||||||
|
|
||||||
|
@ -42,8 +42,6 @@ class VerifySSHStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySSHStatus"
|
|
||||||
description = "Verifies if the SSHD agent is disabled in the default VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh", ofmt="text")]
|
||||||
|
|
||||||
|
@ -83,7 +81,6 @@ class VerifySSHIPv4Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySSHIPv4Acl"
|
|
||||||
description = "Verifies if the SSHD agent has IPv4 ACL(s) configured."
|
description = "Verifies if the SSHD agent has IPv4 ACL(s) configured."
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh ip access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh ip access-list summary", revision=1)]
|
||||||
|
@ -132,7 +129,6 @@ class VerifySSHIPv6Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySSHIPv6Acl"
|
|
||||||
description = "Verifies if the SSHD agent has IPv6 ACL(s) configured."
|
description = "Verifies if the SSHD agent has IPv6 ACL(s) configured."
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh ipv6 access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management ssh ipv6 access-list summary", revision=1)]
|
||||||
|
@ -179,8 +175,6 @@ class VerifyTelnetStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTelnetStatus"
|
|
||||||
description = "Verifies if Telnet is disabled in the default VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management telnet", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management telnet", revision=1)]
|
||||||
|
|
||||||
|
@ -210,8 +204,6 @@ class VerifyAPIHttpStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAPIHttpStatus"
|
|
||||||
description = "Verifies if eAPI HTTP server is disabled globally."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands", revision=1)]
|
||||||
|
|
||||||
|
@ -242,7 +234,6 @@ class VerifyAPIHttpsSSL(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAPIHttpsSSL"
|
|
||||||
description = "Verifies if the eAPI has a valid SSL profile."
|
description = "Verifies if the eAPI has a valid SSL profile."
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands", revision=1)]
|
||||||
|
@ -285,8 +276,6 @@ class VerifyAPIIPv4Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAPIIPv4Acl"
|
|
||||||
description = "Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands ip access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands ip access-list summary", revision=1)]
|
||||||
|
|
||||||
|
@ -335,8 +324,6 @@ class VerifyAPIIPv6Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAPIIPv6Acl"
|
|
||||||
description = "Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands ipv6 access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management api http-commands ipv6 access-list summary", revision=1)]
|
||||||
|
|
||||||
|
@ -395,8 +382,6 @@ class VerifyAPISSLCertificate(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAPISSLCertificate"
|
|
||||||
description = "Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="show management security ssl certificate", revision=1),
|
AntaCommand(command="show management security ssl certificate", revision=1),
|
||||||
|
@ -498,15 +483,13 @@ class VerifyBannerLogin(AntaTest):
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.security:
|
anta.tests.security:
|
||||||
- VerifyBannerLogin:
|
- VerifyBannerLogin:
|
||||||
login_banner: |
|
login_banner: |
|
||||||
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
# Use of this source code is governed by the Apache License 2.0
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
# that can be found in the LICENSE file.
|
# that can be found in the LICENSE file.
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBannerLogin"
|
|
||||||
description = "Verifies the login banner of a device."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show banner login", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show banner login", revision=1)]
|
||||||
|
|
||||||
|
@ -542,15 +525,13 @@ class VerifyBannerMotd(AntaTest):
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.security:
|
anta.tests.security:
|
||||||
- VerifyBannerMotd:
|
- VerifyBannerMotd:
|
||||||
motd_banner: |
|
motd_banner: |
|
||||||
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
# Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
# Use of this source code is governed by the Apache License 2.0
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
# that can be found in the LICENSE file.
|
# that can be found in the LICENSE file.
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyBannerMotd"
|
|
||||||
description = "Verifies the motd banner of a device."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show banner motd", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show banner motd", revision=1)]
|
||||||
|
|
||||||
|
@ -604,8 +585,6 @@ class VerifyIPv4ACL(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIPv4ACL"
|
|
||||||
description = "Verifies the configuration of IPv4 ACLs."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip access-lists {acl}", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip access-lists {acl}", revision=1)]
|
||||||
|
|
||||||
|
@ -669,8 +648,7 @@ class VerifyIPv4ACL(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyIPSecConnHealth(AntaTest):
|
class VerifyIPSecConnHealth(AntaTest):
|
||||||
"""
|
"""Verifies all IPv4 security connections.
|
||||||
Verifies all IPv4 security connections.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -685,8 +663,6 @@ class VerifyIPSecConnHealth(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyIPSecConnHealth"
|
|
||||||
description = "Verifies all IPv4 security connections."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip security connection vrf all")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip security connection vrf all")]
|
||||||
|
|
||||||
|
@ -716,16 +692,22 @@ class VerifyIPSecConnHealth(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifySpecificIPSecConn(AntaTest):
|
class VerifySpecificIPSecConn(AntaTest):
|
||||||
"""
|
"""Verifies the IPv4 security connections.
|
||||||
Verifies the state of IPv4 security connections for a specified peer.
|
|
||||||
|
|
||||||
It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.
|
This test performs the following checks for each peer:
|
||||||
If these addresses are not provided, it will verify all paths for the specified peer.
|
|
||||||
|
1. Validates that the VRF is configured.
|
||||||
|
2. Checks for the presence of IPv4 security connections for the specified peer.
|
||||||
|
3. For each relevant peer:
|
||||||
|
- If source and destination addresses are provided, verifies the security connection for the specific path exists and is `Established`.
|
||||||
|
- If no addresses are provided, verifies that all security connections associated with the peer are `Established`.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.
|
* Success: If all checks pass for all specified IPv4 security connections.
|
||||||
* Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.
|
* Failure: If any of the following occur:
|
||||||
|
- No IPv4 security connections are found for the peer
|
||||||
|
- The security connection is not established for the specified path or any of the peer connections is not established when no path is specified.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -744,36 +726,16 @@ class VerifySpecificIPSecConn(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySpecificIPSecConn"
|
|
||||||
description = "Verifies IPv4 security connections for a peer."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip security connection vrf {vrf} path peer {peer}")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip security connection vrf {vrf} path peer {peer}", revision=2)]
|
||||||
|
|
||||||
class Input(AntaTest.Input):
|
class Input(AntaTest.Input):
|
||||||
"""Input model for the VerifySpecificIPSecConn test."""
|
"""Input model for the VerifySpecificIPSecConn test."""
|
||||||
|
|
||||||
ip_security_connections: list[IPSecPeers]
|
ip_security_connections: list[IPSecPeer]
|
||||||
"""List of IP4v security peers."""
|
"""List of IP4v security peers."""
|
||||||
|
IPSecPeers: ClassVar[type[IPSecPeers]] = IPSecPeers
|
||||||
class IPSecPeers(BaseModel):
|
"""To maintain backward compatibility."""
|
||||||
"""Details of IPv4 security peers."""
|
|
||||||
|
|
||||||
peer: IPv4Address
|
|
||||||
"""IPv4 address of the peer."""
|
|
||||||
|
|
||||||
vrf: str = "default"
|
|
||||||
"""Optional VRF for the IP security peer."""
|
|
||||||
|
|
||||||
connections: list[IPSecConn] | None = None
|
|
||||||
"""Optional list of IPv4 security connections of a peer."""
|
|
||||||
|
|
||||||
class IPSecConn(BaseModel):
|
|
||||||
"""Details of IPv4 security connections for a peer."""
|
|
||||||
|
|
||||||
source_address: IPv4Address
|
|
||||||
"""Source IPv4 address of the connection."""
|
|
||||||
destination_address: IPv4Address
|
|
||||||
"""Destination IPv4 address of the connection."""
|
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
||||||
"""Render the template for each input IP Sec connection."""
|
"""Render the template for each input IP Sec connection."""
|
||||||
|
@ -783,15 +745,15 @@ class VerifySpecificIPSecConn(AntaTest):
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifySpecificIPSecConn."""
|
"""Main test function for VerifySpecificIPSecConn."""
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
|
|
||||||
for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):
|
for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):
|
||||||
conn_output = command_output.json_output["connections"]
|
conn_output = command_output.json_output["connections"]
|
||||||
peer = command_output.params.peer
|
|
||||||
vrf = command_output.params.vrf
|
|
||||||
conn_input = input_peer.connections
|
conn_input = input_peer.connections
|
||||||
|
vrf = input_peer.vrf
|
||||||
|
|
||||||
# Check if IPv4 security connection is configured
|
# Check if IPv4 security connection is configured
|
||||||
if not conn_output:
|
if not conn_output:
|
||||||
self.result.is_failure(f"No IPv4 security connection configured for peer `{peer}`.")
|
self.result.is_failure(f"{input_peer} - Not configured")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If connection details are not provided then check all connections of a peer
|
# If connection details are not provided then check all connections of a peer
|
||||||
|
@ -801,10 +763,8 @@ class VerifySpecificIPSecConn(AntaTest):
|
||||||
if state != "Established":
|
if state != "Established":
|
||||||
source = conn_data.get("saddr")
|
source = conn_data.get("saddr")
|
||||||
destination = conn_data.get("daddr")
|
destination = conn_data.get("daddr")
|
||||||
vrf = conn_data.get("tunnelNs")
|
|
||||||
self.result.is_failure(
|
self.result.is_failure(
|
||||||
f"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` "
|
f"{input_peer} Source: {source} Destination: {destination} - Connection down - Expected: Established, Actual: {state}"
|
||||||
f"but found `{state}` instead."
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -820,19 +780,14 @@ class VerifySpecificIPSecConn(AntaTest):
|
||||||
if (source_input, destination_input, vrf) in existing_connections:
|
if (source_input, destination_input, vrf) in existing_connections:
|
||||||
existing_state = existing_connections[(source_input, destination_input, vrf)]
|
existing_state = existing_connections[(source_input, destination_input, vrf)]
|
||||||
if existing_state != "Established":
|
if existing_state != "Established":
|
||||||
self.result.is_failure(
|
failure = f"Expected: Established, Actual: {existing_state}"
|
||||||
f"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` "
|
self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection down - {failure}")
|
||||||
f"for peer `{peer}` is `Established` but found `{existing_state}` instead."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.result.is_failure(
|
self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection not found.")
|
||||||
f"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyHardwareEntropy(AntaTest):
|
class VerifyHardwareEntropy(AntaTest):
|
||||||
"""
|
"""Verifies hardware entropy generation is enabled on device.
|
||||||
Verifies hardware entropy generation is enabled on device.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -847,8 +802,6 @@ class VerifyHardwareEntropy(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyHardwareEntropy"
|
|
||||||
description = "Verifies hardware entropy generation is enabled on device."
|
|
||||||
categories: ClassVar[list[str]] = ["security"]
|
categories: ClassVar[list[str]] = ["security"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management security")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management security")]
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
# Mypy does not understand AntaTest.Input typing
|
# Mypy does not understand AntaTest.Input typing
|
||||||
# mypy: disable-error-code=attr-defined
|
# mypy: disable-error-code=attr-defined
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from anta.custom_types import ErrDisableInterval, ErrDisableReasons
|
from anta.custom_types import ErrDisableInterval, ErrDisableReasons
|
||||||
|
from anta.input_models.services import DnsServer
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
from anta.tools import get_dict_superset, get_failed_logs, get_item
|
from anta.tools import get_dict_superset, get_failed_logs
|
||||||
|
|
||||||
|
|
||||||
class VerifyHostname(AntaTest):
|
class VerifyHostname(AntaTest):
|
||||||
|
@ -34,8 +34,6 @@ class VerifyHostname(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyHostname"
|
|
||||||
description = "Verifies the hostname of a device."
|
|
||||||
categories: ClassVar[list[str]] = ["services"]
|
categories: ClassVar[list[str]] = ["services"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hostname", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hostname", revision=1)]
|
||||||
|
|
||||||
|
@ -77,7 +75,6 @@ class VerifyDNSLookup(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyDNSLookup"
|
|
||||||
description = "Verifies the DNS name to IP address resolution."
|
description = "Verifies the DNS name to IP address resolution."
|
||||||
categories: ClassVar[list[str]] = ["services"]
|
categories: ClassVar[list[str]] = ["services"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="bash timeout 10 nslookup {domain}", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="bash timeout 10 nslookup {domain}", revision=1)]
|
||||||
|
@ -109,10 +106,17 @@ class VerifyDNSLookup(AntaTest):
|
||||||
class VerifyDNSServers(AntaTest):
|
class VerifyDNSServers(AntaTest):
|
||||||
"""Verifies if the DNS (Domain Name Service) servers are correctly configured.
|
"""Verifies if the DNS (Domain Name Service) servers are correctly configured.
|
||||||
|
|
||||||
|
This test performs the following checks for each specified DNS Server:
|
||||||
|
|
||||||
|
1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.
|
||||||
|
2. Ensuring an appropriate priority level.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.
|
* Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.
|
||||||
* Failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.
|
* Failure: The test will fail if any of the following conditions are met:
|
||||||
|
- The provided DNS server is not configured.
|
||||||
|
- The provided DNS server with designated VRF and priority does not match the expected information.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
@ -129,8 +133,6 @@ class VerifyDNSServers(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyDNSServers"
|
|
||||||
description = "Verifies if the DNS servers are correctly configured."
|
|
||||||
categories: ClassVar[list[str]] = ["services"]
|
categories: ClassVar[list[str]] = ["services"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip name-server", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip name-server", revision=1)]
|
||||||
|
|
||||||
|
@ -139,38 +141,28 @@ class VerifyDNSServers(AntaTest):
|
||||||
|
|
||||||
dns_servers: list[DnsServer]
|
dns_servers: list[DnsServer]
|
||||||
"""List of DNS servers to verify."""
|
"""List of DNS servers to verify."""
|
||||||
|
DnsServer: ClassVar[type[DnsServer]] = DnsServer
|
||||||
class DnsServer(BaseModel):
|
|
||||||
"""Model for a DNS server."""
|
|
||||||
|
|
||||||
server_address: IPv4Address | IPv6Address
|
|
||||||
"""The IPv4/IPv6 address of the DNS server."""
|
|
||||||
vrf: str = "default"
|
|
||||||
"""The VRF for the DNS server. Defaults to 'default' if not provided."""
|
|
||||||
priority: int = Field(ge=0, le=4)
|
|
||||||
"""The priority of the DNS server from 0 to 4, lower is first."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyDNSServers."""
|
"""Main test function for VerifyDNSServers."""
|
||||||
command_output = self.instance_commands[0].json_output["nameServerConfigs"]
|
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
|
|
||||||
|
command_output = self.instance_commands[0].json_output["nameServerConfigs"]
|
||||||
for server in self.inputs.dns_servers:
|
for server in self.inputs.dns_servers:
|
||||||
address = str(server.server_address)
|
address = str(server.server_address)
|
||||||
vrf = server.vrf
|
vrf = server.vrf
|
||||||
priority = server.priority
|
priority = server.priority
|
||||||
input_dict = {"ipAddr": address, "vrf": vrf}
|
input_dict = {"ipAddr": address, "vrf": vrf}
|
||||||
|
|
||||||
if get_item(command_output, "ipAddr", address) is None:
|
# Check if the DNS server is configured with specified VRF.
|
||||||
self.result.is_failure(f"DNS server `{address}` is not configured with any VRF.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (output := get_dict_superset(command_output, input_dict)) is None:
|
if (output := get_dict_superset(command_output, input_dict)) is None:
|
||||||
self.result.is_failure(f"DNS server `{address}` is not configured with VRF `{vrf}`.")
|
self.result.is_failure(f"{server} - Not configured")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check if the DNS server priority matches with expected.
|
||||||
if output["priority"] != priority:
|
if output["priority"] != priority:
|
||||||
self.result.is_failure(f"For DNS server `{address}`, the expected priority is `{priority}`, but `{output['priority']}` was found instead.")
|
self.result.is_failure(f"{server} - Incorrect priority - Priority: {output['priority']}")
|
||||||
|
|
||||||
|
|
||||||
class VerifyErrdisableRecovery(AntaTest):
|
class VerifyErrdisableRecovery(AntaTest):
|
||||||
|
@ -194,8 +186,6 @@ class VerifyErrdisableRecovery(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyErrdisableRecovery"
|
|
||||||
description = "Verifies the errdisable recovery reason, status, and interval."
|
|
||||||
categories: ClassVar[list[str]] = ["services"]
|
categories: ClassVar[list[str]] = ["services"]
|
||||||
# NOTE: Only `text` output format is supported for this command
|
# NOTE: Only `text` output format is supported for this command
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show errdisable recovery", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show errdisable recovery", ofmt="text")]
|
||||||
|
|
|
@ -34,7 +34,6 @@ class VerifySnmpStatus(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpStatus"
|
|
||||||
description = "Verifies if the SNMP agent is enabled."
|
description = "Verifies if the SNMP agent is enabled."
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
||||||
|
@ -73,7 +72,6 @@ class VerifySnmpIPv4Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpIPv4Acl"
|
|
||||||
description = "Verifies if the SNMP agent has IPv4 ACL(s) configured."
|
description = "Verifies if the SNMP agent has IPv4 ACL(s) configured."
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp ipv4 access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp ipv4 access-list summary", revision=1)]
|
||||||
|
@ -122,7 +120,6 @@ class VerifySnmpIPv6Acl(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpIPv6Acl"
|
|
||||||
description = "Verifies if the SNMP agent has IPv6 ACL(s) configured."
|
description = "Verifies if the SNMP agent has IPv6 ACL(s) configured."
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp ipv6 access-list summary", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp ipv6 access-list summary", revision=1)]
|
||||||
|
@ -170,8 +167,6 @@ class VerifySnmpLocation(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpLocation"
|
|
||||||
description = "Verifies the SNMP location of a device."
|
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
||||||
|
|
||||||
|
@ -213,8 +208,6 @@ class VerifySnmpContact(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpContact"
|
|
||||||
description = "Verifies the SNMP contact of a device."
|
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
||||||
|
|
||||||
|
@ -261,8 +254,6 @@ class VerifySnmpPDUCounters(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpPDUCounters"
|
|
||||||
description = "Verifies the SNMP PDU counters."
|
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
||||||
|
|
||||||
|
@ -317,8 +308,6 @@ class VerifySnmpErrorCounters(AntaTest):
|
||||||
- inBadCommunityNames
|
- inBadCommunityNames
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySnmpErrorCounters"
|
|
||||||
description = "Verifies the SNMP error counters."
|
|
||||||
categories: ClassVar[list[str]] = ["snmp"]
|
categories: ClassVar[list[str]] = ["snmp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ class VerifyEOSVersion(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyEOSVersion"
|
|
||||||
description = "Verifies the EOS version of the device."
|
description = "Verifies the EOS version of the device."
|
||||||
categories: ClassVar[list[str]] = ["software"]
|
categories: ClassVar[list[str]] = ["software"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", revision=1)]
|
||||||
|
@ -74,7 +73,6 @@ class VerifyTerminAttrVersion(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTerminAttrVersion"
|
|
||||||
description = "Verifies the TerminAttr version of the device."
|
description = "Verifies the TerminAttr version of the device."
|
||||||
categories: ClassVar[list[str]] = ["software"]
|
categories: ClassVar[list[str]] = ["software"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
|
||||||
|
@ -112,8 +110,6 @@ class VerifyEOSExtensions(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyEOSExtensions"
|
|
||||||
description = "Verifies that all EOS extensions installed on the device are enabled for boot persistence."
|
|
||||||
categories: ClassVar[list[str]] = ["software"]
|
categories: ClassVar[list[str]] = ["software"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
|
||||||
AntaCommand(command="show extensions", revision=2),
|
AntaCommand(command="show extensions", revision=2),
|
||||||
|
|
|
@ -36,8 +36,6 @@ class VerifySTPMode(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySTPMode"
|
|
||||||
description = "Verifies the configured STP mode for a provided list of VLAN(s)."
|
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show spanning-tree vlan {vlan}", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show spanning-tree vlan {vlan}", revision=1)]
|
||||||
|
|
||||||
|
@ -93,8 +91,6 @@ class VerifySTPBlockedPorts(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySTPBlockedPorts"
|
|
||||||
description = "Verifies there is no STP blocked ports."
|
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree blockedports", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree blockedports", revision=1)]
|
||||||
|
|
||||||
|
@ -126,8 +122,6 @@ class VerifySTPCounters(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySTPCounters"
|
|
||||||
description = "Verifies there is no errors in STP BPDU packets."
|
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree counters", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree counters", revision=1)]
|
||||||
|
|
||||||
|
@ -163,7 +157,6 @@ class VerifySTPForwardingPorts(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySTPForwardingPorts"
|
|
||||||
description = "Verifies that all interfaces are forwarding for a provided list of VLAN(s)."
|
description = "Verifies that all interfaces are forwarding for a provided list of VLAN(s)."
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show spanning-tree topology vlan {vlan} status", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show spanning-tree topology vlan {vlan} status", revision=1)]
|
||||||
|
@ -222,8 +215,6 @@ class VerifySTPRootPriority(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifySTPRootPriority"
|
|
||||||
description = "Verifies the STP root priority for a provided list of VLAN or MST instance ID(s)."
|
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree root detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree root detail", revision=1)]
|
||||||
|
|
||||||
|
@ -279,8 +270,6 @@ class VerifyStpTopologyChanges(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyStpTopologyChanges"
|
|
||||||
description = "Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold."
|
|
||||||
categories: ClassVar[list[str]] = ["stp"]
|
categories: ClassVar[list[str]] = ["stp"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree topology status detail", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree topology status detail", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -7,32 +7,36 @@
|
||||||
# mypy: disable-error-code=attr-defined
|
# mypy: disable-error-code=attr-defined
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from anta.decorators import deprecated_test_class
|
||||||
|
from anta.input_models.stun import StunClientTranslation
|
||||||
from anta.custom_types import Port
|
|
||||||
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
from anta.models import AntaCommand, AntaTemplate, AntaTest
|
||||||
from anta.tools import get_failed_logs, get_value
|
from anta.tools import get_value
|
||||||
|
|
||||||
|
|
||||||
class VerifyStunClient(AntaTest):
|
class VerifyStunClientTranslation(AntaTest):
|
||||||
"""
|
"""Verifies the translation for a source address on a STUN client.
|
||||||
Verifies the configuration of the STUN client, specifically the IPv4 source address and port.
|
|
||||||
|
|
||||||
Optionally, it can also verify the public address and port.
|
This test performs the following checks for each specified address family:
|
||||||
|
|
||||||
|
1. Validates that there is a translation for the source address on the STUN client.
|
||||||
|
2. If public IP and port details are provided, validates their correctness against the configuration.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
* Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.
|
* Success: If all of the following conditions are met:
|
||||||
* Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.
|
- The test will pass if the source address translation is present.
|
||||||
|
- If public IP and port details are provided, they must also match the translation information.
|
||||||
|
* Failure: If any of the following occur:
|
||||||
|
- There is no translation for the source address on the STUN client.
|
||||||
|
- The public IP or port details, if specified, are incorrect.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.stun:
|
anta.tests.stun:
|
||||||
- VerifyStunClient:
|
- VerifyStunClientTranslation:
|
||||||
stun_clients:
|
stun_clients:
|
||||||
- source_address: 172.18.3.2
|
- source_address: 172.18.3.2
|
||||||
public_address: 172.18.3.21
|
public_address: 172.18.3.21
|
||||||
|
@ -45,27 +49,15 @@ class VerifyStunClient(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyStunClient"
|
|
||||||
description = "Verifies the STUN client is configured with the specified IPv4 source address and port. Validate the public IP and port if provided."
|
|
||||||
categories: ClassVar[list[str]] = ["stun"]
|
categories: ClassVar[list[str]] = ["stun"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}", revision=1)]
|
||||||
|
|
||||||
class Input(AntaTest.Input):
|
class Input(AntaTest.Input):
|
||||||
"""Input model for the VerifyStunClient test."""
|
"""Input model for the VerifyStunClientTranslation test."""
|
||||||
|
|
||||||
stun_clients: list[ClientAddress]
|
stun_clients: list[StunClientTranslation]
|
||||||
|
"""List of STUN clients."""
|
||||||
class ClientAddress(BaseModel):
|
StunClientTranslation: ClassVar[type[StunClientTranslation]] = StunClientTranslation
|
||||||
"""Source and public address/port details of STUN client."""
|
|
||||||
|
|
||||||
source_address: IPv4Address
|
|
||||||
"""IPv4 source address of STUN client."""
|
|
||||||
source_port: Port = 4500
|
|
||||||
"""Source port number for STUN client."""
|
|
||||||
public_address: IPv4Address | None = None
|
|
||||||
"""Optional IPv4 public address of STUN client."""
|
|
||||||
public_port: Port | None = None
|
|
||||||
"""Optional public port number for STUN client."""
|
|
||||||
|
|
||||||
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
def render(self, template: AntaTemplate) -> list[AntaCommand]:
|
||||||
"""Render the template for each STUN translation."""
|
"""Render the template for each STUN translation."""
|
||||||
|
@ -73,53 +65,61 @@ class VerifyStunClient(AntaTest):
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyStunClient."""
|
"""Main test function for VerifyStunClientTranslation."""
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
|
|
||||||
# Iterate over each command output and corresponding client input
|
# Iterate over each command output and corresponding client input
|
||||||
for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):
|
for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):
|
||||||
bindings = command.json_output["bindings"]
|
bindings = command.json_output["bindings"]
|
||||||
source_address = str(command.params.source_address)
|
input_public_address = client_input.public_address
|
||||||
source_port = command.params.source_port
|
input_public_port = client_input.public_port
|
||||||
|
|
||||||
# If no bindings are found for the STUN client, mark the test as a failure and continue with the next client
|
# If no bindings are found for the STUN client, mark the test as a failure and continue with the next client
|
||||||
if not bindings:
|
if not bindings:
|
||||||
self.result.is_failure(f"STUN client transaction for source `{source_address}:{source_port}` is not found.")
|
self.result.is_failure(f"{client_input} - STUN client translation not found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract the public address and port from the client input
|
|
||||||
public_address = client_input.public_address
|
|
||||||
public_port = client_input.public_port
|
|
||||||
|
|
||||||
# Extract the transaction ID from the bindings
|
# Extract the transaction ID from the bindings
|
||||||
transaction_id = next(iter(bindings.keys()))
|
transaction_id = next(iter(bindings.keys()))
|
||||||
|
|
||||||
# Prepare the actual and expected STUN data for comparison
|
# Verifying the public address if provided
|
||||||
actual_stun_data = {
|
if input_public_address and str(input_public_address) != (actual_public_address := get_value(bindings, f"{transaction_id}.publicAddress.ip")):
|
||||||
"source ip": get_value(bindings, f"{transaction_id}.sourceAddress.ip"),
|
self.result.is_failure(f"{client_input} - Incorrect public-facing address - Expected: {input_public_address} Actual: {actual_public_address}")
|
||||||
"source port": get_value(bindings, f"{transaction_id}.sourceAddress.port"),
|
|
||||||
}
|
|
||||||
expected_stun_data = {"source ip": source_address, "source port": source_port}
|
|
||||||
|
|
||||||
# If public address is provided, add it to the actual and expected STUN data
|
# Verifying the public port if provided
|
||||||
if public_address is not None:
|
if input_public_port and input_public_port != (actual_public_port := get_value(bindings, f"{transaction_id}.publicAddress.port")):
|
||||||
actual_stun_data["public ip"] = get_value(bindings, f"{transaction_id}.publicAddress.ip")
|
self.result.is_failure(f"{client_input} - Incorrect public-facing port - Expected: {input_public_port} Actual: {actual_public_port}")
|
||||||
expected_stun_data["public ip"] = str(public_address)
|
|
||||||
|
|
||||||
# If public port is provided, add it to the actual and expected STUN data
|
|
||||||
if public_port is not None:
|
|
||||||
actual_stun_data["public port"] = get_value(bindings, f"{transaction_id}.publicAddress.port")
|
|
||||||
expected_stun_data["public port"] = public_port
|
|
||||||
|
|
||||||
# If the actual STUN data does not match the expected STUN data, mark the test as failure
|
@deprecated_test_class(new_tests=["VerifyStunClientTranslation"], removal_in_version="v2.0.0")
|
||||||
if actual_stun_data != expected_stun_data:
|
class VerifyStunClient(VerifyStunClientTranslation):
|
||||||
failed_log = get_failed_logs(expected_stun_data, actual_stun_data)
|
"""(Deprecated) Verifies the translation for a source address on a STUN client.
|
||||||
self.result.is_failure(f"For STUN source `{source_address}:{source_port}`:{failed_log}")
|
|
||||||
|
Alias for the VerifyStunClientTranslation test to maintain backward compatibility.
|
||||||
|
When initialized, it will emit a deprecation warning and call the VerifyStunClientTranslation test.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```yaml
|
||||||
|
anta.tests.stun:
|
||||||
|
- VerifyStunClient:
|
||||||
|
stun_clients:
|
||||||
|
- source_address: 172.18.3.2
|
||||||
|
public_address: 172.18.3.21
|
||||||
|
source_port: 4500
|
||||||
|
public_port: 6006
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Remove this class in ANTA v2.0.0.
|
||||||
|
|
||||||
|
# required to redefine name an description to overwrite parent class.
|
||||||
|
name = "VerifyStunClient"
|
||||||
|
description = "(Deprecated) Verifies the translation for a source address on a STUN client."
|
||||||
|
|
||||||
|
|
||||||
class VerifyStunServer(AntaTest):
|
class VerifyStunServer(AntaTest):
|
||||||
"""
|
"""Verifies the STUN server status is enabled and running.
|
||||||
Verifies the STUN server status is enabled and running.
|
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -134,8 +134,6 @@ class VerifyStunServer(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyStunServer"
|
|
||||||
description = "Verifies the STUN server status is enabled and running."
|
|
||||||
categories: ClassVar[list[str]] = ["stun"]
|
categories: ClassVar[list[str]] = ["stun"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show stun server status", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show stun server status", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from ipaddress import IPv4Address
|
|
||||||
from typing import TYPE_CHECKING, ClassVar
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from anta.custom_types import PositiveInteger
|
||||||
|
from anta.input_models.system import NTPServer
|
||||||
from anta.custom_types import Hostname, PositiveInteger
|
|
||||||
from anta.models import AntaCommand, AntaTest
|
from anta.models import AntaCommand, AntaTest
|
||||||
from anta.tools import get_failed_logs, get_value
|
from anta.tools import get_value
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from anta.models import AntaTemplate
|
from anta.models import AntaTemplate
|
||||||
|
@ -42,7 +40,6 @@ class VerifyUptime(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyUptime"
|
|
||||||
description = "Verifies the device uptime."
|
description = "Verifies the device uptime."
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show uptime", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show uptime", revision=1)]
|
||||||
|
@ -80,8 +77,6 @@ class VerifyReloadCause(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyReloadCause"
|
|
||||||
description = "Verifies the last reload cause of the device."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show reload cause", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show reload cause", revision=1)]
|
||||||
|
|
||||||
|
@ -112,19 +107,18 @@ class VerifyCoredump(AntaTest):
|
||||||
* Success: The test will pass if there are NO core dump(s) in /var/core.
|
* Success: The test will pass if there are NO core dump(s) in /var/core.
|
||||||
* Failure: The test will fail if there are core dump(s) in /var/core.
|
* Failure: The test will fail if there are core dump(s) in /var/core.
|
||||||
|
|
||||||
Info
|
Notes
|
||||||
----
|
-----
|
||||||
* This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.
|
* This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
```yaml
|
```yaml
|
||||||
anta.tests.system:
|
anta.tests.system:
|
||||||
- VerifyCoreDump:
|
- VerifyCoredump:
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyCoredump"
|
|
||||||
description = "Verifies there are no core dump files."
|
description = "Verifies there are no core dump files."
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system coredump", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system coredump", revision=1)]
|
||||||
|
@ -143,7 +137,7 @@ class VerifyCoredump(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyAgentLogs(AntaTest):
|
class VerifyAgentLogs(AntaTest):
|
||||||
"""Verifies that no agent crash reports are present on the device.
|
"""Verifies there are no agent crash reports.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -158,8 +152,6 @@ class VerifyAgentLogs(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyAgentLogs"
|
|
||||||
description = "Verifies there are no agent crash reports."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show agent logs crash", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show agent logs crash", ofmt="text")]
|
||||||
|
|
||||||
|
@ -191,8 +183,6 @@ class VerifyCPUUtilization(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyCPUUtilization"
|
|
||||||
description = "Verifies whether the CPU utilization is below 75%."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show processes top once", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show processes top once", revision=1)]
|
||||||
|
|
||||||
|
@ -223,8 +213,6 @@ class VerifyMemoryUtilization(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyMemoryUtilization"
|
|
||||||
description = "Verifies whether the memory utilization is below 75%."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version", revision=1)]
|
||||||
|
|
||||||
|
@ -255,8 +243,6 @@ class VerifyFileSystemUtilization(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyFileSystemUtilization"
|
|
||||||
description = "Verifies that no partition is utilizing more than 75% of its disk space."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="bash timeout 10 df -h", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="bash timeout 10 df -h", ofmt="text")]
|
||||||
|
|
||||||
|
@ -286,7 +272,6 @@ class VerifyNTP(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyNTP"
|
|
||||||
description = "Verifies if NTP is synchronised."
|
description = "Verifies if NTP is synchronised."
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ntp status", ofmt="text")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ntp status", ofmt="text")]
|
||||||
|
@ -328,8 +313,6 @@ class VerifyNTPAssociations(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyNTPAssociations"
|
|
||||||
description = "Verifies the Network Time Protocol (NTP) associations."
|
|
||||||
categories: ClassVar[list[str]] = ["system"]
|
categories: ClassVar[list[str]] = ["system"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ntp associations")]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ntp associations")]
|
||||||
|
|
||||||
|
@ -338,55 +321,33 @@ class VerifyNTPAssociations(AntaTest):
|
||||||
|
|
||||||
ntp_servers: list[NTPServer]
|
ntp_servers: list[NTPServer]
|
||||||
"""List of NTP servers."""
|
"""List of NTP servers."""
|
||||||
|
NTPServer: ClassVar[type[NTPServer]] = NTPServer
|
||||||
class NTPServer(BaseModel):
|
|
||||||
"""Model for a NTP server."""
|
|
||||||
|
|
||||||
server_address: Hostname | IPv4Address
|
|
||||||
"""The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration
|
|
||||||
of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.
|
|
||||||
For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output."""
|
|
||||||
preferred: bool = False
|
|
||||||
"""Optional preferred for NTP server. If not provided, it defaults to `False`."""
|
|
||||||
stratum: int = Field(ge=0, le=16)
|
|
||||||
"""NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.
|
|
||||||
Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state."""
|
|
||||||
|
|
||||||
@AntaTest.anta_test
|
@AntaTest.anta_test
|
||||||
def test(self) -> None:
|
def test(self) -> None:
|
||||||
"""Main test function for VerifyNTPAssociations."""
|
"""Main test function for VerifyNTPAssociations."""
|
||||||
failures: str = ""
|
self.result.is_success()
|
||||||
|
|
||||||
if not (peer_details := get_value(self.instance_commands[0].json_output, "peers")):
|
if not (peers := get_value(self.instance_commands[0].json_output, "peers")):
|
||||||
self.result.is_failure("None of NTP peers are not configured.")
|
self.result.is_failure("No NTP peers configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Iterate over each NTP server.
|
# Iterate over each NTP server.
|
||||||
for ntp_server in self.inputs.ntp_servers:
|
for ntp_server in self.inputs.ntp_servers:
|
||||||
server_address = str(ntp_server.server_address)
|
server_address = str(ntp_server.server_address)
|
||||||
preferred = ntp_server.preferred
|
|
||||||
stratum = ntp_server.stratum
|
|
||||||
|
|
||||||
# Check if NTP server details exists.
|
# We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.
|
||||||
if (peer_detail := get_value(peer_details, server_address, separator="..")) is None:
|
matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details["peerIpAddr"], peer})), None)
|
||||||
failures += f"NTP peer {server_address} is not configured.\n"
|
|
||||||
|
if not matching_peer:
|
||||||
|
self.result.is_failure(f"{ntp_server} - Not configured")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Collecting the expected NTP peer details.
|
# Collecting the expected/actual NTP peer details.
|
||||||
expected_peer_details = {"condition": "candidate", "stratum": stratum}
|
exp_condition = "sys.peer" if ntp_server.preferred else "candidate"
|
||||||
if preferred:
|
exp_stratum = ntp_server.stratum
|
||||||
expected_peer_details["condition"] = "sys.peer"
|
act_condition = get_value(peers[matching_peer], "condition")
|
||||||
|
act_stratum = get_value(peers[matching_peer], "stratumLevel")
|
||||||
|
|
||||||
# Collecting the actual NTP peer details.
|
if act_condition != exp_condition or act_stratum != exp_stratum:
|
||||||
actual_peer_details = {"condition": get_value(peer_detail, "condition"), "stratum": get_value(peer_detail, "stratumLevel")}
|
self.result.is_failure(f"{ntp_server} - Bad association - Condition: {act_condition}, Stratum: {act_stratum}")
|
||||||
|
|
||||||
# Collecting failures logs if any.
|
|
||||||
failure_logs = get_failed_logs(expected_peer_details, actual_peer_details)
|
|
||||||
if failure_logs:
|
|
||||||
failures += f"For NTP peer {server_address}:{failure_logs}\n"
|
|
||||||
|
|
||||||
# Check if there are any failures.
|
|
||||||
if not failures:
|
|
||||||
self.result.is_success()
|
|
||||||
else:
|
|
||||||
self.result.is_failure(failures)
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ class VerifyVlanInternalPolicy(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVlanInternalPolicy"
|
|
||||||
description = "Verifies the VLAN internal allocation policy and the range of VLANs."
|
description = "Verifies the VLAN internal allocation policy and the range of VLANs."
|
||||||
categories: ClassVar[list[str]] = ["vlan"]
|
categories: ClassVar[list[str]] = ["vlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan internal allocation policy", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan internal allocation policy", revision=1)]
|
||||||
|
|
|
@ -23,8 +23,8 @@ if TYPE_CHECKING:
|
||||||
class VerifyVxlan1Interface(AntaTest):
|
class VerifyVxlan1Interface(AntaTest):
|
||||||
"""Verifies if the Vxlan1 interface is configured and 'up/up'.
|
"""Verifies if the Vxlan1 interface is configured and 'up/up'.
|
||||||
|
|
||||||
Warning
|
Warnings
|
||||||
-------
|
--------
|
||||||
The name of this test has been updated from 'VerifyVxlan' for better representation.
|
The name of this test has been updated from 'VerifyVxlan' for better representation.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
|
@ -41,7 +41,6 @@ class VerifyVxlan1Interface(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVxlan1Interface"
|
|
||||||
description = "Verifies the Vxlan1 interface status."
|
description = "Verifies the Vxlan1 interface status."
|
||||||
categories: ClassVar[list[str]] = ["vxlan"]
|
categories: ClassVar[list[str]] = ["vxlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
|
||||||
|
@ -65,7 +64,7 @@ class VerifyVxlan1Interface(AntaTest):
|
||||||
|
|
||||||
|
|
||||||
class VerifyVxlanConfigSanity(AntaTest):
|
class VerifyVxlanConfigSanity(AntaTest):
|
||||||
"""Verifies that no issues are detected with the VXLAN configuration.
|
"""Verifies there are no VXLAN config-sanity inconsistencies.
|
||||||
|
|
||||||
Expected Results
|
Expected Results
|
||||||
----------------
|
----------------
|
||||||
|
@ -81,8 +80,6 @@ class VerifyVxlanConfigSanity(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVxlanConfigSanity"
|
|
||||||
description = "Verifies there are no VXLAN config-sanity inconsistencies."
|
|
||||||
categories: ClassVar[list[str]] = ["vxlan"]
|
categories: ClassVar[list[str]] = ["vxlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan config-sanity", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan config-sanity", revision=1)]
|
||||||
|
|
||||||
|
@ -124,8 +121,6 @@ class VerifyVxlanVniBinding(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVxlanVniBinding"
|
|
||||||
description = "Verifies the VNI-VLAN bindings of the Vxlan1 interface."
|
|
||||||
categories: ClassVar[list[str]] = ["vxlan"]
|
categories: ClassVar[list[str]] = ["vxlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan vni", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan vni", revision=1)]
|
||||||
|
|
||||||
|
@ -187,8 +182,6 @@ class VerifyVxlanVtep(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVxlanVtep"
|
|
||||||
description = "Verifies the VTEP peers of the Vxlan1 interface"
|
|
||||||
categories: ClassVar[list[str]] = ["vxlan"]
|
categories: ClassVar[list[str]] = ["vxlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan vtep", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vxlan vtep", revision=1)]
|
||||||
|
|
||||||
|
@ -238,8 +231,6 @@ class VerifyVxlan1ConnSettings(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyVxlan1ConnSettings"
|
|
||||||
description = "Verifies the interface vxlan1 source interface and UDP port."
|
|
||||||
categories: ClassVar[list[str]] = ["vxlan"]
|
categories: ClassVar[list[str]] = ["vxlan"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
|
||||||
|
|
||||||
|
|
|
@ -94,8 +94,7 @@ def get_dict_superset(
|
||||||
*,
|
*,
|
||||||
required: bool = False,
|
required: bool = False,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""Get the first dictionary from a list of dictionaries that is a superset of the input dict.
|
||||||
Get the first dictionary from a list of dictionaries that is a superset of the input dict.
|
|
||||||
|
|
||||||
Returns the supplied default value or None if there is no match and "required" is False.
|
Returns the supplied default value or None if there is no match and "required" is False.
|
||||||
|
|
||||||
|
@ -378,7 +377,7 @@ def safe_command(command: str) -> str:
|
||||||
def convert_categories(categories: list[str]) -> list[str]:
|
def convert_categories(categories: list[str]) -> list[str]:
|
||||||
"""Convert categories for reports.
|
"""Convert categories for reports.
|
||||||
|
|
||||||
if the category is part of the defined acronym, transform it to upper case
|
If the category is part of the defined acronym, transform it to upper case
|
||||||
otherwise capitalize the first letter.
|
otherwise capitalize the first letter.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -395,3 +394,24 @@ def convert_categories(categories: list[str]) -> list[str]:
|
||||||
return [" ".join(word.upper() if word.lower() in ACRONYM_CATEGORIES else word.title() for word in category.split()) for category in categories]
|
return [" ".join(word.upper() if word.lower() in ACRONYM_CATEGORIES else word.title() for word in category.split()) for category in categories]
|
||||||
msg = f"Wrong input type '{type(categories)}' for convert_categories."
|
msg = f"Wrong input type '{type(categories)}' for convert_categories."
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def format_data(data: dict[str, bool]) -> str:
|
||||||
|
"""Format a data dictionary for logging purposes.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data
|
||||||
|
A dictionary containing the data to format.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The formatted data.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
>>> format_data({"advertised": True, "received": True, "enabled": True})
|
||||||
|
"Advertised: True, Received: True, Enabled: True"
|
||||||
|
"""
|
||||||
|
return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
|
||||||
|
|
|
@ -9,4 +9,4 @@ from .config_session import SessionConfig
|
||||||
from .device import Device
|
from .device import Device
|
||||||
from .errors import EapiCommandError
|
from .errors import EapiCommandError
|
||||||
|
|
||||||
__all__ = ["Device", "SessionConfig", "EapiCommandError"]
|
__all__ = ["Device", "EapiCommandError", "SessionConfig"]
|
||||||
|
|
|
@ -34,8 +34,7 @@ __all__ = ["port_check_url"]
|
||||||
|
|
||||||
|
|
||||||
async def port_check_url(url: URL, timeout: int = 5) -> bool:
|
async def port_check_url(url: URL, timeout: int = 5) -> bool:
|
||||||
"""
|
"""Open the port designated by the URL given the timeout in seconds.
|
||||||
Open the port designated by the URL given the timeout in seconds.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -29,8 +29,7 @@ __all__ = ["SessionConfig"]
|
||||||
|
|
||||||
|
|
||||||
class SessionConfig:
|
class SessionConfig:
|
||||||
"""
|
"""Send configuration to a device using the EOS session mechanism.
|
||||||
Send configuration to a device using the EOS session mechanism.
|
|
||||||
|
|
||||||
This is the preferred way of managing configuration changes.
|
This is the preferred way of managing configuration changes.
|
||||||
|
|
||||||
|
@ -44,8 +43,7 @@ class SessionConfig:
|
||||||
CLI_CFG_FACTORY_RESET = "rollback clean-config"
|
CLI_CFG_FACTORY_RESET = "rollback clean-config"
|
||||||
|
|
||||||
def __init__(self, device: Device, name: str) -> None:
|
def __init__(self, device: Device, name: str) -> None:
|
||||||
"""
|
"""Create a new instance of SessionConfig.
|
||||||
Create a new instance of SessionConfig.
|
|
||||||
|
|
||||||
The session config instance bound
|
The session config instance bound
|
||||||
to the given device instance, and using the session `name`.
|
to the given device instance, and using the session `name`.
|
||||||
|
@ -81,8 +79,7 @@ class SessionConfig:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
async def status_all(self) -> dict[str, Any]:
|
async def status_all(self) -> dict[str, Any]:
|
||||||
"""
|
"""Get the status of all the session config on the device.
|
||||||
Get the status of all the session config on the device.
|
|
||||||
|
|
||||||
Run the following command on the device:
|
Run the following command on the device:
|
||||||
# show configuration sessions detail
|
# show configuration sessions detail
|
||||||
|
@ -122,8 +119,7 @@ class SessionConfig:
|
||||||
return await self._cli("show configuration sessions detail") # type: ignore[return-value] # json outformat returns dict[str, Any]
|
return await self._cli("show configuration sessions detail") # type: ignore[return-value] # json outformat returns dict[str, Any]
|
||||||
|
|
||||||
async def status(self) -> dict[str, Any] | None:
|
async def status(self) -> dict[str, Any] | None:
|
||||||
"""
|
"""Get the status of a session config on the device.
|
||||||
Get the status of a session config on the device.
|
|
||||||
|
|
||||||
Run the following command on the device:
|
Run the following command on the device:
|
||||||
# show configuration sessions detail
|
# show configuration sessions detail
|
||||||
|
@ -179,8 +175,7 @@ class SessionConfig:
|
||||||
return res["sessions"].get(self.name)
|
return res["sessions"].get(self.name)
|
||||||
|
|
||||||
async def push(self, content: list[str] | str, *, replace: bool = False) -> None:
|
async def push(self, content: list[str] | str, *, replace: bool = False) -> None:
|
||||||
"""
|
"""Send the configuration content to the device.
|
||||||
Send the configuration content to the device.
|
|
||||||
|
|
||||||
If `replace` is true, then the command "rollback clean-config" is issued
|
If `replace` is true, then the command "rollback clean-config" is issued
|
||||||
before sending the configuration content.
|
before sending the configuration content.
|
||||||
|
@ -218,8 +213,7 @@ class SessionConfig:
|
||||||
await self._cli(commands=commands)
|
await self._cli(commands=commands)
|
||||||
|
|
||||||
async def commit(self, timer: str | None = None) -> None:
|
async def commit(self, timer: str | None = None) -> None:
|
||||||
"""
|
"""Commit the session config.
|
||||||
Commit the session config.
|
|
||||||
|
|
||||||
Run the following command on the device:
|
Run the following command on the device:
|
||||||
# configure session <name>
|
# configure session <name>
|
||||||
|
@ -241,8 +235,7 @@ class SessionConfig:
|
||||||
await self._cli(command)
|
await self._cli(command)
|
||||||
|
|
||||||
async def abort(self) -> None:
|
async def abort(self) -> None:
|
||||||
"""
|
"""Abort the configuration session.
|
||||||
Abort the configuration session.
|
|
||||||
|
|
||||||
Run the following command on the device:
|
Run the following command on the device:
|
||||||
# configure session <name> abort
|
# configure session <name> abort
|
||||||
|
@ -250,8 +243,7 @@ class SessionConfig:
|
||||||
await self._cli(f"{self._cli_config_session} abort")
|
await self._cli(f"{self._cli_config_session} abort")
|
||||||
|
|
||||||
async def diff(self) -> str:
|
async def diff(self) -> str:
|
||||||
"""
|
"""Return the "diff" of the session config relative to the running config.
|
||||||
Return the "diff" of the session config relative to the running config.
|
|
||||||
|
|
||||||
Run the following command on the device:
|
Run the following command on the device:
|
||||||
# show session-config named <name> diffs
|
# show session-config named <name> diffs
|
||||||
|
@ -268,8 +260,7 @@ class SessionConfig:
|
||||||
return await self._cli(f"show session-config named {self.name} diffs", ofmt="text") # type: ignore[return-value] # text outformat returns str
|
return await self._cli(f"show session-config named {self.name} diffs", ofmt="text") # type: ignore[return-value] # text outformat returns str
|
||||||
|
|
||||||
async def load_file(self, filename: str, *, replace: bool = False) -> None:
|
async def load_file(self, filename: str, *, replace: bool = False) -> None:
|
||||||
"""
|
"""Load the configuration from <filename> into the session configuration.
|
||||||
Load the configuration from <filename> into the session configuration.
|
|
||||||
|
|
||||||
If the replace parameter is True then the file contents will replace the existing session config (load-replace).
|
If the replace parameter is True then the file contents will replace the existing session config (load-replace).
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,7 @@ __all__ = ["Device"]
|
||||||
|
|
||||||
|
|
||||||
class Device(httpx.AsyncClient):
|
class Device(httpx.AsyncClient):
|
||||||
"""
|
"""Represent the async JSON-RPC client that communicates with an Arista EOS device.
|
||||||
Represent the async JSON-RPC client that communicates with an Arista EOS device.
|
|
||||||
|
|
||||||
This class inherits directly from the
|
This class inherits directly from the
|
||||||
httpx.AsyncClient, so any initialization options can be passed directly.
|
httpx.AsyncClient, so any initialization options can be passed directly.
|
||||||
|
@ -63,8 +62,7 @@ class Device(httpx.AsyncClient):
|
||||||
port: str | int | None = None,
|
port: str | int | None = None,
|
||||||
**kwargs: Any, # noqa: ANN401
|
**kwargs: Any, # noqa: ANN401
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Initialize the Device class.
|
||||||
Initialize the Device class.
|
|
||||||
|
|
||||||
As a subclass to httpx.AsyncClient, the caller can provide any of those initializers.
|
As a subclass to httpx.AsyncClient, the caller can provide any of those initializers.
|
||||||
Specific parameters for Device class are all optional and described below.
|
Specific parameters for Device class are all optional and described below.
|
||||||
|
@ -111,8 +109,7 @@ class Device(httpx.AsyncClient):
|
||||||
self.headers["Content-Type"] = "application/json-rpc"
|
self.headers["Content-Type"] = "application/json-rpc"
|
||||||
|
|
||||||
async def check_connection(self) -> bool:
|
async def check_connection(self) -> bool:
|
||||||
"""
|
"""Check the target device to ensure that the eAPI port is open and accepting connections.
|
||||||
Check the target device to ensure that the eAPI port is open and accepting connections.
|
|
||||||
|
|
||||||
It is recommended that a Caller checks the connection before involving cli commands,
|
It is recommended that a Caller checks the connection before involving cli commands,
|
||||||
but this step is not required.
|
but this step is not required.
|
||||||
|
@ -124,7 +121,7 @@ class Device(httpx.AsyncClient):
|
||||||
"""
|
"""
|
||||||
return await port_check_url(self.base_url)
|
return await port_check_url(self.base_url)
|
||||||
|
|
||||||
async def cli( # noqa: PLR0913
|
async def cli(
|
||||||
self,
|
self,
|
||||||
command: str | dict[str, Any] | None = None,
|
command: str | dict[str, Any] | None = None,
|
||||||
commands: Sequence[str | dict[str, Any]] | None = None,
|
commands: Sequence[str | dict[str, Any]] | None = None,
|
||||||
|
@ -136,8 +133,7 @@ class Device(httpx.AsyncClient):
|
||||||
expand_aliases: bool = False,
|
expand_aliases: bool = False,
|
||||||
req_id: int | str | None = None,
|
req_id: int | str | None = None,
|
||||||
) -> list[dict[str, Any] | str] | dict[str, Any] | str | None:
|
) -> list[dict[str, Any] | str] | dict[str, Any] | str | None:
|
||||||
"""
|
"""Execute one or more CLI commands.
|
||||||
Execute one or more CLI commands.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -199,7 +195,7 @@ class Device(httpx.AsyncClient):
|
||||||
return None
|
return None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _jsonrpc_command( # noqa: PLR0913
|
def _jsonrpc_command(
|
||||||
self,
|
self,
|
||||||
commands: Sequence[str | dict[str, Any]] | None = None,
|
commands: Sequence[str | dict[str, Any]] | None = None,
|
||||||
ofmt: str | None = None,
|
ofmt: str | None = None,
|
||||||
|
@ -264,8 +260,7 @@ class Device(httpx.AsyncClient):
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
async def jsonrpc_exec(self, jsonrpc: dict[str, Any]) -> list[dict[str, Any] | str]:
|
async def jsonrpc_exec(self, jsonrpc: dict[str, Any]) -> list[dict[str, Any] | str]:
|
||||||
"""
|
"""Execute the JSON-RPC dictionary object.
|
||||||
Execute the JSON-RPC dictionary object.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -12,8 +12,7 @@ import httpx
|
||||||
|
|
||||||
|
|
||||||
class EapiCommandError(RuntimeError):
|
class EapiCommandError(RuntimeError):
|
||||||
"""
|
"""Exception class for EAPI command errors.
|
||||||
Exception class for EAPI command errors.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
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.
|
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
|
> [!TIP]
|
||||||
If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html
|
> If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html
|
||||||
|
|
||||||
## [AntaDevice](../api/device.md#anta.device.AntaDevice) Abstract Class
|
## [AntaDevice](../api/device.md#anta.device.AntaDevice) Abstract Class
|
||||||
|
|
||||||
|
@ -47,8 +47,10 @@ The [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) class is a
|
||||||
--8<-- "parse_anta_inventory_file.py"
|
--8<-- "parse_anta_inventory_file.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note "How to create your inventory file"
|
> [!NOTE]
|
||||||
Please visit this [dedicated section](../usage-inventory-catalog.md) for how to use inventory and catalog files.
|
> **How to create your inventory file**
|
||||||
|
>
|
||||||
|
> Please visit this [dedicated section](../usage-inventory-catalog.md) for how to use inventory and catalog files.
|
||||||
|
|
||||||
### Run EOS commands
|
### Run EOS commands
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
!!! info
|
> [!INFO]
|
||||||
This documentation applies for both creating tests in ANTA or creating your own test package.
|
> This documentation applies for both creating tests in ANTA or creating your own test package.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ A test is a Python class where a test function is defined and will be run by the
|
||||||
|
|
||||||
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:
|
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
|
````python
|
||||||
from anta.models import AntaTest, AntaCommand
|
from anta.models import AntaTest, AntaCommand
|
||||||
from anta.decorators import skip_on_platforms
|
from anta.decorators import skip_on_platforms
|
||||||
|
|
||||||
|
@ -36,8 +36,6 @@ class VerifyTemperature(AntaTest):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "VerifyTemperature"
|
|
||||||
description = "Verifies the device temperature."
|
|
||||||
categories: ClassVar[list[str]] = ["hardware"]
|
categories: ClassVar[list[str]] = ["hardware"]
|
||||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)]
|
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)]
|
||||||
|
|
||||||
|
@ -51,7 +49,7 @@ class VerifyTemperature(AntaTest):
|
||||||
self.result.is_success()
|
self.result.is_success()
|
||||||
else:
|
else:
|
||||||
self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'")
|
self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'")
|
||||||
```
|
````
|
||||||
|
|
||||||
[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.
|
[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.
|
||||||
|
|
||||||
|
@ -61,13 +59,13 @@ Full AntaTest API documentation is available in the [API documentation section](
|
||||||
|
|
||||||
### Class Attributes
|
### Class Attributes
|
||||||
|
|
||||||
- `name` (`str`): Name of the test. Used during reporting.
|
- `name` (`str`, `optional`): Name of the test. Used during reporting. By default set to the Class name.
|
||||||
- `description` (`str`): A human readable description of your test.
|
- `description` (`str`, `optional`): A human readable description of your test. By default set to the first line of the docstring.
|
||||||
- `categories` (`list[str]`): A list of categories in which the test belongs.
|
- `categories` (`list[str]`): A list of categories in which the test belongs.
|
||||||
- `commands` (`[list[AntaCommand | AntaTemplate]]`): 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.
|
- `commands` (`[list[AntaCommand | AntaTemplate]]`): 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.
|
||||||
|
|
||||||
!!! info
|
> [!INFO]
|
||||||
All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation.
|
> All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation.
|
||||||
|
|
||||||
### Instance Attributes
|
### Instance Attributes
|
||||||
|
|
||||||
|
@ -84,11 +82,15 @@ Full AntaTest API documentation is available in the [API documentation section](
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
heading_level: 10
|
heading_level: 10
|
||||||
|
|
||||||
!!! note "Logger object"
|
> [!NOTE]
|
||||||
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.
|
>
|
||||||
|
> - **Logger object**
|
||||||
!!! note "AntaDevice object"
|
>
|
||||||
Even if `device` is not a private attribute, you should not need to access this object in your code.
|
> 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.
|
||||||
|
>
|
||||||
|
> - **AntaDevice object**
|
||||||
|
>
|
||||||
|
> Even if `device` is not a private attribute, you should not need to access this object in your code.
|
||||||
|
|
||||||
### Test Inputs
|
### Test Inputs
|
||||||
|
|
||||||
|
@ -131,8 +133,8 @@ Full `ResultOverwrite` model documentation is available in [API documentation se
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
heading_level: 10
|
heading_level: 10
|
||||||
|
|
||||||
!!! note
|
> [!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.
|
> 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.
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
|
@ -162,8 +164,8 @@ In this section, we will go into all the details of writing an [AntaTest](../api
|
||||||
Import [anta.models.AntaTest](../api/models.md#anta.models.AntaTest) and define your own class.
|
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.
|
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.
|
||||||
|
|
||||||
!!! info
|
> [!NOTE]
|
||||||
Caching can be disabled per `AntaCommand` or `AntaTemplate` by setting the `use_cache` argument to `False`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md).
|
> Caching can be disabled per `AntaCommand` or `AntaTemplate` by setting the `use_cache` argument to `False`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from anta.models import AntaTest, AntaCommand, AntaTemplate
|
from anta.models import AntaTest, AntaCommand, AntaTemplate
|
||||||
|
@ -171,11 +173,11 @@ from anta.models import AntaTest, AntaCommand, AntaTemplate
|
||||||
|
|
||||||
class <YourTestName>(AntaTest):
|
class <YourTestName>(AntaTest):
|
||||||
"""
|
"""
|
||||||
<a docstring description of your test>
|
<a docstring description of your test, the first line is used as description of the test by default>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "YourTestName" # should be your class name
|
# name = <override> # uncomment to override default behavior of name=Class Name
|
||||||
description = "<test description in human reading format>"
|
# description = <override> # uncomment to override default behavior of description=first line of docstring
|
||||||
categories = ["<arbitrary category>", "<another arbitrary category>"]
|
categories = ["<arbitrary category>", "<another arbitrary category>"]
|
||||||
commands = [
|
commands = [
|
||||||
AntaCommand(
|
AntaCommand(
|
||||||
|
@ -195,21 +197,23 @@ class <YourTestName>(AntaTest):
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip "Command revision and version"
|
> [!TIP]
|
||||||
* Most of EOS commands return a JSON structure according to a model (some commands may not be modeled hence the necessity to use `text` outformat sometimes.
|
> **Command revision and version**
|
||||||
* The model can change across time (adding feature, ... ) and when the model is changed in a non backward-compatible way, the **revision** number is bumped. The initial model starts with **revision** 1.
|
>
|
||||||
* A **revision** applies to a particular CLI command whereas a **version** is global to an eAPI call. The **version** is internally translated to a specific **revision** for each CLI command in the RPC call. The currently supported **version** values are `1` and `latest`.
|
> - Most of EOS commands return a JSON structure according to a model (some commands may not be modeled hence the necessity to use `text` outformat sometimes.
|
||||||
* 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)
|
> - The model can change across time (adding feature, ... ) and when the model is changed in a non backward-compatible way, the **revision** number is bumped. The initial model starts with **revision** 1.
|
||||||
* By default, eAPI returns the first revision of each model to ensure that when upgrading, integrations with existing tools are not broken. This is done by using by default `version=1` in eAPI calls.
|
> - A **revision** applies to a particular CLI command whereas a **version** is global to an eAPI call. The **version** is internally translated to a specific **revision** for each CLI command in the RPC call. The currently supported **version** values are `1` and `latest`.
|
||||||
|
> - 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, ANTA uses `version="latest"` in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is `json`. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version.
|
> - By default, eAPI returns the first revision of each model to ensure that when upgrading, integrations with existing tools are not broken. This is done by using by default `version=1` in eAPI calls.
|
||||||
|
>
|
||||||
For instance, the `VerifyBFDPeersHealth` test leverages the first revision of `show bfd peers`:
|
> By default, ANTA uses `version="latest"` in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is `json`. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version.
|
||||||
|
>
|
||||||
```
|
> For instance, the `VerifyBFDPeersHealth` test leverages the first revision of `show bfd peers`:
|
||||||
# revision 1 as later revision introduce additional nesting for type
|
>
|
||||||
commands = [AntaCommand(command="show bfd peers", revision=1)]
|
> ```python
|
||||||
```
|
> # revision 1 as later revision introduce additional nesting for type
|
||||||
|
> commands = [AntaCommand(command="show bfd peers", revision=1)]
|
||||||
|
> ```
|
||||||
|
|
||||||
### Inputs definition
|
### Inputs definition
|
||||||
|
|
||||||
|
@ -244,8 +248,8 @@ You can also leverage [anta.custom_types](../api/types.md) that provides reusabl
|
||||||
|
|
||||||
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.
|
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
|
> [!NOTE]
|
||||||
All the `pydantic` features are supported. For instance you can define [validators](https://docs.pydantic.dev/latest/usage/validators/) for complex input validation.
|
> 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
|
### Template rendering
|
||||||
|
|
||||||
|
@ -340,10 +344,10 @@ class VerifyTemperature(AntaTest):
|
||||||
|
|
||||||
## Access your custom tests in the test catalog
|
## Access your custom tests in the test catalog
|
||||||
|
|
||||||
!!! warning ""
|
> [!WARNING]
|
||||||
This section is required only if you are not merging your development into ANTA. Otherwise, just follow [contribution guide](../contribution.md).
|
> This section is required only if you are not merging your development into ANTA. Otherwise, just follow [contribution guide](../contribution.md).
|
||||||
|
|
||||||
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 the module search path `sys.path` (you can use `PYTHONPATH` for example).
|
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 importable by ANTA hence available in the module search path `sys.path` (you can use `PYTHONPATH` for example).
|
||||||
|
|
||||||
It is very similar to what is documented in [catalog section](../usage-inventory-catalog.md) but you have to use your own package name.2
|
It is very similar to what is documented in [catalog section](../usage-inventory-catalog.md) but you have to use your own package name.2
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for Adaptive Virtual Topology (AVT) tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.avt
|
::: anta.tests.avt
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,18 @@ anta_title: ANTA catalog for Adaptive Virtual Topology (AVT) tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.avt
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
merge_init_into_class: false
|
||||||
|
show_labels: true
|
||||||
|
filters:
|
||||||
|
- "!^__init__"
|
||||||
|
- "!^__str__"
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for BFD tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.bfd
|
::: anta.tests.bfd
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for BFD tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.bfd
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for connectivity tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.connectivity
|
::: anta.tests.connectivity
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for connectivity tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.connectivity
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
20
docs/api/tests.cvx.md
Normal file
20
docs/api/tests.cvx.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
anta_title: ANTA catalog for CVX tests
|
||||||
|
---
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
~ Use of this source code is governed by the Apache License 2.0
|
||||||
|
~ that can be found in the LICENSE file.
|
||||||
|
-->
|
||||||
|
|
||||||
|
::: anta.tests.cvx
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters:
|
||||||
|
- "!test"
|
||||||
|
- "!render"
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for interfaces tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.interfaces
|
::: anta.tests.interfaces
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for interfaces tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.interfaces
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
|
@ -18,6 +18,7 @@ Here are the tests that we currently provide:
|
||||||
- [BFD](tests.bfd.md)
|
- [BFD](tests.bfd.md)
|
||||||
- [Configuration](tests.configuration.md)
|
- [Configuration](tests.configuration.md)
|
||||||
- [Connectivity](tests.connectivity.md)
|
- [Connectivity](tests.connectivity.md)
|
||||||
|
- [CVX](tests.cvx.md)
|
||||||
- [Field Notices](tests.field_notices.md)
|
- [Field Notices](tests.field_notices.md)
|
||||||
- [Flow Tracking](tests.flow_tracking.md)
|
- [Flow Tracking](tests.flow_tracking.md)
|
||||||
- [GreenT](tests.greent.md)
|
- [GreenT](tests.greent.md)
|
||||||
|
@ -44,6 +45,10 @@ Here are the tests that we currently provide:
|
||||||
- [VLAN](tests.vlan.md)
|
- [VLAN](tests.vlan.md)
|
||||||
- [VXLAN](tests.vxlan.md)
|
- [VXLAN](tests.vxlan.md)
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
You can use `anta get tests` from the CLI to list all the tests available with an example. Refer to [documentation](../cli/get-tests.md) for more options.
|
||||||
|
|
||||||
## Using the Tests
|
## Using the Tests
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
|
@ -7,7 +7,13 @@ anta_title: ANTA catalog for BGP tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
!!! info "`multi-agent` Service Routing Protocols Model Requirements"
|
||||||
|
The BGP tests in this section are only supported on switches running the `multi-agent` routing protocols model. Starting from EOS version 4.30.1F, `service routing protocols model` is set to `multi-agent` by default. These BGP commands may **not** be compatible with switches running the legacy `ribd` routing protocols model and may fail if attempted.
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.routing.bgp
|
::: anta.tests.routing.bgp
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -19,3 +25,21 @@ anta_title: ANTA catalog for BGP tests
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
- "!^_[^_]"
|
- "!^_[^_]"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.routing.bgp
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
merge_init_into_class: false
|
||||||
|
show_labels: true
|
||||||
|
filters:
|
||||||
|
- "!^__init__"
|
||||||
|
- "!^__str__"
|
||||||
|
- "!AFI_SAFI_EOS_KEY"
|
||||||
|
- "!eos_key"
|
||||||
|
- "!BgpAfi"
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for generic routing tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.routing.generic
|
::: anta.tests.routing.generic
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for generic routing tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.routing.generic
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
|
@ -8,6 +8,7 @@ anta_title: ANTA catalog for IS-IS tests
|
||||||
-->
|
-->
|
||||||
|
|
||||||
::: anta.tests.routing.isis
|
::: anta.tests.routing.isis
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
|
|
@ -8,6 +8,7 @@ anta_title: ANTA catalog for OSPF tests
|
||||||
-->
|
-->
|
||||||
|
|
||||||
::: anta.tests.routing.ospf
|
::: anta.tests.routing.ospf
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for security tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.security
|
::: anta.tests.security
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,18 @@ anta_title: ANTA catalog for security tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.security
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters:
|
||||||
|
- "!^__init__"
|
||||||
|
- "!^__str__"
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for services tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.services
|
::: anta.tests.services
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for services tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.services
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
|
@ -7,6 +7,8 @@ anta_title: ANTA catalog for STUN tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.stun
|
::: anta.tests.stun
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
|
@ -18,3 +20,18 @@ anta_title: ANTA catalog for STUN tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.stun
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters:
|
||||||
|
- "!^__init__"
|
||||||
|
- "!^__str__"
|
||||||
|
|
|
@ -7,7 +7,10 @@ anta_title: ANTA catalog for System tests
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
::: anta.tests.system
|
::: anta.tests.system
|
||||||
|
|
||||||
options:
|
options:
|
||||||
show_root_heading: false
|
show_root_heading: false
|
||||||
show_root_toc_entry: false
|
show_root_toc_entry: false
|
||||||
|
@ -18,3 +21,16 @@ anta_title: ANTA catalog for System tests
|
||||||
filters:
|
filters:
|
||||||
- "!test"
|
- "!test"
|
||||||
- "!render"
|
- "!render"
|
||||||
|
|
||||||
|
# Input models
|
||||||
|
|
||||||
|
::: anta.input_models.system
|
||||||
|
|
||||||
|
options:
|
||||||
|
show_root_heading: false
|
||||||
|
show_root_toc_entry: false
|
||||||
|
show_bases: false
|
||||||
|
merge_init_into_class: false
|
||||||
|
anta_hide_test_module_description: true
|
||||||
|
show_labels: true
|
||||||
|
filters: ["!^__str__"]
|
||||||
|
|
|
@ -61,6 +61,7 @@ Options:
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
@ -162,8 +163,8 @@ Run templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A
|
||||||
|
|
||||||
### Example of multiple arguments
|
### Example of multiple arguments
|
||||||
|
|
||||||
!!! warning
|
> [!WARNING]
|
||||||
If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.
|
> If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
anta -log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 --device DC1-SPINE1
|
anta -log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 --device DC1-SPINE1
|
||||||
|
|
|
@ -64,6 +64,7 @@ Options:
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
@ -235,12 +236,14 @@ Options:
|
||||||
tag1,tag2,tag3. [env var: ANTA_TAGS]
|
tag1,tag2,tag3. [env var: ANTA_TAGS]
|
||||||
-o, --output PATH Path for test catalog [default: ./tech-support]
|
-o, --output PATH Path for test catalog [default: ./tech-support]
|
||||||
--latest INTEGER Number of scheduled show-tech to retrieve
|
--latest INTEGER Number of scheduled show-tech to retrieve
|
||||||
--configure Ensure devices have 'aaa authorization exec default
|
--configure [DEPRECATED] Ensure devices have 'aaa authorization
|
||||||
local' configured (required for SCP on EOS). THIS
|
exec default local' configured (required for SCP on
|
||||||
WILL CHANGE THE CONFIGURATION OF YOUR NETWORK.
|
EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR
|
||||||
|
NETWORK.
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
|
||||||
|
|
||||||
When executed, this command fetches tech-support files and downloads them locally into a device-specific subfolder within the designated folder. You can specify the output folder with the `--output` option.
|
When executed, this command fetches tech-support files and downloads them locally into a device-specific subfolder within the designated folder. You can specify the output folder with the `--output` option.
|
||||||
|
@ -248,13 +251,18 @@ When executed, this command fetches tech-support files and downloads them locall
|
||||||
ANTA uses SCP to download files from devices and will not trust unknown SSH hosts by default. Add the SSH public keys of your devices to your `known_hosts` file or use the `anta --insecure` option to ignore SSH host keys validation.
|
ANTA uses SCP to download files from devices and will not trust unknown SSH hosts by default. Add the SSH public keys of your devices to your `known_hosts` file or use the `anta --insecure` option to ignore SSH host keys validation.
|
||||||
|
|
||||||
The configuration `aaa authorization exec default` must be present on devices to be able to use SCP.
|
The configuration `aaa authorization exec default` must be present on devices to be able to use SCP.
|
||||||
ANTA can automatically configure `aaa authorization exec default local` using the `anta exec collect-tech-support --configure` option.
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Deprecation**
|
||||||
|
>
|
||||||
|
> ANTA can automatically configure `aaa authorization exec default local` using the `anta exec collect-tech-support --configure` option but this option is deprecated and will be removed in ANTA 2.0.0.
|
||||||
|
|
||||||
If you require specific AAA configuration for `aaa authorization exec default`, like `aaa authorization exec default none` or `aaa authorization exec default group tacacs+`, you will need to configure it manually.
|
If you require specific AAA configuration for `aaa authorization exec default`, like `aaa authorization exec default none` or `aaa authorization exec default group tacacs+`, you will need to configure it manually.
|
||||||
|
|
||||||
The `--latest` option allows retrieval of a specific number of the most recent tech-support files.
|
The `--latest` option allows retrieval of a specific number of the most recent tech-support files.
|
||||||
|
|
||||||
!!! warning
|
> [!WARNING]
|
||||||
By default **all** the tech-support files present on the devices are retrieved.
|
> By default **all** the tech-support files present on the devices are retrieved.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
|
|
@ -52,8 +52,8 @@ Options:
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip
|
> [!TIP]
|
||||||
By default, `anta get inventory` only provides information that doesn't rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the `--connected` option.
|
> By default, `anta get inventory` only provides information that doesn't rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the `--connected` option.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
|
120
docs/cli/get-tests.md
Normal file
120
docs/cli/get-tests.md
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
---
|
||||||
|
anta_title: Retrieving Tests information
|
||||||
|
---
|
||||||
|
<!--
|
||||||
|
~ Copyright (c) 2023-2024 Arista Networks, Inc.
|
||||||
|
~ Use of this source code is governed by the Apache License 2.0
|
||||||
|
~ that can be found in the LICENSE file.
|
||||||
|
-->
|
||||||
|
|
||||||
|
`anta get tests` commands help you discover available tests in ANTA.
|
||||||
|
|
||||||
|
### Command overview
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Usage: anta get tests [OPTIONS]
|
||||||
|
|
||||||
|
Show all builtin ANTA tests with an example output retrieved from each test
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--module TEXT Filter tests by module name. [default: anta.tests]
|
||||||
|
--test TEXT Filter by specific test name. If module is specified,
|
||||||
|
searches only within that module.
|
||||||
|
--short Display test names without their inputs.
|
||||||
|
--count Print only the number of tests found.
|
||||||
|
--help Show this message and exit.
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> By default, `anta get tests` will retrieve all tests available in ANTA.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Default usage
|
||||||
|
|
||||||
|
``` yaml title="anta get tests"
|
||||||
|
anta.tests.aaa:
|
||||||
|
- VerifyAcctConsoleMethods:
|
||||||
|
# Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).
|
||||||
|
methods:
|
||||||
|
- local
|
||||||
|
- none
|
||||||
|
- logging
|
||||||
|
types:
|
||||||
|
- system
|
||||||
|
- exec
|
||||||
|
- commands
|
||||||
|
- dot1x
|
||||||
|
- VerifyAcctDefaultMethods:
|
||||||
|
# Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).
|
||||||
|
methods:
|
||||||
|
- local
|
||||||
|
- none
|
||||||
|
- logging
|
||||||
|
types:
|
||||||
|
- system
|
||||||
|
- exec
|
||||||
|
- commands
|
||||||
|
- dot1x
|
||||||
|
[...]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Module usage
|
||||||
|
|
||||||
|
To retrieve all the tests from `anta.tests.stun`.
|
||||||
|
|
||||||
|
``` yaml title="anta get tests --module anta.tests.stun"
|
||||||
|
anta.tests.stun:
|
||||||
|
- VerifyStunClient:
|
||||||
|
# Verifies STUN client settings, including local IP/port and optionally public IP/port.
|
||||||
|
stun_clients:
|
||||||
|
- source_address: 172.18.3.2
|
||||||
|
public_address: 172.18.3.21
|
||||||
|
source_port: 4500
|
||||||
|
public_port: 6006
|
||||||
|
- source_address: 100.64.3.2
|
||||||
|
public_address: 100.64.3.21
|
||||||
|
source_port: 4500
|
||||||
|
public_port: 6006
|
||||||
|
- VerifyStunServer:
|
||||||
|
# Verifies the STUN server status is enabled and running.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test usage
|
||||||
|
|
||||||
|
``` yaml title="anta get tests --test VerifyTacacsSourceIntf"
|
||||||
|
anta.tests.aaa:
|
||||||
|
- VerifyTacacsSourceIntf:
|
||||||
|
# Verifies TACACS source-interface for a specified VRF.
|
||||||
|
intf: Management0
|
||||||
|
vrf: MGMT
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> You can filter tests by providing a prefix - ANTA will return all tests that start with your specified string.
|
||||||
|
|
||||||
|
```yaml title="anta get tests --test VerifyTacacs"
|
||||||
|
anta.tests.aaa:
|
||||||
|
- VerifyTacacsServerGroups:
|
||||||
|
# Verifies if the provided TACACS server group(s) are configured.
|
||||||
|
groups:
|
||||||
|
- TACACS-GROUP1
|
||||||
|
- TACACS-GROUP2
|
||||||
|
- VerifyTacacsServers:
|
||||||
|
# Verifies TACACS servers are configured for a specified VRF.
|
||||||
|
servers:
|
||||||
|
- 10.10.10.21
|
||||||
|
- 10.10.10.22
|
||||||
|
vrf: MGMT
|
||||||
|
- VerifyTacacsSourceIntf:
|
||||||
|
# Verifies TACACS source-interface for a specified VRF.
|
||||||
|
intf: Management0
|
||||||
|
vrf: MGMT
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Count the tests
|
||||||
|
|
||||||
|
```bash title="anta get tests --count"
|
||||||
|
There are 155 tests available in `anta.tests`.
|
||||||
|
```
|
|
@ -31,26 +31,13 @@ Options:
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
> [!WARNING]
|
||||||
|
>
|
||||||
`anta get from-ansible` does not support inline vaulted variables, comment them out to generate your inventory.
|
> - `anta get from-ansible` does not support inline vaulted variables, comment them out to generate your inventory.
|
||||||
If the vaulted variable is necessary to build the inventory (e.g. `ansible_host`), it needs to be unvaulted for `from-ansible` command to work."
|
>
|
||||||
|
> - If the vaulted variable is necessary to build the inventory (e.g. `ansible_host`), it needs to be unvaulted for `from-ansible` command to work."
|
||||||
The output is an inventory where the name of the container is added as a tag for each host:
|
>
|
||||||
|
> - The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance when using the `--ansible-group` option.
|
||||||
```yaml
|
|
||||||
anta_inventory:
|
|
||||||
hosts:
|
|
||||||
- host: 10.73.252.41
|
|
||||||
name: srv-pod01
|
|
||||||
- host: 10.73.252.42
|
|
||||||
name: srv-pod02
|
|
||||||
- host: 10.73.252.43
|
|
||||||
name: srv-pod03
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance when using the `--ansible-group` option.
|
|
||||||
|
|
||||||
By default, if user does not provide `--output` file, anta will save output to configured anta inventory (`anta --inventory`). If the output file has content, anta will ask user to overwrite when running in interactive console. This mechanism can be controlled by triggers in case of CI usage: `--overwrite` to force anta to overwrite file. If not set, anta will exit
|
By default, if user does not provide `--output` file, anta will save output to configured anta inventory (`anta --inventory`). If the output file has content, anta will ask user to overwrite when running in interactive console. This mechanism can be controlled by triggers in case of CI usage: `--overwrite` to force anta to overwrite file. If not set, anta will exit
|
||||||
|
|
||||||
|
@ -60,7 +47,7 @@ By default, if user does not provide `--output` file, anta will save output to c
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
tooling:
|
all:
|
||||||
children:
|
children:
|
||||||
endpoints:
|
endpoints:
|
||||||
hosts:
|
hosts:
|
||||||
|
@ -80,3 +67,16 @@ tooling:
|
||||||
ansible_host: 10.73.252.43
|
ansible_host: 10.73.252.43
|
||||||
type: endpoint
|
type: endpoint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The output is an inventory where the name of the container is added as a tag for each host:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
anta_inventory:
|
||||||
|
hosts:
|
||||||
|
- host: 10.73.252.41
|
||||||
|
name: srv-pod01
|
||||||
|
- host: 10.73.252.42
|
||||||
|
name: srv-pod02
|
||||||
|
- host: 10.73.252.43
|
||||||
|
name: srv-pod03
|
||||||
|
```
|
||||||
|
|
|
@ -52,8 +52,8 @@ anta_inventory:
|
||||||
- pod2
|
- pod2
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
> [!WARNING]
|
||||||
The current implementation only considers devices directly attached to a specific container when using the `--cvp-container` option.
|
> The current implementation only considers devices directly attached to a specific container when using the `--cvp-container` option.
|
||||||
|
|
||||||
## Creating an inventory from multiple containers
|
## Creating an inventory from multiple containers
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ ANTA provides a set of commands for performing NRFU tests on devices. These comm
|
||||||
|
|
||||||
All commands under the `anta nrfu` namespace require a catalog yaml file specified with the `--catalog` option and a device inventory file specified with the `--inventory` option.
|
All commands under the `anta nrfu` namespace require a catalog yaml file specified with the `--catalog` option and a device inventory file specified with the `--inventory` option.
|
||||||
|
|
||||||
!!! info
|
> [!TIP]
|
||||||
Issuing the command `anta nrfu` will run `anta nrfu table` without any option.
|
> Issuing the command `anta nrfu` will run `anta nrfu table` without any option.
|
||||||
|
|
||||||
### Tag management
|
### Tag management
|
||||||
|
|
||||||
|
|
|
@ -45,9 +45,10 @@ Then, run the CLI without options:
|
||||||
anta nrfu
|
anta nrfu
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
> [!NOTE]
|
||||||
All environment variables may not be needed for every commands.
|
> All environment variables may not be needed for every commands.
|
||||||
Refer to `<command> --help` for the comprehensive environment variables names.
|
>
|
||||||
|
> Refer to `<command> --help` for the comprehensive environment variables names.
|
||||||
|
|
||||||
Below are the environment variables usable with the `anta nrfu` command:
|
Below are the environment variables usable with the `anta nrfu` command:
|
||||||
|
|
||||||
|
@ -63,8 +64,8 @@ Below are the environment variables usable with the `anta nrfu` command:
|
||||||
| ANTA_ENABLE | Whether it is necessary to go to enable mode on devices. | No |
|
| ANTA_ENABLE | Whether it is necessary to go to enable mode on devices. | No |
|
||||||
| ANTA_ENABLE_PASSWORD | The optional enable password, when this variable is set, ANTA_ENABLE or `--enable` is required. | No |
|
| ANTA_ENABLE_PASSWORD | The optional enable password, when this variable is set, ANTA_ENABLE or `--enable` is required. | No |
|
||||||
|
|
||||||
!!! info
|
> [!NOTE]
|
||||||
Caching can be disabled with the global parameter `--disable-cache`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md).
|
> Caching can be disabled with the global parameter `--disable-cache`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md).
|
||||||
|
|
||||||
## ANTA Exit Codes
|
## ANTA Exit Codes
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
~ that can be found in the LICENSE file.
|
~ that can be found in the LICENSE file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
ANTA commands can be used with a `--tags` option. This option **filters the inventory** with the specified tag(s) when running the command.
|
ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the `--tags` CLI option acts as a filter to execute specific test/device combinations.
|
||||||
|
|
||||||
Tags can also be used to **restrict a specific test** to a set of devices when using `anta nrfu`.
|
|
||||||
|
|
||||||
## Defining tags
|
## Defining tags
|
||||||
|
|
||||||
|
@ -94,10 +92,11 @@ anta.tests.interfaces:
|
||||||
tags: ['spine']
|
tags: ['spine']
|
||||||
```
|
```
|
||||||
|
|
||||||
> A tag used to filter a test can also be a device name
|
> [!TIP]
|
||||||
|
>
|
||||||
!!! tip "Use different input values for a specific test"
|
> - A tag used to filter a test can also be a device name
|
||||||
Leverage tags to define different input values for a specific test. See the `VerifyUptime` example above.
|
>
|
||||||
|
> - **Use different input values for a specific test**: Leverage tags to define different input values for a specific test. See the `VerifyUptime` example above.
|
||||||
|
|
||||||
## Using tags
|
## Using tags
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ $ pip install -e .[dev,cli]
|
||||||
$ pip list -e
|
$ pip list -e
|
||||||
Package Version Editable project location
|
Package Version Editable project location
|
||||||
------- ------- -------------------------
|
------- ------- -------------------------
|
||||||
anta 1.1.0 /mnt/lab/projects/anta
|
anta 1.2.0 /mnt/lab/projects/anta
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, [`tox`](https://tox.wiki/) is configured with few environments to run CI locally:
|
Then, [`tox`](https://tox.wiki/) is configured with few environments to run CI locally:
|
||||||
|
@ -86,9 +86,9 @@ Success: no issues found in 82 source files
|
||||||
|
|
||||||
> NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares.
|
> NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares.
|
||||||
|
|
||||||
## Unit tests
|
## Unit tests with Pytest
|
||||||
|
|
||||||
To keep high quality code, we require to provide a Pytest for every tests implemented in ANTA.
|
To keep high quality code, we require to provide a **Pytest** for every tests implemented in ANTA.
|
||||||
|
|
||||||
All submodule should have its own pytest section under `tests/units/anta_tests/<submodule-name>.py`.
|
All submodule should have its own pytest section under `tests/units/anta_tests/<submodule-name>.py`.
|
||||||
|
|
||||||
|
|
45
docs/faq.md
45
docs/faq.md
|
@ -110,6 +110,17 @@ anta_title: Frequently Asked Questions (FAQ)
|
||||||
pip install -U pyopenssl>22.0
|
pip install -U pyopenssl>22.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Caveat running on non-POSIX platforms (e.g. Windows)
|
||||||
|
|
||||||
|
???+ faq "Caveat running on non-POSIX platforms (e.g. Windows)"
|
||||||
|
|
||||||
|
While ANTA should in general work on non-POSIX platforms (e.g. Windows),
|
||||||
|
there are some known limitations:
|
||||||
|
|
||||||
|
- On non-Posix platforms, ANTA is not able to check and/or adjust the system limit of file descriptors.
|
||||||
|
|
||||||
|
ANTA test suite is being run in the CI on a Windows runner.
|
||||||
|
|
||||||
## `__NSCFConstantString initialize` error on OSX
|
## `__NSCFConstantString initialize` error on OSX
|
||||||
|
|
||||||
???+ faq "`__NSCFConstantString initialize` error on OSX"
|
???+ faq "`__NSCFConstantString initialize` error on OSX"
|
||||||
|
@ -124,6 +135,40 @@ anta_title: Frequently Asked Questions (FAQ)
|
||||||
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## EOS AAA configuration for an ANTA-only user
|
||||||
|
|
||||||
|
???+ faq "EOS AAA configuration for an ANTA-only user"
|
||||||
|
|
||||||
|
Here is a starting guide to configure an ANTA-only user to run ANTA tests on a device.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
This example is not using TACACS / RADIUS but only local AAA
|
||||||
|
|
||||||
|
1. Configure the following role.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
role anta-users
|
||||||
|
10 permit command show
|
||||||
|
20 deny command .*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then add other commands if they are required for your test catalog (`ping` for example) and then tighten down the show commands to only those required for your tests.
|
||||||
|
|
||||||
|
2. Configure the following authorization (You may need to adapt depending on your AAA setup).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aaa authorization commands all default local
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure a user for the role.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
user anta role anta-users secret <secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. You can then use the credentials `anta` / `<secret>` to run ANTA against the device and adjust the role as required.
|
||||||
|
|
||||||
# Still facing issues?
|
# Still facing issues?
|
||||||
|
|
||||||
If you've tried the above solutions and continue to experience problems, please follow the [troubleshooting](troubleshooting.md) instructions and report the issue in our [GitHub repository](https://github.com/aristanetworks/anta).
|
If you've tried the above solutions and continue to experience problems, please follow the [troubleshooting](troubleshooting.md) instructions and report the issue in our [GitHub repository](https://github.com/aristanetworks/anta).
|
||||||
|
|
|
@ -48,26 +48,7 @@ management api http-commands
|
||||||
ANTA uses an inventory to list the target devices for the tests. You can create a file manually with this format:
|
ANTA uses an inventory to list the target devices for the tests. You can create a file manually with this format:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
anta_inventory:
|
--8<-- "getting-started/inventory.yml"
|
||||||
hosts:
|
|
||||||
- host: 192.168.0.10
|
|
||||||
name: spine01
|
|
||||||
tags: ['fabric', 'spine']
|
|
||||||
- host: 192.168.0.11
|
|
||||||
name: spine02
|
|
||||||
tags: ['fabric', 'spine']
|
|
||||||
- host: 192.168.0.12
|
|
||||||
name: leaf01
|
|
||||||
tags: ['fabric', 'leaf']
|
|
||||||
- host: 192.168.0.13
|
|
||||||
name: leaf02
|
|
||||||
tags: ['fabric', 'leaf']
|
|
||||||
- host: 192.168.0.14
|
|
||||||
name: leaf03
|
|
||||||
tags: ['fabric', 'leaf']
|
|
||||||
- host: 192.168.0.15
|
|
||||||
name: leaf04
|
|
||||||
tags: ['fabric', 'leaf']
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> You can read more details about how to build your inventory [here](usage-inventory-catalog.md#device-inventory)
|
> You can read more details about how to build your inventory [here](usage-inventory-catalog.md#device-inventory)
|
||||||
|
@ -90,31 +71,7 @@ The structure to follow is like:
|
||||||
Here is an example for basic tests:
|
Here is an example for basic tests:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Load anta.tests.software
|
--8<-- "getting-started/catalog.yml"
|
||||||
anta.tests.software:
|
|
||||||
- VerifyEOSVersion: # Verifies the device is running one of the allowed EOS version.
|
|
||||||
versions: # List of allowed EOS versions.
|
|
||||||
- 4.25.4M
|
|
||||||
- 4.26.1F
|
|
||||||
- '4.28.3M-28837868.4283M (engineering build)'
|
|
||||||
- VerifyTerminAttrVersion:
|
|
||||||
versions:
|
|
||||||
- v1.22.1
|
|
||||||
|
|
||||||
anta.tests.system:
|
|
||||||
- VerifyUptime: # Verifies the device uptime is higher than a value.
|
|
||||||
minimum: 1
|
|
||||||
- VerifyNTP:
|
|
||||||
- VerifySyslog:
|
|
||||||
|
|
||||||
anta.tests.mlag:
|
|
||||||
- VerifyMlagStatus:
|
|
||||||
- VerifyMlagInterfaces:
|
|
||||||
- VerifyMlagConfigSanity:
|
|
||||||
|
|
||||||
anta.tests.configuration:
|
|
||||||
- VerifyZeroTouch: # Verifies ZeroTouch is disabled.
|
|
||||||
- VerifyRunningConfigDiffs:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test your network
|
## Test your network
|
||||||
|
@ -135,128 +92,32 @@ This entrypoint has multiple options to manage test coverage and reporting.
|
||||||
|
|
||||||
To run the NRFU, you need to select an output format amongst ["json", "table", "text", "tpl-report"]. For a first usage, `table` is recommended. By default all test results for all devices are rendered but it can be changed to a report per test case or per host
|
To run the NRFU, you need to select an output format amongst ["json", "table", "text", "tpl-report"]. For a first usage, `table` is recommended. By default all test results for all devices are rendered but it can be changed to a report per test case or per host
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The following examples shows how to pass all the CLI options.
|
||||||
|
|
||||||
|
See how to use environment variables instead in the [CLI overview](cli/overview.md#anta-environment-variables)
|
||||||
|
|
||||||
#### Default report using table
|
#### Default report using table
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
anta nrfu \
|
--8<-- "getting-started/anta_nrfu_table.sh"
|
||||||
--username tom \
|
--8<-- "getting-started/anta_nrfu_table.output"
|
||||||
--password arista123 \
|
|
||||||
--enable \
|
|
||||||
--enable-password t \
|
|
||||||
--inventory .personal/inventory_atd.yml \
|
|
||||||
--catalog .personal/tests-bases.yml \
|
|
||||||
table --tags leaf
|
|
||||||
|
|
||||||
|
|
||||||
╭────────────────────── Settings ──────────────────────╮
|
|
||||||
│ Running ANTA tests: │
|
|
||||||
│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │
|
|
||||||
│ - Tests catalog contains 10 tests │
|
|
||||||
╰──────────────────────────────────────────────────────╯
|
|
||||||
[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
|
|
||||||
┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
|
|
||||||
┃ Device IP ┃ Test Name ┃ Test Status ┃ Message(s) ┃ Test description ┃ Test category ┃
|
|
||||||
┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
|
|
||||||
│ leaf01 │ VerifyEOSVersion │ success │ │ Verifies the device is running one of the allowed EOS version. │ software │
|
|
||||||
│ leaf01 │ VerifyTerminAttrVersion │ success │ │ Verifies the device is running one of the allowed TerminAttr │ software │
|
|
||||||
│ │ │ │ │ version. │ │
|
|
||||||
│ leaf01 │ VerifyUptime │ success │ │ Verifies the device uptime is higher than a value. │ system │
|
|
||||||
│ leaf01 │ VerifyNTP │ success │ │ Verifies NTP is synchronised. │ system │
|
|
||||||
│ leaf01 │ VerifySyslog │ success │ │ Verifies the device had no syslog message with a severity of warning │ system │
|
|
||||||
│ │ │ │ │ (or a more severe message) during the last 7 days. │ │
|
|
||||||
│ leaf01 │ VerifyMlagStatus │ skipped │ MLAG is disabled │ This test verifies the health status of the MLAG configuration. │ mlag │
|
|
||||||
│ leaf01 │ VerifyMlagInterfaces │ skipped │ MLAG is disabled │ This test verifies there are no inactive or active-partial MLAG │ mlag │
|
|
||||||
[...]
|
|
||||||
│ leaf04 │ VerifyMlagConfigSanity │ skipped │ MLAG is disabled │ This test verifies there are no MLAG config-sanity inconsistencies. │ mlag │
|
|
||||||
│ leaf04 │ VerifyZeroTouch │ success │ │ Verifies ZeroTouch is disabled. │ configuration │
|
|
||||||
│ leaf04 │ VerifyRunningConfigDiffs │ success │ │ │ configuration │
|
|
||||||
└───────────┴──────────────────────────┴─────────────┴──────────────────┴──────────────────────────────────────────────────────────────────────┴───────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Report in text mode
|
#### Report in text mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ anta nrfu \
|
--8<-- "getting-started/anta_nrfu_text.sh"
|
||||||
--username tom \
|
--8<-- "getting-started/anta_nrfu_text.output"
|
||||||
--password arista123 \
|
|
||||||
--enable \
|
|
||||||
--enable-password t \
|
|
||||||
--inventory .personal/inventory_atd.yml \
|
|
||||||
--catalog .personal/tests-bases.yml \
|
|
||||||
text --tags leaf
|
|
||||||
|
|
||||||
╭────────────────────── Settings ──────────────────────╮
|
|
||||||
│ Running ANTA tests: │
|
|
||||||
│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │
|
|
||||||
│ - Tests catalog contains 10 tests │
|
|
||||||
╰──────────────────────────────────────────────────────╯
|
|
||||||
[10:20:47] INFO Running ANTA tests... runner.py:75
|
|
||||||
• Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:01 • 0:00:00
|
|
||||||
leaf01 :: VerifyEOSVersion :: SUCCESS
|
|
||||||
leaf01 :: VerifyTerminAttrVersion :: SUCCESS
|
|
||||||
leaf01 :: VerifyUptime :: SUCCESS
|
|
||||||
leaf01 :: VerifyNTP :: SUCCESS
|
|
||||||
leaf01 :: VerifySyslog :: SUCCESS
|
|
||||||
leaf01 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)
|
|
||||||
leaf01 :: VerifyMlagInterfaces :: SKIPPED (MLAG is disabled)
|
|
||||||
leaf01 :: VerifyMlagConfigSanity :: SKIPPED (MLAG is disabled)
|
|
||||||
[...]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Report in JSON format
|
#### Report in JSON format
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ anta nrfu \
|
--8<-- "getting-started/anta_nrfu_json.sh"
|
||||||
--username tom \
|
--8<-- "getting-started/anta_nrfu_json.output"
|
||||||
--password arista123 \
|
|
||||||
--enable \
|
|
||||||
--enable-password t \
|
|
||||||
--inventory .personal/inventory_atd.yml \
|
|
||||||
--catalog .personal/tests-bases.yml \
|
|
||||||
json --tags leaf
|
|
||||||
|
|
||||||
╭────────────────────── Settings ──────────────────────╮
|
|
||||||
│ Running ANTA tests: │
|
|
||||||
│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │
|
|
||||||
│ - Tests catalog contains 10 tests │
|
|
||||||
╰──────────────────────────────────────────────────────╯
|
|
||||||
[10:21:51] INFO Running ANTA tests... runner.py:75
|
|
||||||
• Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:02 • 0:00:00
|
|
||||||
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
||||||
│ JSON results of all tests │
|
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "leaf01",
|
|
||||||
"test": "VerifyEOSVersion",
|
|
||||||
"categories": [
|
|
||||||
"software"
|
|
||||||
],
|
|
||||||
"description": "Verifies the device is running one of the allowed EOS version.",
|
|
||||||
"result": "success",
|
|
||||||
"messages": [],
|
|
||||||
"custom_field": "None",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "leaf01",
|
|
||||||
"test": "VerifyTerminAttrVersion",
|
|
||||||
"categories": [
|
|
||||||
"software"
|
|
||||||
],
|
|
||||||
"description": "Verifies the device is running one of the allowed TerminAttr version.",
|
|
||||||
"result": "success",
|
|
||||||
"messages": [],
|
|
||||||
"custom_field": "None",
|
|
||||||
},
|
|
||||||
[...]
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can find more information under the **usage** section of the website
|
|
||||||
|
|
||||||
### Basic usage in a Python script
|
### Basic usage in a Python script
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
|
@ -25,9 +25,8 @@ The ANTA package and the cli require some packages that are not part of the Pyth
|
||||||
pip install anta
|
pip install anta
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
> [!WARNING]
|
||||||
|
> This command alone **will not** install the ANTA CLI requirements.
|
||||||
* This command alone **will not** install the ANTA CLI requirements.
|
|
||||||
|
|
||||||
### Install ANTA CLI as an application with `pipx`
|
### Install ANTA CLI as an application with `pipx`
|
||||||
|
|
||||||
|
@ -37,9 +36,8 @@ pip install anta
|
||||||
pipx install anta[cli]
|
pipx install anta[cli]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Info
|
> [!INFO]
|
||||||
|
> Please take the time to read through the installation instructions of `pipx` before getting started.
|
||||||
Please take the time to read through the installation instructions of `pipx` before getting started.
|
|
||||||
|
|
||||||
### Install CLI from Pypi server
|
### Install CLI from Pypi server
|
||||||
|
|
||||||
|
@ -80,13 +78,13 @@ which anta
|
||||||
/home/tom/.pyenv/shims/anta
|
/home/tom/.pyenv/shims/anta
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
> [!WARNING]
|
||||||
Before running the `anta --version` command, please be aware that some users have reported issues related to the `urllib3` package. If you encounter an error at this step, please refer to our [FAQ](faq.md) page for guidance on resolving it.
|
> Before running the `anta --version` command, please be aware that some users have reported issues related to the `urllib3` package. If you encounter an error at this step, please refer to our [FAQ](faq.md) page for guidance on resolving it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check ANTA version
|
# Check ANTA version
|
||||||
anta --version
|
anta --version
|
||||||
anta, version v1.1.0
|
anta, version v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## EOS Requirements
|
## EOS Requirements
|
||||||
|
|
29
docs/scripts/generate_examples_tests.py
Executable file
29
docs/scripts/generate_examples_tests.py
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) 2024 Arista Networks, Inc.
|
||||||
|
# Use of this source code is governed by the Apache License 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
"""Generates examples/tests.py."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from pathlib import Path
|
||||||
|
from sys import path
|
||||||
|
|
||||||
|
# Override global path to load anta from pwd instead of any installed version.
|
||||||
|
path.insert(0, str(Path(__file__).parents[2]))
|
||||||
|
|
||||||
|
examples_tests_path = Path(__file__).parents[2] / "examples" / "tests.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
prev = os.environ.get("TERM", "")
|
||||||
|
os.environ["TERM"] = "dumb"
|
||||||
|
# imported after TERM is set to act upon rich console.
|
||||||
|
from anta.cli.get.commands import tests # noqa: E402
|
||||||
|
|
||||||
|
with examples_tests_path.open("w") as f:
|
||||||
|
f.write("---\n")
|
||||||
|
with redirect_stdout(f):
|
||||||
|
# removing the style
|
||||||
|
tests()
|
||||||
|
|
||||||
|
os.environ["TERM"] = prev
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue