1
0
Fork 0

Adding upstream version 1.2.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 11:55:09 +01:00
parent 77504588ab
commit 6fd6eb426a
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
154 changed files with 7346 additions and 5000 deletions

View file

@ -46,7 +46,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
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
steps:
- uses: actions/checkout@v4
@ -108,7 +108,7 @@ jobs:
needs: [lint-python, type-python]
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Setup Python
@ -119,6 +119,23 @@ jobs:
run: pip install tox tox-gh-actions
- name: "Run pytest via tox for ${{ matrix.python }}"
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:
name: Build offline documentation for testing
runs-on: ubuntu-20.04
@ -149,4 +166,4 @@ jobs:
uses: CodSpeedHQ/action@v3
with:
token: ${{ secrets.CODSPEED_TOKEN }}
run: pytest --codspeed --no-cov --log-cli-level INFO tests/benchmark
run: pytest --codspeed --no-cov --log-cli-level INFO tests/benchmark

View file

@ -7,8 +7,13 @@ on:
jobs:
pypi:
name: Publish version to Pypi servers
name: Publish Python 🐍 distribution 📦 to PyPI
runs-on: ubuntu-latest
environment:
name: production
url: https://pypi.org/p/anta
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
@ -19,11 +24,8 @@ jobs:
- name: Build package
run: |
python -m build
- name: Publish package to Pypi
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
release-coverage:
name: Updated ANTA release coverage badge

View file

@ -46,7 +46,7 @@ repos:
- '<!--| ~| -->'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
rev: v0.8.4
hooks:
- id: ruff
name: Run Ruff linter
@ -55,7 +55,7 @@ repos:
name: Run Ruff formatter
- repo: https://github.com/pycqa/pylint
rev: "v3.3.1"
rev: "v3.3.2"
hooks:
- id: pylint
name: Check code style with pylint
@ -85,7 +85,7 @@ repos:
types: [text]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
rev: v1.14.0
hooks:
- id: mypy
name: Check typing with mypy
@ -100,7 +100,7 @@ repos:
files: ^(anta|tests)/
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.42.0
rev: v0.43.0
hooks:
- id: markdownlint
name: Check Markdown files style.
@ -108,3 +108,19 @@ repos:
- --config=.github/markdownlint.yaml
- --ignore-path=.github/markdownlintignore
- --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

View file

@ -35,7 +35,7 @@ except ImportError as exc:
cli = build_cli(exc)
__all__ = ["cli", "anta"]
__all__ = ["anta", "cli"]
if __name__ == "__main__":
cli()

View file

@ -84,7 +84,10 @@ def snapshot(inventory: AntaInventory, tags: set[str] | None, commands_list: Pat
)
@click.option(
"--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,
is_flag=True,
show_default=True,

View file

@ -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)
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 = []
# 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

View file

@ -17,3 +17,4 @@ get.add_command(commands.from_cvp)
get.add_command(commands.from_ansible)
get.add_command(commands.inventory)
get.add_command(commands.tags)
get.add_command(commands.tests)

View file

@ -22,7 +22,7 @@ from anta.cli.console import console
from anta.cli.get.utils import inventory_output_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:
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
logger.info("Getting inventory for container %s from CloudVision instance '%s'", container, host)
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
@ -101,7 +105,7 @@ def from_ansible(ctx: click.Context, output: Path, ansible_group: str, ansible_i
output=output,
ansible_group=ansible_group,
)
except ValueError as e:
except (ValueError, OSError) as e:
logger.error(str(e))
ctx.exit(ExitCode.USAGE_ERROR)
@ -132,3 +136,25 @@ def tags(inventory: AntaInventory, **kwargs: Any) -> None:
tags.update(device.tags)
console.print("Tags found:")
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)

View file

@ -6,8 +6,14 @@
from __future__ import annotations
import functools
import importlib
import inspect
import json
import logging
import pkgutil
import re
import sys
import textwrap
from pathlib import Path
from sys import stdin
from typing import Any, Callable
@ -17,9 +23,11 @@ import requests
import urllib3
import yaml
from anta.cli.console import console
from anta.cli.utils import ExitCode
from anta.inventory import AntaInventory
from anta.inventory.models import AntaInventoryHost, AntaInventoryInput
from anta.models import AntaTest
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:
"""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)
with output.open(mode="w", encoding="UTF-8") as out_fd:
out_fd.write(yaml.dump({AntaInventory.INVENTORY_ROOT_KEY: yaml.safe_load(i.yaml())}))
logger.info("ANTA inventory file has been created: '%s'", output)
try:
with output.open(mode="w", encoding="UTF-8") as out_fd:
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:
@ -204,3 +229,148 @@ def create_inventory_from_ansible(inventory: Path, output: Path, ansible_group:
raise ValueError(msg)
ansible_hosts = deep_yaml_parsing(ansible_inventory)
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

View file

@ -116,8 +116,12 @@ def print_text(ctx: click.Context) -> None:
"""Print results as simple text."""
console.print()
for test in _get_result_manager(ctx).results:
message = f" ({test.messages[0]!s})" if len(test.messages) > 0 else ""
console.print(f"{test.name} :: {test.test} :: [{test.result}]{test.result.upper()}[/{test.result}]{message}", highlight=False)
if len(test.messages) <= 1:
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:

View file

@ -17,3 +17,12 @@ MD_REPORT_TOC = """**Table of Contents:**
- [Summary Totals Per Category](#summary-totals-per-category)
- [Test Results](#test-results)"""
"""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."""

View file

@ -208,3 +208,33 @@ SnmpPdu = Literal["inGetPdus", "inGetNextPdus", "inSetPdus", "outGetResponsePdus
SnmpErrorCounter = Literal[
"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",
]

View file

@ -17,7 +17,8 @@ if TYPE_CHECKING:
F = TypeVar("F", bound=Callable[..., Any])
def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]:
# TODO: Remove this decorator in ANTA v2.0.0 in favor of deprecated_test_class
def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]: # pragma: no cover
"""Return a decorator to log a message of WARNING severity when a test is deprecated.
Parameters
@ -62,6 +63,57 @@ def deprecated_test(new_tests: list[str] | None = None) -> Callable[[F], F]:
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]:
"""Return a decorator to skip a test based on the device's hardware model.

View file

@ -255,7 +255,7 @@ class AsyncEOSDevice(AntaDevice):
"""
def __init__(
def __init__( # noqa: PLR0913
self,
host: str,
username: str,
@ -372,7 +372,7 @@ class AsyncEOSDevice(AntaDevice):
"""
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.
Supports outformat `json` and `text` as output structure.
@ -409,15 +409,7 @@ class AsyncEOSDevice(AntaDevice):
command.output = response[-1]
except asynceapi.EapiCommandError as e:
# This block catches exceptions related to EOS issuing an 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 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)
self._log_eapi_command_error(command, e)
except TimeoutException as e:
# This block catches Timeout exceptions.
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)
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:
"""Update attributes of an AsyncEOSDevice instance.

View 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
View 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
View 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}"

View 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
View 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"

View 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

View 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."""

View 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}"

View 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}"

View 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)

View 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
View 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}"

View 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})"

View file

@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
from pydantic import BaseModel, ConfigDict, ValidationError, create_model
from anta import GITHUB_SUGGESTION
from anta.constants import KNOWN_EOS_ERRORS
from anta.custom_types import REGEXP_EOS_BLACKLIST_CMDS, Revision
from anta.logger import anta_log_exception, exc_to_str
from anta.result_manager.models import AntaTestStatus, TestResult
@ -240,7 +241,12 @@ class AntaCommand(BaseModel):
@property
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
------
@ -250,8 +256,22 @@ class AntaCommand(BaseModel):
"""
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 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):
@ -284,8 +304,7 @@ class AntaTest(ABC):
The following is an example of an AntaTest subclass implementation:
```python
class VerifyReachability(AntaTest):
name = "VerifyReachability"
description = "Test the network reachability to one or many destination IP(s)."
'''Test the network reachability to one or many destination IP(s).'''
categories = ["connectivity"]
commands = [AntaTemplate(template="ping vrf {vrf} {dst} source {src} repeat 2")]
@ -326,12 +345,19 @@ class AntaTest(ABC):
Python logger for this test instance.
"""
# Mandatory class attributes
# TODO: find a way to tell mypy these are mandatory for child classes - maybe Protocol
# Optional class attributes
name: 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]]
commands: ClassVar[list[AntaTemplate | AntaCommand]]
# Class attributes to handle the progress bar of ANTA CLI
progress: Progress | None = None
nrfu_task: TaskID | None = None
@ -505,12 +531,19 @@ class AntaTest(ABC):
self.instance_commands[index].output = data
def __init_subclass__(cls) -> None:
"""Verify that the mandatory class attributes are defined."""
mandatory_attributes = ["name", "description", "categories", "commands"]
for attr in mandatory_attributes:
if not hasattr(cls, attr):
msg = f"Class {cls.__module__}.{cls.__name__} is missing required class attribute {attr}"
raise NotImplementedError(msg)
"""Verify that the mandatory class attributes are defined and set name and description if not set."""
mandatory_attributes = ["categories", "commands"]
if missing_attrs := [attr for attr in mandatory_attributes if not hasattr(cls, attr)]:
msg = f"Class {cls.__module__}.{cls.__name__} is missing required class attribute(s): {', '.join(missing_attrs)}"
raise AttributeError(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
def module(self) -> str:
@ -617,14 +650,9 @@ class AntaTest(ABC):
AntaTest.update_progress()
return self.result
if 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))
else:
self.result.is_error(message="\n".join([f"{c.command} has failed: {', '.join(c.errors)}" for c in cmds]))
if self.failed_commands:
self._handle_failed_commands()
AntaTest.update_progress()
return self.result
@ -644,6 +672,28 @@ class AntaTest(ABC):
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
def update_progress(cls: type[AntaTest]) -> None:
"""Update progress bar for all AntaTest objects if it exists."""

0
anta/py.typed Normal file
View file

View file

@ -58,8 +58,7 @@ class ReportCsv:
@classmethod
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
----------
@ -108,7 +107,7 @@ class ReportCsv:
]
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(
csvfile,
delimiter=",",

View file

@ -8,7 +8,7 @@ from __future__ import annotations
import logging
import re
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.logger import anta_log_exception
@ -17,7 +17,6 @@ from anta.tools import convert_categories
if TYPE_CHECKING:
from collections.abc import Generator
from io import TextIOWrapper
from pathlib import Path
from anta.result_manager import ResultManager
@ -72,7 +71,7 @@ class MDReportBase(ABC):
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.
Parameters

View file

@ -6,15 +6,20 @@
from __future__ import annotations
import json
import logging
from collections import defaultdict
from functools import cached_property
from itertools import chain
from typing import Any
from anta.result_manager.models import AntaTestStatus, TestResult
from .models import CategoryStats, DeviceStats, TestStats
logger = logging.getLogger(__name__)
# pylint: disable=too-many-instance-attributes
class ResultManager:
"""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:
"""Class constructor.
@ -89,13 +103,16 @@ class ResultManager:
If the status of the added test is error, the status is untouched and the
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.status: AntaTestStatus = AntaTestStatus.UNSET
self.error_status = False
self.device_stats: defaultdict[str, DeviceStats] = defaultdict(DeviceStats)
self.category_stats: defaultdict[str, CategoryStats] = defaultdict(CategoryStats)
self.test_stats: defaultdict[str, TestStats] = defaultdict(TestStats)
# Initialize the statistics attributes
self._reset_stats()
def __len__(self) -> int:
"""Implement __len__ method to count number of results."""
@ -110,26 +127,43 @@ class ResultManager:
def results(self, value: list[TestResult]) -> None:
"""Set the list of TestResult."""
# When setting the results, we need to reset the state of the current instance
self._result_entries = []
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)
self.reset()
for result in value:
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
def json(self) -> str:
"""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
def sorted_category_stats(self) -> dict[str, CategoryStats]:
"""A property that returns the category_stats dictionary sorted by key name."""
self._ensure_stats_in_sync()
return dict(sorted(self.category_stats.items()))
@cached_property
@ -148,11 +182,18 @@ class ResultManager:
if test_status == "error":
self.error_status = True
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
elif self.status == "success" and test_status == "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:
"""Update the statistics based on the test result.
@ -164,7 +205,7 @@ class ResultManager:
count_attr = f"tests_{result.result}_count"
# 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)
if result.result in ("failure", "error"):
device_stats.tests_failure.add(result.test)
@ -174,16 +215,34 @@ class ResultManager:
# Update category stats
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)
# Update test stats
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)
if result.result in ("failure", "error"):
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:
"""Add a result to the ResultManager instance.
@ -197,7 +256,7 @@ class ResultManager:
"""
self._result_entries.append(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
self.__dict__.pop("results_by_status", None)

View file

@ -8,7 +8,7 @@ from __future__ import annotations
import asyncio
import logging
import os
import resource
import sys
from collections import defaultdict
from typing import TYPE_CHECKING, Any
@ -26,36 +26,39 @@ if TYPE_CHECKING:
from anta.result_manager import ResultManager
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__)
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:
"""Log cache statistics for each device in the inventory.
@ -146,22 +149,29 @@ def prepare_tests(
# Using a set to avoid inserting duplicate tests
device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
total_test_count = 0
# Create the device to tests mapping from the tags
for device in inventory.devices:
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
continue
device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))
else:
# 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])
# Add the tests with matching tags from device tags
device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))
# Then add the tests with matching tags from 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 = (
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)
return None
@ -169,7 +179,7 @@ def prepare_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.
Parameters
@ -177,7 +187,7 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
selected_tests
A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.
manager
A ResultManager
An optional ResultManager object to pre-populate with the test results. Used in dry-run mode.
Returns
-------
@ -189,7 +199,8 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
for test in test_definitions:
try:
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())
except Exception as e: # noqa: PERF203, BLE001
# An AntaTest instance is potentially user-defined code.
@ -205,7 +216,7 @@ def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinitio
@cprofile()
async def main( # noqa: PLR0913
async def main(
manager: ResultManager,
inventory: AntaInventory,
catalog: AntaCatalog,
@ -240,9 +251,6 @@ async def main( # noqa: PLR0913
dry_run
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:
logger.info("The list of tests is empty, exiting")
return
@ -263,10 +271,19 @@ async def main( # noqa: PLR0913
"--- ANTA NRFU Run Information ---\n"
f"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\n"
f"Total number of selected tests: {final_tests_count}\n"
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)
if final_tests_count > limits[0]:
@ -276,7 +293,7 @@ async def main( # noqa: PLR0913
"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:
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))
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)

View file

@ -35,8 +35,6 @@ class VerifyTacacsSourceIntf(AntaTest):
```
"""
name = "VerifyTacacsSourceIntf"
description = "Verifies TACACS source-interface for a specified VRF."
categories: ClassVar[list[str]] = ["aaa"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show tacacs", revision=1)]
@ -173,19 +167,17 @@ class VerifyAuthenMethods(AntaTest):
```yaml
anta.tests.aaa:
- VerifyAuthenMethods:
methods:
- local
- none
- logging
types:
- login
- enable
- dot1x
methods:
- local
- none
- logging
types:
- login
- enable
- dot1x
```
"""
name = "VerifyAuthenMethods"
description = "Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x)."
categories: ClassVar[list[str]] = ["aaa"]
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"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show aaa methods accounting", revision=1)]

View file

@ -7,19 +7,16 @@
# mypy: disable-error-code=attr-defined
from __future__ import annotations
from ipaddress import IPv4Address
from typing import ClassVar
from pydantic import BaseModel
from anta.decorators import skip_on_platforms
from anta.input_models.avt import AVTPath
from anta.models import AntaCommand, AntaTemplate, AntaTest
from anta.tools import get_value
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
----------------
@ -34,7 +31,6 @@ class VerifyAVTPathHealth(AntaTest):
```
"""
name = "VerifyAVTPathHealth"
description = "Verifies the status of all AVT paths for all VRFs."
categories: ClassVar[list[str]] = ["avt"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]
@ -73,15 +69,22 @@ class VerifyAVTPathHealth(AntaTest):
class VerifyAVTSpecificPath(AntaTest):
"""
Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.
"""Verifies the Adaptive Virtual Topology (AVT) path.
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
----------------
* 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.
If multiple paths are configured, the test will pass only if all the paths are valid and active.
* Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,
or does not match the specified type.
* Success: The test will pass if all of the following conditions are met:
- All AVT paths for the specified VRF are active, valid, and match the specified path type (direct/multihop), if provided.
- If multiple paths are configured, the test will pass only if all paths meet these criteria.
* 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
--------
@ -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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaTemplate(template="show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}")
]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path", revision=1)]
class Input(AntaTest.Input):
"""Input model for the VerifyAVTSpecificPath test."""
avt_paths: list[AVTPaths]
avt_paths: list[AVTPath]
"""List of AVT paths to verify."""
class AVTPaths(BaseModel):
"""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]
AVTPaths: ClassVar[type[AVTPath]] = AVTPath
"""To maintain backward compatibility."""
@skip_on_platforms(["cEOSLab", "vEOS-lab"])
@AntaTest.anta_test
@ -135,64 +118,43 @@ class VerifyAVTSpecificPath(AntaTest):
# Assume the test is successful until a failure is detected
self.result.is_success()
# Process each command in the instance
for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):
# Extract the command output and parameters
vrf = command.params.vrf
avt_name = command.params.avt_name
peer = str(command.params.destination)
command_output = self.instance_commands[0].json_output
for avt_path in self.inputs.avt_paths:
if (path_output := get_value(command_output, f"vrfs.{avt_path.vrf}.avts.{avt_path.avt_name}.avtPaths")) is None:
self.result.is_failure(f"{avt_path} - No AVT path configured")
return
command_output = command.json_output.get("vrfs", {})
# 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
path_found = path_type_found = False
# Check each AVT path
for path, path_data in avt_paths.items():
# If the path does not match the expected next hop, skip to the next path
if path_data.get("nexthopAddr") != next_hop:
continue
nexthop_path_found = True
for path, path_data in path_output.items():
dest = path_data.get("destination")
nexthop = path_data.get("nexthopAddr")
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 input_path_type and path_type != input_path_type:
continue
if not avt_path.path_type:
path_found = all([dest == str(avt_path.destination), nexthop == str(avt_path.next_hop)])
path_type_found = True
valid = get_value(path_data, "flags.valid")
active = get_value(path_data, "flags.active")
else:
path_type_found = all([dest == str(avt_path.destination), nexthop == str(avt_path.next_hop), path_type == avt_path.path_type])
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 not all([valid, active]):
failure_reasons = []
if not get_value(path_data, "flags.active"):
failure_reasons.append("inactive")
if not get_value(path_data, "flags.valid"):
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}'."
)
# If no matching path found, mark the test as failed
if not path_found:
if avt_path.path_type and not path_type_found:
self.result.is_failure(f"{avt_path} Path Type: {avt_path.path_type} - Path not found")
else:
self.result.is_failure(f"{avt_path} - Path not found")
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
----------------
@ -208,7 +170,6 @@ class VerifyAVTRole(AntaTest):
```
"""
name = "VerifyAVTRole"
description = "Verifies the AVT role of a device."
categories: ClassVar[list[str]] = ["avt"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show adaptive-virtual-topology path")]

View file

@ -8,12 +8,11 @@
from __future__ import annotations
from datetime import datetime, timezone
from ipaddress import IPv4Address
from typing import TYPE_CHECKING, Any, ClassVar
from typing import TYPE_CHECKING, 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.tools import get_value
@ -22,12 +21,24 @@ if TYPE_CHECKING:
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
----------------
* Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.
* 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.
* Success: If all of the following conditions are met:
- 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
--------
@ -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"]
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."""
bfd_peers: list[BFDPeer]
"""List of IPv4 BFD peers."""
class BFDPeer(BaseModel):
"""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`."""
"""List of IPv4 BFD"""
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
"""To maintain backward compatibility."""
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyBFDSpecificPeers."""
failures: dict[Any, Any] = {}
self.result.is_success()
# Iterating over BFD peers
for bfd_peer in self.inputs.bfd_peers:
@ -78,31 +81,33 @@ class VerifyBFDSpecificPeers(AntaTest):
# Check if BFD peer configured
if not bfd_output:
failures[peer] = {vrf: "Not Configured"}
self.result.is_failure(f"{bfd_peer} - Not found")
continue
# Check BFD peer status and remote disc
if not (bfd_output.get("status") == "up" and bfd_output.get("remoteDisc") != 0):
failures[peer] = {
vrf: {
"status": bfd_output.get("status"),
"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}")
state = bfd_output.get("status")
remote_disc = bfd_output.get("remoteDisc")
if not (state == "up" and remote_disc != 0):
self.result.is_failure(f"{bfd_peer} - Session not properly established - State: {state} Remote Discriminator: {remote_disc}")
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
----------------
* Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.
* Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.
* Success: If all of the following conditions are met:
- 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
--------
@ -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"]
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."""
bfd_peers: list[BFDPeer]
"""List of BFD peers."""
class BFDPeer(BaseModel):
"""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."""
"""List of IPv4 BFD"""
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
"""To maintain backward compatibility"""
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyBFDPeersIntervals."""
failures: dict[Any, Any] = {}
self.result.is_success()
# Iterating over BFD peers
for bfd_peers in self.inputs.bfd_peers:
peer = str(bfd_peers.peer_address)
vrf = bfd_peers.vrf
tx_interval = bfd_peers.tx_interval
rx_interval = bfd_peers.rx_interval
multiplier = bfd_peers.multiplier
for bfd_peer in self.inputs.bfd_peers:
peer = str(bfd_peer.peer_address)
vrf = bfd_peer.vrf
tx_interval = bfd_peer.tx_interval
rx_interval = bfd_peer.rx_interval
multiplier = bfd_peer.multiplier
# Check if BFD peer configured
bfd_output = get_value(
@ -168,7 +159,7 @@ class VerifyBFDPeersIntervals(AntaTest):
separator="..",
)
if not bfd_output:
failures[peer] = {vrf: "Not Configured"}
self.result.is_failure(f"{bfd_peer} - Not found")
continue
# 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_rx_interval = bfd_details.get("operRxInterval") // 1000
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 not intervals_ok:
failures[peer] = {
vrf: {
"tx_interval": op_tx_interval,
"rx_interval": op_rx_interval,
"multiplier": detect_multiplier,
}
}
if op_tx_interval != tx_interval:
self.result.is_failure(f"{bfd_peer} - Incorrect Transmit interval - Expected: {tx_interval} Actual: {op_tx_interval}")
# Check if any failures
if not failures:
self.result.is_success()
else:
self.result.is_failure(f"Following BFD peers are not configured or timers are not correct:\n{failures}")
if op_rx_interval != rx_interval:
self.result.is_failure(f"{bfd_peer} - Incorrect Receive interval - Expected: {rx_interval} Actual: {op_rx_interval}")
if detect_multiplier != multiplier:
self.result.is_failure(f"{bfd_peer} - Incorrect Multiplier - Expected: {multiplier} Actual: {detect_multiplier}")
class VerifyBFDPeersHealth(AntaTest):
"""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
----------------
* Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero,
and the last downtime of each peer is above the defined threshold.
* Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero,
or the last downtime of any peer is below the defined threshold.
* Success: If all of the following conditions are met:
- All BFD peers across the VRFs are up and remote disc is non-zero.
- Last downtime of each peer is above the defined threshold, if specified.
* 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
--------
@ -218,8 +205,6 @@ class VerifyBFDPeersHealth(AntaTest):
```
"""
name = "VerifyBFDPeersHealth"
description = "Verifies the health of all IPv4 BFD peers."
categories: ClassVar[list[str]] = ["bfd"]
# revision 1 as later revision introduces additional nesting for type
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
@ -236,18 +221,13 @@ class VerifyBFDPeersHealth(AntaTest):
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyBFDPeersHealth."""
# Initialize failure strings
down_failures = []
up_failures = []
self.result.is_success()
# Extract the current timestamp and command output
clock_output = self.instance_commands[1].json_output
current_timestamp = clock_output["utcTime"]
bfd_output = self.instance_commands[0].json_output
# set the initial result
self.result.is_success()
# Check if any IPv4 BFD peer is configured
ipv4_neighbors_exist = any(vrf_data["ipv4Neighbors"] for vrf_data in bfd_output["vrfs"].values())
if not ipv4_neighbors_exist:
@ -260,40 +240,40 @@ class VerifyBFDPeersHealth(AntaTest):
for peer_data in neighbor_data["peerStats"].values():
peer_status = peer_data["status"]
remote_disc = peer_data["remoteDisc"]
remote_disc_info = f" with remote disc {remote_disc}" if remote_disc == 0 else ""
last_down = peer_data["lastDown"]
hours_difference = (
datetime.fromtimestamp(current_timestamp, tz=timezone.utc) - datetime.fromtimestamp(last_down, tz=timezone.utc)
).total_seconds() / 3600
# Check if peer status is not up
if peer_status != "up":
down_failures.append(f"{peer} is {peer_status} in {vrf} VRF{remote_disc_info}.")
if not (peer_status == "up" and remote_disc != 0):
self.result.is_failure(
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
elif 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}.")
# 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}")
if self.inputs.down_threshold and hours_difference < self.inputs.down_threshold:
self.result.is_failure(
f"Peer: {peer} VRF: {vrf} - Session failure detected within the expected uptime threshold ({round(hours_difference)} hours ago)"
)
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
----------------
* Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).
* 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).
* Success: If all of the following conditions are met:
- 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
--------
@ -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"]
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."""
bfd_peers: list[BFDPeer]
"""List of IPv4 BFD peers."""
class BFDPeer(BaseModel):
"""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."""
"""List of IPv4 BFD"""
BFDPeer: ClassVar[type[BFDPeer]] = BFDPeer
"""To maintain backward compatibility"""
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyBFDPeersRegProtocols."""
# Initialize failure messages
failures: dict[Any, Any] = {}
self.result.is_success()
# Iterating over BFD peers, extract the parameters and command output
for bfd_peer in self.inputs.bfd_peers:
@ -348,16 +317,11 @@ class VerifyBFDPeersRegProtocols(AntaTest):
# Check if BFD peer configured
if not bfd_output:
failures[peer] = {vrf: "Not Configured"}
self.result.is_failure(f"{bfd_peer} - Not found")
continue
# 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:
failures[peer] = {vrf: sorted(difference)}
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}")
failures = " ".join(f"`{item}`" for item in difference)
self.result.is_failure(f"{bfd_peer} - {failures} routing protocol(s) not configured")

View file

@ -33,8 +33,6 @@ class VerifyZeroTouch(AntaTest):
```
"""
name = "VerifyZeroTouch"
description = "Verifies ZeroTouch is disabled"
categories: ClassVar[list[str]] = ["configuration"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config diffs", ofmt="text")]
@ -98,13 +94,12 @@ class VerifyRunningConfigLines(AntaTest):
```yaml
anta.tests.configuration:
- VerifyRunningConfigLines:
regex_patterns:
- "^enable password.*$"
- "bla bla"
regex_patterns:
- "^enable password.*$"
- "bla bla"
```
"""
name = "VerifyRunningConfigLines"
description = "Search the Running-Config for the given RegEx patterns."
categories: ClassVar[list[str]] = ["configuration"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config", ofmt="text")]

View file

@ -7,12 +7,9 @@
# mypy: disable-error-code=attr-defined
from __future__ import annotations
from ipaddress import IPv4Address
from typing import ClassVar
from pydantic import BaseModel
from anta.custom_types import Interface
from anta.input_models.connectivity import Host, LLDPNeighbor, Neighbor
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"]
# Removing the <space> between '{size}' and '{df_bit}' to compensate the df-bit set default value
# i.e if df-bit kept disable then it will add redundant space in between the command
# Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaTemplate(template="ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}", revision=1)
]
@ -57,62 +51,43 @@ class VerifyReachability(AntaTest):
hosts: list[Host]
"""List of host to ping."""
class Host(BaseModel):
"""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."""
Host: ClassVar[type[Host]] = Host
"""To maintain backward compatibility."""
def render(self, template: AntaTemplate) -> list[AntaCommand]:
"""Render the template for each host in the input list."""
commands = []
for host in self.inputs.hosts:
# Enables do not fragment bit in IP header if needed else keeping disable.
# Adding the <space> at start to compensate change in AntaTemplate
df_bit = " df-bit" if host.df_bit else ""
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
return [
template.render(
destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=" df-bit" if host.df_bit else ""
)
for host in self.inputs.hosts
]
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyReachability."""
failures = []
self.result.is_success()
for command in self.instance_commands:
src = command.params.source
dst = command.params.destination
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}")
for command, host in zip(self.instance_commands, self.inputs.hosts):
if f"{host.repeat} received" not in command.json_output["messages"][0]:
self.result.is_failure(f"{host} - Unreachable")
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
----------------
* 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:
- The provided LLDP neighbor is not found.
- The system name or port of the LLDP neighbor does not match the provided information.
- The provided LLDP neighbor is not found in the LLDP table.
- The system name or port of the LLDP neighbor does not match the expected information.
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show lldp neighbors detail", revision=1)]
class Input(AntaTest.Input):
"""Input model for the VerifyLLDPNeighbors test."""
neighbors: list[Neighbor]
neighbors: list[LLDPNeighbor]
"""List of LLDP neighbors."""
class Neighbor(BaseModel):
"""Model for an LLDP neighbor."""
port: Interface
"""LLDP port."""
neighbor_device: str
"""LLDP neighbor device."""
neighbor_port: Interface
"""LLDP neighbor port."""
Neighbor: ClassVar[type[Neighbor]] = Neighbor
"""To maintain backward compatibility."""
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyLLDPNeighbors."""
failures: dict[str, list[str]] = {}
self.result.is_success()
output = self.instance_commands[0].json_output["lldpNeighbors"]
for neighbor in self.inputs.neighbors:
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
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
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
for info in lldp_neighbor_info
):
neighbors = "\n ".join(
[
f"{neighbor[0]}_{neighbor[1]}"
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))
)
if not match_found:
failure_msg = [f"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}" for info in lldp_neighbor_info]
self.result.is_failure(f"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}")

283
anta/tests/cvx.py Normal file
View 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}")

View file

@ -34,7 +34,6 @@ class VerifyFieldNotice44Resolution(AntaTest):
```
"""
name = "VerifyFieldNotice44Resolution"
description = "Verifies that the device is using the correct Aboot version per FN0044."
categories: ClassVar[list[str]] = ["field notices"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]
@ -110,15 +109,11 @@ class VerifyFieldNotice44Resolution(AntaTest):
self.result.is_success()
incorrect_aboot_version = (
aboot_version.startswith("4.0.")
and int(aboot_version.split(".")[2]) < 7
or aboot_version.startswith("4.1.")
and int(aboot_version.split(".")[2]) < 1
(aboot_version.startswith("4.0.") and int(aboot_version.split(".")[2]) < 7)
or (aboot_version.startswith("4.1.") and int(aboot_version.split(".")[2]) < 1)
or (
aboot_version.startswith("6.0.")
and int(aboot_version.split(".")[2]) < 9
or aboot_version.startswith("6.1.")
and int(aboot_version.split(".")[2]) < 7
(aboot_version.startswith("6.0.") and int(aboot_version.split(".")[2]) < 9)
or (aboot_version.startswith("6.1.") and int(aboot_version.split(".")[2]) < 7)
)
)
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."
categories: ClassVar[list[str]] = ["field notices"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show version detail", revision=1)]

View file

@ -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:
"""
Validate the record export configuration against the tracker info.
"""Validate the record export configuration against the tracker info.
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:
"""
Validate the exporter configurations against the tracker info.
"""Validate the exporter configurations against the tracker info.
Parameters
----------
@ -74,8 +72,7 @@ def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str,
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.
@ -89,7 +86,7 @@ class VerifyHardwareFlowTrackerStatus(AntaTest):
--------
```yaml
anta.tests.flow_tracking:
- VerifyFlowTrackingHardware:
- VerifyHardwareFlowTrackerStatus:
trackers:
- name: FLOW-TRACKER
record_export:
@ -102,7 +99,6 @@ class VerifyHardwareFlowTrackerStatus(AntaTest):
```
"""
name = "VerifyHardwareFlowTrackerStatus"
description = (
"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration."
)

View file

@ -25,11 +25,11 @@ class VerifyGreenTCounters(AntaTest):
--------
```yaml
anta.tests.greent:
- VerifyGreenT:
- VerifyGreenTCounters:
```
"""
name = "VerifyGreenTCounters"
description = "Verifies if the GreenT counters are incremented."
categories: ClassVar[list[str]] = ["greent"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard counters", revision=1)]
@ -57,12 +57,12 @@ class VerifyGreenT(AntaTest):
--------
```yaml
anta.tests.greent:
- VerifyGreenTCounters:
- VerifyGreenT:
```
"""
name = "VerifyGreenT"
description = "Verifies if a GreenT policy is created."
description = "Verifies if a GreenT policy other than the default is created."
categories: ClassVar[list[str]] = ["greent"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show monitor telemetry postcard policy profile", revision=1)]

View file

@ -36,8 +36,6 @@ class VerifyTransceiversManufacturers(AntaTest):
```
"""
name = "VerifyTransceiversManufacturers"
description = "Verifies if all transceivers come from approved manufacturers."
categories: ClassVar[list[str]] = ["hardware"]
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"]
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"]
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"]
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"]
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."
categories: ClassVar[list[str]] = ["hardware"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware counter drop", revision=1)]

View file

@ -8,17 +8,18 @@
from __future__ import annotations
import re
from ipaddress import IPv4Network
from typing import Any, ClassVar, Literal
from ipaddress import IPv4Interface
from typing import Any, ClassVar
from pydantic import BaseModel, Field
from pydantic_extra_types.mac_address import MacAddress
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.input_models.interfaces import InterfaceState
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
@ -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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
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"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces status", revision=1)]
@ -191,16 +184,20 @@ class VerifyInterfaceErrDisabled(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
- 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
This test performs the following checks for each specified interface:
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
----------------
* Success: The test will pass if the provided interfaces are all in the expected state.
* Failure: The test will fail if any interface is not in the expected state.
* Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.
* 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
--------
@ -219,8 +216,6 @@ class VerifyInterfacesStatus(AntaTest):
```
"""
name = "VerifyInterfacesStatus"
description = "Verifies the status of the provided interfaces."
categories: ClassVar[list[str]] = ["interfaces"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
@ -229,30 +224,17 @@ class VerifyInterfacesStatus(AntaTest):
interfaces: list[InterfaceState]
"""List of interfaces with their expected state."""
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."""
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyInterfacesStatus."""
command_output = self.instance_commands[0].json_output
self.result.is_success()
intf_not_configured = []
intf_wrong_state = []
command_output = self.instance_commands[0].json_output
for interface in self.inputs.interfaces:
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
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 interface.line_protocol_status:
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 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):
intf_wrong_state.append(f"{interface.name} is {status}/{proto}")
if intf_not_configured:
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}")
elif interface.status == "up" and (status != "up" or proto != "up"):
self.result.is_failure(f"{interface.name} - Expected: up/up, Actual: {status}/{proto}")
elif interface.status != status:
self.result.is_failure(f"{interface.name} - Expected: {interface.status}, Actual: {status}")
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"]
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"]
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"]
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."
categories: ClassVar[list[str]] = ["interfaces"]
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"]
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."
categories: ClassVar[list[str]] = ["interfaces"]
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."
categories: ClassVar[list[str]] = ["interfaces"]
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."
categories: ClassVar[list[str]] = ["interfaces"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]
@ -662,14 +629,13 @@ class VerifyInterfaceIPv4(AntaTest):
- VerifyInterfaceIPv4:
interfaces:
- name: Ethernet2
primary_ip: 172.30.11.0/31
primary_ip: 172.30.11.1/31
secondary_ips:
- 10.10.10.0/31
- 10.10.10.1/31
- 10.10.10.10/31
```
"""
name = "VerifyInterfaceIPv4"
description = "Verifies the interface IPv4 addresses."
categories: ClassVar[list[str]] = ["interfaces"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip interface {interface}", revision=2)]
@ -685,9 +651,9 @@ class VerifyInterfaceIPv4(AntaTest):
name: Interface
"""Name of the interface."""
primary_ip: IPv4Network
primary_ip: IPv4Interface
"""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."""
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces")]
@ -886,17 +848,27 @@ class VerifyInterfacesSpeed(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.
- Ensures that the synchronization is established.
- Ensures the interfaces are in the correct state for collecting and distributing traffic.
- 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.)
This test performs the following checks for each specified interface:
1. Verifies that the interface is a member of the LACP port channel.
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
----------------
* Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.
* Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.
* Success: Interface is bundled and all LACP states match expected values for both actor and partner
* 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
--------
@ -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"]
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):
"""Input model for the VerifyLACPInterfacesStatus test."""
interfaces: list[LACPInterface]
"""List of LACP member interface."""
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]
interfaces: list[InterfaceState]
"""List of interfaces with their expected state."""
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
@AntaTest.anta_test
def test(self) -> None:
@ -940,21 +899,17 @@ class VerifyLACPInterfacesStatus(AntaTest):
# Member port verification parameters.
member_port_details = ["activity", "aggregation", "synchronization", "collecting", "distributing", "timeout"]
# Iterating over command output for different interfaces
for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):
interface = input_entry.name
portchannel = input_entry.portchannel
command_output = self.instance_commands[0].json_output
for interface in self.inputs.interfaces:
# Verify if a PortChannel is configured with the provided interface
if not (interface_details := get_value(command.json_output, f"portChannels.{portchannel}.interfaces.{interface}")):
self.result.is_failure(f"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.")
if not (interface_details := get_value(command_output, f"portChannels..{interface.portchannel}..interfaces..{interface.name}", separator="..")):
self.result.is_failure(f"{interface} - Not configured")
continue
# Verify the interface is bundled in port channel.
actor_port_status = interface_details.get("actorPortStatus")
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(message)
self.result.is_failure(f"{interface} - Not bundled - Port Status: {actor_port_status}")
continue
# Collecting actor and partner port details
@ -969,21 +924,12 @@ class VerifyLACPInterfacesStatus(AntaTest):
# Forming expected interface 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 actual_interface_output != expected_interface_output:
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 (act_port_details := actual_interface_output["actor_port_details"]) != expected_details:
self.result.is_failure(f"{interface} - Actor port details mismatch - {format_data(act_port_details)}")
if actor_port_failed_log:
message += f"Actor port details:{actor_port_failed_log}\n"
if partner_port_failed_log:
message += f"Partner port details:{partner_port_failed_log}\n"
self.result.is_failure(message)
if (part_port_details := actual_interface_output["partner_port_details"]) != expected_details:
self.result.is_failure(f"{interface} - Partner port details mismatch - {format_data(part_port_details)}")

View file

@ -30,7 +30,6 @@ class VerifyLANZ(AntaTest):
```
"""
name = "VerifyLANZ"
description = "Verifies if LANZ is enabled."
categories: ClassVar[list[str]] = ["lanz"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show queue-monitor length status", revision=1)]

View file

@ -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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging", ofmt="text")]
@ -176,10 +170,22 @@ class VerifyLoggingHosts(AntaTest):
class VerifyLoggingLogsGeneration(AntaTest):
"""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
----------------
* Success: The test will pass if logs are generated.
* Failure: The test will fail if logs are NOT generated.
* Success: If logs are being generated and the test message is found in recent logs.
* 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
--------
@ -189,8 +195,6 @@ class VerifyLoggingLogsGeneration(AntaTest):
```
"""
name = "VerifyLoggingLogsGeneration"
description = "Verifies if logs are generated."
categories: ClassVar[list[str]] = ["logging"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaCommand(command="send log level informational message ANTA VerifyLoggingLogsGeneration validation", ofmt="text"),
@ -213,10 +217,23 @@ class VerifyLoggingLogsGeneration(AntaTest):
class VerifyLoggingHostname(AntaTest):
"""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
----------------
* Success: The test will pass if logs are generated with the device FQDN.
* Failure: The test will fail if logs are NOT generated with the device FQDN.
* Success: If logs are generated with the device's complete 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
--------
@ -226,8 +243,6 @@ class VerifyLoggingHostname(AntaTest):
```
"""
name = "VerifyLoggingHostname"
description = "Verifies if logs are generated with the device FQDN."
categories: ClassVar[list[str]] = ["logging"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaCommand(command="show hostname", revision=1),
@ -257,10 +272,24 @@ class VerifyLoggingHostname(AntaTest):
class VerifyLoggingTimestamp(AntaTest):
"""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
----------------
* Success: The test will pass if logs are generated with the appropriate timestamp.
* Failure: The test will fail if logs are NOT generated with the appropriate timestamp.
* Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.
* 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
--------
@ -270,8 +299,6 @@ class VerifyLoggingTimestamp(AntaTest):
```
"""
name = "VerifyLoggingTimestamp"
description = "Verifies if logs are generated with the appropriate timestamp."
categories: ClassVar[list[str]] = ["logging"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show logging threshold errors", ofmt="text")]

View file

@ -36,8 +36,6 @@ class VerifyMlagStatus(AntaTest):
```
"""
name = "VerifyMlagStatus"
description = "Verifies the health status of the MLAG configuration."
categories: ClassVar[list[str]] = ["mlag"]
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"]
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"]
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"]
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."
categories: ClassVar[list[str]] = ["mlag"]
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."
categories: ClassVar[list[str]] = ["mlag"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show mlag detail", revision=2)]

View file

@ -35,8 +35,6 @@ class VerifyIGMPSnoopingVlans(AntaTest):
```
"""
name = "VerifyIGMPSnoopingVlans"
description = "Verifies the IGMP snooping status for the provided VLANs."
categories: ClassVar[list[str]] = ["multicast"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip igmp snooping", revision=1)]

View file

@ -18,8 +18,7 @@ from anta.tools import get_value
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.
@ -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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show path-selection paths", revision=1)]
@ -73,8 +70,7 @@ class VerifyPathsHealth(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.
@ -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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaTemplate(template="show path-selection paths peer {peer} path-group {group} source {source} destination {destination}", revision=1)

View file

@ -33,7 +33,6 @@ class VerifyUnifiedForwardingTableMode(AntaTest):
```
"""
name = "VerifyUnifiedForwardingTableMode"
description = "Verifies the device is using the expected UFT mode."
categories: ClassVar[list[str]] = ["profiles"]
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."
categories: ClassVar[list[str]] = ["profiles"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware tcam profile", revision=1)]

View file

@ -33,7 +33,6 @@ class VerifyPtpModeStatus(AntaTest):
```
"""
name = "VerifyPtpModeStatus"
description = "Verifies that the device is configured as a PTP Boundary Clock."
categories: ClassVar[list[str]] = ["ptp"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]
@ -80,7 +79,6 @@ class VerifyPtpGMStatus(AntaTest):
gmid: str
"""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."
categories: ClassVar[list[str]] = ["ptp"]
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."
categories: ClassVar[list[str]] = ["ptp"]
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."
categories: ClassVar[list[str]] = ["ptp"]
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."
categories: ClassVar[list[str]] = ["ptp"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ptp", revision=2)]

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,9 @@ from typing import TYPE_CHECKING, ClassVar, Literal
from pydantic import model_validator
from anta.custom_types import PositiveInteger
from anta.input_models.routing.generic import IPv4Routes
from anta.models import AntaCommand, AntaTemplate, AntaTest
from anta.tools import get_value
if TYPE_CHECKING:
import sys
@ -26,7 +28,7 @@ if TYPE_CHECKING:
class VerifyRoutingProtocolModel(AntaTest):
"""Verifies the configured routing protocol model is the one we expect.
"""Verifies the configured routing protocol model.
Expected Results
----------------
@ -43,8 +45,6 @@ class VerifyRoutingProtocolModel(AntaTest):
```
"""
name = "VerifyRoutingProtocolModel"
description = "Verifies the configured routing protocol model."
categories: ClassVar[list[str]] = ["routing"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaTemplate(template="show ip route vrf {vrf} {route}", revision=4),
@ -187,3 +183,76 @@ class VerifyRoutingTableEntry(AntaTest):
self.result.is_success()
else:
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}")

View file

@ -158,8 +158,6 @@ class VerifyISISNeighborState(AntaTest):
```
"""
name = "VerifyISISNeighborState"
description = "Verifies all IS-IS neighbors are in UP state."
categories: ClassVar[list[str]] = ["isis"]
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"]
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"
categories: ClassVar[list[str]] = ["isis"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)]
@ -333,9 +328,7 @@ class VerifyISISInterfaceMode(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
----------------
@ -356,12 +349,9 @@ class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
- interface: Ethernet2
address: 10.0.1.3
sid_origin: dynamic
```
"""
name = "VerifyISISSegmentRoutingAdjacencySegments"
description = "Verify expected Adjacency segments are correctly visible for each interface."
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
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):
"""
Verify dataplane of a list of ISIS-SR instances.
"""Verify dataplane of a list of ISIS-SR instances.
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing", ofmt="json")]
@ -530,8 +517,7 @@ class VerifyISISSegmentRoutingDataplane(AntaTest):
class VerifyISISSegmentRoutingTunnels(AntaTest):
"""
Verify ISIS-SR tunnels computed by device.
"""Verify ISIS-SR tunnels computed by device.
Expected Results
----------------
@ -543,26 +529,24 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
--------
```yaml
anta.tests.routing:
isis:
isis:
- VerifyISISSegmentRoutingTunnels:
entries:
# Check only endpoint
- endpoint: 1.0.0.122/32
# Check endpoint and via TI-LFA
- endpoint: 1.0.0.13/32
vias:
- type: tunnel
tunnel_id: ti-lfa
# Check endpoint and via IP routers
- endpoint: 1.0.0.14/32
vias:
- type: ip
nexthop: 1.1.1.1
# Check only endpoint
- endpoint: 1.0.0.122/32
# Check endpoint and via TI-LFA
- endpoint: 1.0.0.13/32
vias:
- type: tunnel
tunnel_id: ti-lfa
# Check endpoint and via IP routers
- endpoint: 1.0.0.14/32
vias:
- type: ip
nexthop: 1.1.1.1
```
"""
name = "VerifyISISSegmentRoutingTunnels"
description = "Verify ISIS-SR tunnels computed by device"
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
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))
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
----------
@ -666,8 +649,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
return True
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
----------
@ -694,8 +676,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
return True
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
----------
@ -722,8 +703,7 @@ class VerifyISISSegmentRoutingTunnels(AntaTest):
return True
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
----------

View file

@ -109,8 +109,6 @@ class VerifyOSPFNeighborState(AntaTest):
```
"""
name = "VerifyOSPFNeighborState"
description = "Verifies all OSPF neighbors are in FULL state."
categories: ClassVar[list[str]] = ["ospf"]
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"]
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."
categories: ClassVar[list[str]] = ["ospf"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf", revision=1)]

View file

@ -8,12 +8,12 @@ from __future__ import annotations
# Mypy does not understand AntaTest.Input typing
# mypy: disable-error-code=attr-defined
from datetime import datetime, timezone
from ipaddress import IPv4Address
from typing import TYPE_CHECKING, ClassVar, get_args
from pydantic import BaseModel, Field, model_validator
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.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"]
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."
categories: ClassVar[list[str]] = ["security"]
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."
categories: ClassVar[list[str]] = ["security"]
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"]
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"]
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."
categories: ClassVar[list[str]] = ["security"]
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"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaCommand(command="show management security ssl certificate", revision=1),
@ -498,15 +483,13 @@ class VerifyBannerLogin(AntaTest):
```yaml
anta.tests.security:
- VerifyBannerLogin:
login_banner: |
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
login_banner: |
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
```
"""
name = "VerifyBannerLogin"
description = "Verifies the login banner of a device."
categories: ClassVar[list[str]] = ["security"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show banner login", revision=1)]
@ -542,15 +525,13 @@ class VerifyBannerMotd(AntaTest):
```yaml
anta.tests.security:
- VerifyBannerMotd:
motd_banner: |
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
motd_banner: |
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
```
"""
name = "VerifyBannerMotd"
description = "Verifies the motd banner of a device."
categories: ClassVar[list[str]] = ["security"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show ip access-lists {acl}", revision=1)]
@ -669,8 +648,7 @@ class VerifyIPv4ACL(AntaTest):
class VerifyIPSecConnHealth(AntaTest):
"""
Verifies all IPv4 security connections.
"""Verifies all IPv4 security connections.
Expected Results
----------------
@ -685,8 +663,6 @@ class VerifyIPSecConnHealth(AntaTest):
```
"""
name = "VerifyIPSecConnHealth"
description = "Verifies all IPv4 security connections."
categories: ClassVar[list[str]] = ["security"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip security connection vrf all")]
@ -716,16 +692,22 @@ class VerifyIPSecConnHealth(AntaTest):
class VerifySpecificIPSecConn(AntaTest):
"""
Verifies the state of IPv4 security connections for a specified peer.
"""Verifies the IPv4 security connections.
It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.
If these addresses are not provided, it will verify all paths for the specified peer.
This test performs the following checks for each 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
----------------
* Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.
* 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.
* Success: If all checks pass for all specified IPv4 security connections.
* 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
--------
@ -744,36 +726,16 @@ class VerifySpecificIPSecConn(AntaTest):
```
"""
name = "VerifySpecificIPSecConn"
description = "Verifies IPv4 security connections for a peer."
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):
"""Input model for the VerifySpecificIPSecConn test."""
ip_security_connections: list[IPSecPeers]
ip_security_connections: list[IPSecPeer]
"""List of IP4v security peers."""
class IPSecPeers(BaseModel):
"""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."""
IPSecPeers: ClassVar[type[IPSecPeers]] = IPSecPeers
"""To maintain backward compatibility."""
def render(self, template: AntaTemplate) -> list[AntaCommand]:
"""Render the template for each input IP Sec connection."""
@ -783,15 +745,15 @@ class VerifySpecificIPSecConn(AntaTest):
def test(self) -> None:
"""Main test function for VerifySpecificIPSecConn."""
self.result.is_success()
for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):
conn_output = command_output.json_output["connections"]
peer = command_output.params.peer
vrf = command_output.params.vrf
conn_input = input_peer.connections
vrf = input_peer.vrf
# Check if IPv4 security connection is configured
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
# If connection details are not provided then check all connections of a peer
@ -801,10 +763,8 @@ class VerifySpecificIPSecConn(AntaTest):
if state != "Established":
source = conn_data.get("saddr")
destination = conn_data.get("daddr")
vrf = conn_data.get("tunnelNs")
self.result.is_failure(
f"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` "
f"but found `{state}` instead."
f"{input_peer} Source: {source} Destination: {destination} - Connection down - Expected: Established, Actual: {state}"
)
continue
@ -820,19 +780,14 @@ class VerifySpecificIPSecConn(AntaTest):
if (source_input, destination_input, vrf) in existing_connections:
existing_state = existing_connections[(source_input, destination_input, vrf)]
if existing_state != "Established":
self.result.is_failure(
f"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` "
f"for peer `{peer}` is `Established` but found `{existing_state}` instead."
)
failure = f"Expected: Established, Actual: {existing_state}"
self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection down - {failure}")
else:
self.result.is_failure(
f"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found."
)
self.result.is_failure(f"{input_peer} Source: {source_input} Destination: {destination_input} - Connection not found.")
class VerifyHardwareEntropy(AntaTest):
"""
Verifies hardware entropy generation is enabled on device.
"""Verifies hardware entropy generation is enabled on device.
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show management security")]

View file

@ -7,14 +7,14 @@ from __future__ import annotations
# Mypy does not understand AntaTest.Input typing
# mypy: disable-error-code=attr-defined
from ipaddress import IPv4Address, IPv6Address
from typing import ClassVar
from pydantic import BaseModel, Field
from pydantic import BaseModel
from anta.custom_types import ErrDisableInterval, ErrDisableReasons
from anta.input_models.services import DnsServer
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):
@ -34,8 +34,6 @@ class VerifyHostname(AntaTest):
```
"""
name = "VerifyHostname"
description = "Verifies the hostname of a device."
categories: ClassVar[list[str]] = ["services"]
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."
categories: ClassVar[list[str]] = ["services"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="bash timeout 10 nslookup {domain}", revision=1)]
@ -109,10 +106,17 @@ class VerifyDNSLookup(AntaTest):
class VerifyDNSServers(AntaTest):
"""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
----------------
* 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
--------
@ -129,8 +133,6 @@ class VerifyDNSServers(AntaTest):
```
"""
name = "VerifyDNSServers"
description = "Verifies if the DNS servers are correctly configured."
categories: ClassVar[list[str]] = ["services"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip name-server", revision=1)]
@ -139,38 +141,28 @@ class VerifyDNSServers(AntaTest):
dns_servers: list[DnsServer]
"""List of DNS servers to verify."""
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."""
DnsServer: ClassVar[type[DnsServer]] = DnsServer
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyDNSServers."""
command_output = self.instance_commands[0].json_output["nameServerConfigs"]
self.result.is_success()
command_output = self.instance_commands[0].json_output["nameServerConfigs"]
for server in self.inputs.dns_servers:
address = str(server.server_address)
vrf = server.vrf
priority = server.priority
input_dict = {"ipAddr": address, "vrf": vrf}
if get_item(command_output, "ipAddr", address) is None:
self.result.is_failure(f"DNS server `{address}` is not configured with any VRF.")
continue
# Check if the DNS server is configured with specified VRF.
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
# Check if the DNS server priority matches with expected.
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):
@ -194,8 +186,6 @@ class VerifyErrdisableRecovery(AntaTest):
```
"""
name = "VerifyErrdisableRecovery"
description = "Verifies the errdisable recovery reason, status, and interval."
categories: ClassVar[list[str]] = ["services"]
# NOTE: Only `text` output format is supported for this command
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show errdisable recovery", ofmt="text")]

View file

@ -34,7 +34,6 @@ class VerifySnmpStatus(AntaTest):
```
"""
name = "VerifySnmpStatus"
description = "Verifies if the SNMP agent is enabled."
categories: ClassVar[list[str]] = ["snmp"]
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."
categories: ClassVar[list[str]] = ["snmp"]
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."
categories: ClassVar[list[str]] = ["snmp"]
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"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]
@ -317,8 +308,6 @@ class VerifySnmpErrorCounters(AntaTest):
- inBadCommunityNames
"""
name = "VerifySnmpErrorCounters"
description = "Verifies the SNMP error counters."
categories: ClassVar[list[str]] = ["snmp"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)]

View file

@ -34,7 +34,6 @@ class VerifyEOSVersion(AntaTest):
```
"""
name = "VerifyEOSVersion"
description = "Verifies the EOS version of the device."
categories: ClassVar[list[str]] = ["software"]
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."
categories: ClassVar[list[str]] = ["software"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
AntaCommand(command="show extensions", revision=2),

View file

@ -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"]
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"]
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"]
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)."
categories: ClassVar[list[str]] = ["stp"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show spanning-tree topology status detail", revision=1)]

View file

@ -7,32 +7,36 @@
# mypy: disable-error-code=attr-defined
from __future__ import annotations
from ipaddress import IPv4Address
from typing import ClassVar
from pydantic import BaseModel
from anta.custom_types import Port
from anta.decorators import deprecated_test_class
from anta.input_models.stun import StunClientTranslation
from anta.models import AntaCommand, AntaTemplate, AntaTest
from anta.tools import get_failed_logs, get_value
from anta.tools import get_value
class VerifyStunClient(AntaTest):
"""
Verifies the configuration of the STUN client, specifically the IPv4 source address and port.
class VerifyStunClientTranslation(AntaTest):
"""Verifies the translation for a source address on a STUN client.
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
----------------
* Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.
* Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.
* Success: If all of the following conditions are met:
- The test will pass if the 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
--------
```yaml
anta.tests.stun:
- VerifyStunClient:
- VerifyStunClientTranslation:
stun_clients:
- source_address: 172.18.3.2
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}")]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template="show stun client translations {source_address} {source_port}", revision=1)]
class Input(AntaTest.Input):
"""Input model for the VerifyStunClient test."""
"""Input model for the VerifyStunClientTranslation test."""
stun_clients: list[ClientAddress]
class ClientAddress(BaseModel):
"""Source and public address/port details of STUN client."""
source_address: IPv4Address
"""IPv4 source address of STUN client."""
source_port: Port = 4500
"""Source port number for STUN client."""
public_address: IPv4Address | None = None
"""Optional IPv4 public address of STUN client."""
public_port: Port | None = None
"""Optional public port number for STUN client."""
stun_clients: list[StunClientTranslation]
"""List of STUN clients."""
StunClientTranslation: ClassVar[type[StunClientTranslation]] = StunClientTranslation
def render(self, template: AntaTemplate) -> list[AntaCommand]:
"""Render the template for each STUN translation."""
@ -73,53 +65,61 @@ class VerifyStunClient(AntaTest):
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyStunClient."""
"""Main test function for VerifyStunClientTranslation."""
self.result.is_success()
# Iterate over each command output and corresponding client input
for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):
bindings = command.json_output["bindings"]
source_address = str(command.params.source_address)
source_port = command.params.source_port
input_public_address = client_input.public_address
input_public_port = client_input.public_port
# If no bindings are found for the STUN client, mark the test as a failure and continue with the next client
if not bindings:
self.result.is_failure(f"STUN client transaction for source `{source_address}:{source_port}` is not found.")
self.result.is_failure(f"{client_input} - STUN client translation not found.")
continue
# Extract the public address and port from the client input
public_address = client_input.public_address
public_port = client_input.public_port
# Extract the transaction ID from the bindings
transaction_id = next(iter(bindings.keys()))
# Prepare the actual and expected STUN data for comparison
actual_stun_data = {
"source ip": get_value(bindings, f"{transaction_id}.sourceAddress.ip"),
"source port": get_value(bindings, f"{transaction_id}.sourceAddress.port"),
}
expected_stun_data = {"source ip": source_address, "source port": source_port}
# Verifying the public address if provided
if input_public_address and str(input_public_address) != (actual_public_address := get_value(bindings, f"{transaction_id}.publicAddress.ip")):
self.result.is_failure(f"{client_input} - Incorrect public-facing address - Expected: {input_public_address} Actual: {actual_public_address}")
# If public address is provided, add it to the actual and expected STUN data
if public_address is not None:
actual_stun_data["public ip"] = get_value(bindings, f"{transaction_id}.publicAddress.ip")
expected_stun_data["public ip"] = str(public_address)
# Verifying the public port if provided
if input_public_port and input_public_port != (actual_public_port := get_value(bindings, f"{transaction_id}.publicAddress.port")):
self.result.is_failure(f"{client_input} - Incorrect public-facing port - Expected: {input_public_port} Actual: {actual_public_port}")
# If public port is provided, add it to the actual and expected STUN data
if public_port is not None:
actual_stun_data["public port"] = get_value(bindings, f"{transaction_id}.publicAddress.port")
expected_stun_data["public port"] = public_port
# If the actual STUN data does not match the expected STUN data, mark the test as failure
if actual_stun_data != expected_stun_data:
failed_log = get_failed_logs(expected_stun_data, actual_stun_data)
self.result.is_failure(f"For STUN source `{source_address}:{source_port}`:{failed_log}")
@deprecated_test_class(new_tests=["VerifyStunClientTranslation"], removal_in_version="v2.0.0")
class VerifyStunClient(VerifyStunClientTranslation):
"""(Deprecated) Verifies the translation for a source address on a STUN client.
Alias for the VerifyStunClientTranslation test to maintain backward compatibility.
When initialized, it will emit a deprecation warning and call the VerifyStunClientTranslation test.
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):
"""
Verifies the STUN server status is enabled and running.
"""Verifies the STUN server status is enabled and running.
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show stun server status", revision=1)]

View file

@ -8,14 +8,12 @@
from __future__ import annotations
import re
from ipaddress import IPv4Address
from typing import TYPE_CHECKING, ClassVar
from pydantic import BaseModel, Field
from anta.custom_types import Hostname, PositiveInteger
from anta.custom_types import PositiveInteger
from anta.input_models.system import NTPServer
from anta.models import AntaCommand, AntaTest
from anta.tools import get_failed_logs, get_value
from anta.tools import get_value
if TYPE_CHECKING:
from anta.models import AntaTemplate
@ -42,7 +40,6 @@ class VerifyUptime(AntaTest):
```
"""
name = "VerifyUptime"
description = "Verifies the device uptime."
categories: ClassVar[list[str]] = ["system"]
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"]
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.
* 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.
Examples
--------
```yaml
anta.tests.system:
- VerifyCoreDump:
- VerifyCoredump:
```
"""
name = "VerifyCoredump"
description = "Verifies there are no core dump files."
categories: ClassVar[list[str]] = ["system"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system coredump", revision=1)]
@ -143,7 +137,7 @@ class VerifyCoredump(AntaTest):
class VerifyAgentLogs(AntaTest):
"""Verifies that no agent crash reports are present on the device.
"""Verifies there are no agent crash reports.
Expected Results
----------------
@ -158,8 +152,6 @@ class VerifyAgentLogs(AntaTest):
```
"""
name = "VerifyAgentLogs"
description = "Verifies there are no agent crash reports."
categories: ClassVar[list[str]] = ["system"]
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"]
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"]
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"]
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."
categories: ClassVar[list[str]] = ["system"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ntp associations")]
@ -338,55 +321,33 @@ class VerifyNTPAssociations(AntaTest):
ntp_servers: list[NTPServer]
"""List of NTP servers."""
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."""
NTPServer: ClassVar[type[NTPServer]] = NTPServer
@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifyNTPAssociations."""
failures: str = ""
self.result.is_success()
if not (peer_details := get_value(self.instance_commands[0].json_output, "peers")):
self.result.is_failure("None of NTP peers are not configured.")
if not (peers := get_value(self.instance_commands[0].json_output, "peers")):
self.result.is_failure("No NTP peers configured")
return
# Iterate over each NTP server.
for ntp_server in self.inputs.ntp_servers:
server_address = str(ntp_server.server_address)
preferred = ntp_server.preferred
stratum = ntp_server.stratum
# Check if NTP server details exists.
if (peer_detail := get_value(peer_details, server_address, separator="..")) is None:
failures += f"NTP peer {server_address} is not configured.\n"
# We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.
matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details["peerIpAddr"], peer})), None)
if not matching_peer:
self.result.is_failure(f"{ntp_server} - Not configured")
continue
# Collecting the expected NTP peer details.
expected_peer_details = {"condition": "candidate", "stratum": stratum}
if preferred:
expected_peer_details["condition"] = "sys.peer"
# Collecting the expected/actual NTP peer details.
exp_condition = "sys.peer" if ntp_server.preferred else "candidate"
exp_stratum = ntp_server.stratum
act_condition = get_value(peers[matching_peer], "condition")
act_stratum = get_value(peers[matching_peer], "stratumLevel")
# Collecting the actual NTP peer details.
actual_peer_details = {"condition": get_value(peer_detail, "condition"), "stratum": get_value(peer_detail, "stratumLevel")}
# 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)
if act_condition != exp_condition or act_stratum != exp_stratum:
self.result.is_failure(f"{ntp_server} - Bad association - Condition: {act_condition}, Stratum: {act_stratum}")

View file

@ -38,7 +38,6 @@ class VerifyVlanInternalPolicy(AntaTest):
```
"""
name = "VerifyVlanInternalPolicy"
description = "Verifies the VLAN internal allocation policy and the range of VLANs."
categories: ClassVar[list[str]] = ["vlan"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show vlan internal allocation policy", revision=1)]

View file

@ -23,8 +23,8 @@ if TYPE_CHECKING:
class VerifyVxlan1Interface(AntaTest):
"""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.
Expected Results
@ -41,7 +41,6 @@ class VerifyVxlan1Interface(AntaTest):
```
"""
name = "VerifyVxlan1Interface"
description = "Verifies the Vxlan1 interface status."
categories: ClassVar[list[str]] = ["vxlan"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces description", revision=1)]
@ -65,7 +64,7 @@ class VerifyVxlan1Interface(AntaTest):
class VerifyVxlanConfigSanity(AntaTest):
"""Verifies that no issues are detected with the VXLAN configuration.
"""Verifies there are no VXLAN config-sanity inconsistencies.
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"]
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"]
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"]
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show interfaces", revision=1)]

View file

@ -94,8 +94,7 @@ def get_dict_superset(
*,
required: bool = False,
) -> 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.
@ -378,7 +377,7 @@ def safe_command(command: str) -> str:
def convert_categories(categories: list[str]) -> list[str]:
"""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.
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]
msg = f"Wrong input type '{type(categories)}' for convert_categories."
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())

View file

@ -9,4 +9,4 @@ from .config_session import SessionConfig
from .device import Device
from .errors import EapiCommandError
__all__ = ["Device", "SessionConfig", "EapiCommandError"]
__all__ = ["Device", "EapiCommandError", "SessionConfig"]

View file

@ -34,8 +34,7 @@ __all__ = ["port_check_url"]
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
----------

View file

@ -29,8 +29,7 @@ __all__ = ["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.
@ -44,8 +43,7 @@ class SessionConfig:
CLI_CFG_FACTORY_RESET = "rollback clean-config"
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
to the given device instance, and using the session `name`.
@ -81,8 +79,7 @@ class SessionConfig:
# -------------------------------------------------------------------------
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:
# 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]
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:
# show configuration sessions detail
@ -179,8 +175,7 @@ class SessionConfig:
return res["sessions"].get(self.name)
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
before sending the configuration content.
@ -218,8 +213,7 @@ class SessionConfig:
await self._cli(commands=commands)
async def commit(self, timer: str | None = None) -> None:
"""
Commit the session config.
"""Commit the session config.
Run the following command on the device:
# configure session <name>
@ -241,8 +235,7 @@ class SessionConfig:
await self._cli(command)
async def abort(self) -> None:
"""
Abort the configuration session.
"""Abort the configuration session.
Run the following command on the device:
# configure session <name> abort
@ -250,8 +243,7 @@ class SessionConfig:
await self._cli(f"{self._cli_config_session} abort")
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:
# 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
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).

View file

@ -43,8 +43,7 @@ __all__ = ["Device"]
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
httpx.AsyncClient, so any initialization options can be passed directly.
@ -63,8 +62,7 @@ class Device(httpx.AsyncClient):
port: str | int | None = None,
**kwargs: Any, # noqa: ANN401
) -> None:
"""
Initialize the Device class.
"""Initialize the Device class.
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.
@ -111,8 +109,7 @@ class Device(httpx.AsyncClient):
self.headers["Content-Type"] = "application/json-rpc"
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,
but this step is not required.
@ -124,7 +121,7 @@ class Device(httpx.AsyncClient):
"""
return await port_check_url(self.base_url)
async def cli( # noqa: PLR0913
async def cli(
self,
command: 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,
req_id: int | str | None = None,
) -> list[dict[str, Any] | str] | dict[str, Any] | str | None:
"""
Execute one or more CLI commands.
"""Execute one or more CLI commands.
Parameters
----------
@ -199,7 +195,7 @@ class Device(httpx.AsyncClient):
return None
raise
def _jsonrpc_command( # noqa: PLR0913
def _jsonrpc_command(
self,
commands: Sequence[str | dict[str, Any]] | None = None,
ofmt: str | None = None,
@ -264,8 +260,7 @@ class Device(httpx.AsyncClient):
return cmd
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
----------

View file

@ -12,8 +12,7 @@ import httpx
class EapiCommandError(RuntimeError):
"""
Exception class for EAPI command errors.
"""Exception class for EAPI command errors.
Attributes
----------

View file

@ -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.
!!! 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
> [!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
## [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"
```
!!! note "How to create your inventory file"
Please visit this [dedicated section](../usage-inventory-catalog.md) for how to use inventory and catalog files.
> [!NOTE]
> **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

View file

@ -4,8 +4,8 @@
~ that can be found in the LICENSE file.
-->
!!! info
This documentation applies for both creating tests in ANTA or creating your own test package.
> [!INFO]
> 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.
@ -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:
```python
````python
from anta.models import AntaTest, AntaCommand
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"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment temperature", revision=1)]
@ -51,7 +49,7 @@ class VerifyTemperature(AntaTest):
self.result.is_success()
else:
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.
@ -61,13 +59,13 @@ Full AntaTest API documentation is available in the [API documentation section](
### Class Attributes
- `name` (`str`): Name of the test. Used during reporting.
- `description` (`str`): A human readable description of your test.
- `name` (`str`, `optional`): Name of the test. Used during reporting. By default set to the Class name.
- `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.
- `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
All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation.
> [!INFO]
> All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation.
### Instance Attributes
@ -84,11 +82,15 @@ Full AntaTest API documentation is available in the [API documentation section](
show_root_toc_entry: false
heading_level: 10
!!! note "Logger object"
ANTA already provides comprehensive logging at every steps of a test execution. The [AntaTest](../api/models.md#anta.models.AntaTest) class also provides a `logger` attribute that is a Python logger specific to the test instance. See [Python documentation](https://docs.python.org/3/library/logging.html) for more information.
!!! note "AntaDevice object"
Even if `device` is not a private attribute, you should not need to access this object in your code.
> [!NOTE]
>
> - **Logger object**
>
> ANTA already provides comprehensive logging at every steps of a test execution. The [AntaTest](../api/models.md#anta.models.AntaTest) class also provides a `logger` attribute that is a Python logger specific to the test instance. See [Python documentation](https://docs.python.org/3/library/logging.html) for more information.
>
> - **AntaDevice object**
>
> Even if `device` is not a private attribute, you should not need to access this object in your code.
### Test Inputs
@ -131,8 +133,8 @@ Full `ResultOverwrite` model documentation is available in [API documentation se
show_root_toc_entry: false
heading_level: 10
!!! 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.
> [!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.
### 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.
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
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).
> [!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).
```python
from anta.models import AntaTest, AntaCommand, AntaTemplate
@ -171,11 +173,11 @@ from anta.models import AntaTest, AntaCommand, AntaTemplate
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
description = "<test description in human reading format>"
# name = <override> # uncomment to override default behavior of name=Class Name
# description = <override> # uncomment to override default behavior of description=first line of docstring
categories = ["<arbitrary category>", "<another arbitrary category>"]
commands = [
AntaCommand(
@ -195,21 +197,23 @@ class <YourTestName>(AntaTest):
]
```
!!! tip "Command revision and version"
* 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.
* 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`.
* A **revision takes precedence over a version** (e.g. if a command is run with version="latest" and revision=1, the first revision of the model is returned)
* By default, eAPI returns the first revision of each model to ensure that when upgrading, integrations with existing tools are not broken. This is done by using by default `version=1` in eAPI calls.
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)]
```
> [!TIP]
> **Command revision and version**
>
> - 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.
> - 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`.
> - A **revision takes precedence over a version** (e.g. if a command is run with version="latest" and revision=1, the first revision of the model is returned)
> - By default, eAPI returns the first revision of each model to ensure that when upgrading, integrations with existing tools are not broken. This is done by using by default `version=1` in eAPI calls.
>
> 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`:
>
> ```python
> # revision 1 as later revision introduce additional nesting for type
> commands = [AntaCommand(command="show bfd peers", revision=1)]
> ```
### 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.
!!! note
All the `pydantic` features are supported. For instance you can define [validators](https://docs.pydantic.dev/latest/usage/validators/) for complex input validation.
> [!NOTE]
> All the `pydantic` features are supported. For instance you can define [validators](https://docs.pydantic.dev/latest/usage/validators/) for complex input validation.
### Template rendering
@ -340,10 +344,10 @@ class VerifyTemperature(AntaTest):
## Access your custom tests in the test catalog
!!! warning ""
This section is required only if you are not merging your development into ANTA. Otherwise, just follow [contribution guide](../contribution.md).
> [!WARNING]
> 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

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for Adaptive Virtual Topology (AVT) tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.avt
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,18 @@ anta_title: ANTA catalog for Adaptive Virtual Topology (AVT) tests
filters:
- "!test"
- "!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__"

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for BFD tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.bfd
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for BFD tests
filters:
- "!test"
- "!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__"]

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for connectivity tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.connectivity
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for connectivity tests
filters:
- "!test"
- "!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
View 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"

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for interfaces tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.interfaces
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for interfaces tests
filters:
- "!test"
- "!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__"]

View file

@ -18,6 +18,7 @@ Here are the tests that we currently provide:
- [BFD](tests.bfd.md)
- [Configuration](tests.configuration.md)
- [Connectivity](tests.connectivity.md)
- [CVX](tests.cvx.md)
- [Field Notices](tests.field_notices.md)
- [Flow Tracking](tests.flow_tracking.md)
- [GreenT](tests.greent.md)
@ -44,6 +45,10 @@ Here are the tests that we currently provide:
- [VLAN](tests.vlan.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
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).

View file

@ -7,7 +7,13 @@ anta_title: ANTA catalog for BGP tests
~ 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
options:
show_root_heading: false
show_root_toc_entry: false
@ -19,3 +25,21 @@ anta_title: ANTA catalog for BGP tests
- "!test"
- "!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"

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for generic routing tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.routing.generic
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for generic routing tests
filters:
- "!test"
- "!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__"]

View file

@ -8,6 +8,7 @@ anta_title: ANTA catalog for IS-IS tests
-->
::: anta.tests.routing.isis
options:
show_root_heading: false
show_root_toc_entry: false

View file

@ -8,6 +8,7 @@ anta_title: ANTA catalog for OSPF tests
-->
::: anta.tests.routing.ospf
options:
show_root_heading: false
show_root_toc_entry: false

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for security tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.security
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,18 @@ anta_title: ANTA catalog for security tests
filters:
- "!test"
- "!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__"

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for services tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.services
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for services tests
filters:
- "!test"
- "!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__"]

View file

@ -7,6 +7,8 @@ anta_title: ANTA catalog for STUN tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.stun
options:
show_root_heading: false
@ -18,3 +20,18 @@ anta_title: ANTA catalog for STUN tests
filters:
- "!test"
- "!render"
# Input models
::: anta.input_models.stun
options:
show_root_heading: false
show_root_toc_entry: false
show_bases: false
merge_init_into_class: false
anta_hide_test_module_description: true
show_labels: true
filters:
- "!^__init__"
- "!^__str__"

View file

@ -7,7 +7,10 @@ anta_title: ANTA catalog for System tests
~ that can be found in the LICENSE file.
-->
# Tests
::: anta.tests.system
options:
show_root_heading: false
show_root_toc_entry: false
@ -18,3 +21,16 @@ anta_title: ANTA catalog for System tests
filters:
- "!test"
- "!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__"]

View file

@ -61,6 +61,7 @@ Options:
--help Show this message and exit.
```
> [!TIP]
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
### Example
@ -162,8 +163,8 @@ Run templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A
### Example of multiple arguments
!!! warning
If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.
> [!WARNING]
> If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.
```bash
anta -log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 --device DC1-SPINE1    

View file

@ -64,6 +64,7 @@ Options:
--help Show this message and exit.
```
> [!TIP]
> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices
### Example
@ -235,12 +236,14 @@ Options:
tag1,tag2,tag3. [env var: ANTA_TAGS]
-o, --output PATH Path for test catalog [default: ./tech-support]
--latest INTEGER Number of scheduled show-tech to retrieve
--configure Ensure devices have 'aaa authorization exec default
local' configured (required for SCP on EOS). THIS
WILL CHANGE THE CONFIGURATION OF YOUR NETWORK.
--configure [DEPRECATED] Ensure devices have 'aaa authorization
exec default local' configured (required for SCP on
EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR
NETWORK.
--help Show this message and exit.
```
> [!TIP]
> `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.
@ -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.
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.
The `--latest` option allows retrieval of a specific number of the most recent tech-support files.
!!! warning
By default **all** the tech-support files present on the devices are retrieved.
> [!WARNING]
> By default **all** the tech-support files present on the devices are retrieved.
### Example

View file

@ -52,8 +52,8 @@ Options:
--help Show this message and exit.
```
!!! 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.
> [!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.
### Example

120
docs/cli/get-tests.md Normal file
View 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`.
```

View file

@ -31,26 +31,13 @@ Options:
--help Show this message and exit.
```
!!! warning
`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."
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
```
!!! 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.
> [!WARNING]
>
> - `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."
>
> - 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
@ -60,7 +47,7 @@ By default, if user does not provide `--output` file, anta will save output to c
```yaml
---
tooling:
all:
children:
endpoints:
hosts:
@ -80,3 +67,16 @@ tooling:
ansible_host: 10.73.252.43
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
```

View file

@ -52,8 +52,8 @@ anta_inventory:
- pod2
```
!!! warning
The current implementation only considers devices directly attached to a specific container when using the `--cvp-container` option.
> [!WARNING]
> The current implementation only considers devices directly attached to a specific container when using the `--cvp-container` option.
## Creating an inventory from multiple containers

View file

@ -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.
!!! info
Issuing the command `anta nrfu` will run `anta nrfu table` without any option.
> [!TIP]
> Issuing the command `anta nrfu` will run `anta nrfu table` without any option.
### Tag management

View file

@ -45,9 +45,10 @@ Then, run the CLI without options:
anta nrfu
```
!!! note
All environment variables may not be needed for every commands.
Refer to `<command> --help` for the comprehensive environment variables names.
> [!NOTE]
> All environment variables may not be needed for every commands.
>
> Refer to `<command> --help` for the comprehensive environment variables names.
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_PASSWORD | The optional enable password, when this variable is set, ANTA_ENABLE or `--enable` is required. | No |
!!! info
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).
> [!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).
## ANTA Exit Codes

View file

@ -4,9 +4,7 @@
~ 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.
Tags can also be used to **restrict a specific test** to a set of devices when using `anta nrfu`.
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.
## Defining tags
@ -94,10 +92,11 @@ anta.tests.interfaces:
tags: ['spine']
```
> A tag used to filter a test can also be a device name
!!! tip "Use different input values for a specific test"
Leverage tags to define different input values for a specific test. See the `VerifyUptime` example above.
> [!TIP]
>
> - A tag used to filter a test can also be a device name
>
> - **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

View file

@ -29,7 +29,7 @@ $ pip install -e .[dev,cli]
$ pip list -e
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:
@ -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.
## 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`.

View file

@ -110,6 +110,17 @@ anta_title: Frequently Asked Questions (FAQ)
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
???+ faq "`__NSCFConstantString initialize` error on OSX"
@ -124,6 +135,40 @@ anta_title: Frequently Asked Questions (FAQ)
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?
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).

View file

@ -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:
```yaml
anta_inventory:
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']
--8<-- "getting-started/inventory.yml"
```
> 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:
```yaml
# Load anta.tests.software
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:
--8<-- "getting-started/catalog.yml"
```
## 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
!!! 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
```bash
anta nrfu \
--username tom \
--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 │
└───────────┴──────────────────────────┴─────────────┴──────────────────┴──────────────────────────────────────────────────────────────────────┴───────────────┘
--8<-- "getting-started/anta_nrfu_table.sh"
--8<-- "getting-started/anta_nrfu_table.output"
```
#### Report in text mode
```bash
$ anta nrfu \
--username tom \
--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)
[...]
--8<-- "getting-started/anta_nrfu_text.sh"
--8<-- "getting-started/anta_nrfu_text.output"
```
#### Report in JSON format
```bash
$ anta nrfu \
--username tom \
--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",
},
[...]
]
--8<-- "getting-started/anta_nrfu_json.sh"
--8<-- "getting-started/anta_nrfu_json.output"
```
You can find more information under the **usage** section of the website
### Basic usage in a Python script
```python

View file

@ -25,9 +25,8 @@ The ANTA package and the cli require some packages that are not part of the Pyth
pip install anta
```
!!! Warning
* This command alone **will not** install the ANTA CLI requirements.
> [!WARNING]
> This command alone **will not** install the ANTA CLI requirements.
### Install ANTA CLI as an application with `pipx`
@ -37,9 +36,8 @@ pip install anta
pipx install anta[cli]
```
!!! Info
Please take the time to read through the installation instructions of `pipx` before getting started.
> [!INFO]
> Please take the time to read through the installation instructions of `pipx` before getting started.
### Install CLI from Pypi server
@ -80,13 +78,13 @@ which anta
/home/tom/.pyenv/shims/anta
```
!!! 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.
> [!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.
```bash
# Check ANTA version
anta --version
anta, version v1.1.0
anta, version v1.2.0
```
## EOS Requirements

View 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