Adding upstream version 1.1.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 11:54:23 +01:00
parent f13b7abbd8
commit 77504588ab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
196 changed files with 10121 additions and 3780 deletions

View file

@ -10,129 +10,51 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
import httpx
import pytest
from asyncssh import SSHClientConnection, SSHClientConnectionOptions
from httpx import ConnectError, HTTPError
from rich import print as rprint
import asynceapi
from anta.device import AntaDevice, AsyncEOSDevice
from anta.models import AntaCommand
from tests.lib.fixture import COMMAND_OUTPUT
from tests.lib.utils import generate_test_ids_list
from asynceapi import EapiCommandError
from tests.units.conftest import COMMAND_OUTPUT
if TYPE_CHECKING:
from _pytest.mark.structures import ParameterSet
INIT_DATA: list[dict[str, Any]] = [
{
"name": "no name, no port",
"device": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
},
"expected": {"name": "42.42.42.42"},
},
{
"name": "no name, port",
"device": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
"port": 666,
},
"expected": {"name": "42.42.42.42:666"},
},
{
"name": "name",
"device": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
"name": "test.anta.ninja",
"disable_cache": True,
},
"expected": {"name": "test.anta.ninja"},
},
{
"name": "insecure",
"device": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
"name": "test.anta.ninja",
"insecure": True,
},
"expected": {"name": "test.anta.ninja"},
},
INIT_PARAMS: list[ParameterSet] = [
pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta"}, {"name": "42.42.42.42"}, id="no name, no port"),
pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta", "port": 666}, {"name": "42.42.42.42:666"}, id="no name, port"),
pytest.param(
{"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "test.anta.ninja", "disable_cache": True}, {"name": "test.anta.ninja"}, id="name"
),
pytest.param(
{"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "test.anta.ninja", "insecure": True}, {"name": "test.anta.ninja"}, id="insecure"
),
]
EQUALITY_DATA: list[dict[str, Any]] = [
{
"name": "equal",
"device1": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
},
"device2": {
"host": "42.42.42.42",
"username": "anta",
"password": "blah",
},
"expected": True,
},
{
"name": "equals-name",
"device1": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
"name": "device1",
},
"device2": {
"host": "42.42.42.42",
"username": "plop",
"password": "anta",
"name": "device2",
},
"expected": True,
},
{
"name": "not-equal-port",
"device1": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
},
"device2": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
"port": 666,
},
"expected": False,
},
{
"name": "not-equal-host",
"device1": {
"host": "42.42.42.41",
"username": "anta",
"password": "anta",
},
"device2": {
"host": "42.42.42.42",
"username": "anta",
"password": "anta",
},
"expected": False,
},
EQUALITY_PARAMS: list[ParameterSet] = [
pytest.param({"host": "42.42.42.42", "username": "anta", "password": "anta"}, {"host": "42.42.42.42", "username": "anta", "password": "blah"}, True, id="equal"),
pytest.param(
{"host": "42.42.42.42", "username": "anta", "password": "anta", "name": "device1"},
{"host": "42.42.42.42", "username": "plop", "password": "anta", "name": "device2"},
True,
id="equals-name",
),
pytest.param(
{"host": "42.42.42.42", "username": "anta", "password": "anta"},
{"host": "42.42.42.42", "username": "anta", "password": "anta", "port": 666},
False,
id="not-equal-port",
),
pytest.param(
{"host": "42.42.42.41", "username": "anta", "password": "anta"}, {"host": "42.42.42.42", "username": "anta", "password": "anta"}, False, id="not-equal-host"
),
]
ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
{
"name": "command",
"device": {},
"command": {
ASYNCEAPI_COLLECT_PARAMS: list[ParameterSet] = [
pytest.param(
{},
{
"command": "show version",
"patch_kwargs": {
"return_value": [
@ -155,11 +77,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memTotal": 8099732,
"memFree": 4989568,
"isIntlVersion": False,
},
],
}
]
},
},
"expected": {
{
"output": {
"mfgName": "Arista",
"modelName": "DCS-7280CR3-32P4-F",
@ -182,11 +104,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
},
"errors": [],
},
},
{
"name": "enable",
"device": {"enable": True},
"command": {
id="command",
),
pytest.param(
{"enable": True},
{
"command": "show version",
"patch_kwargs": {
"return_value": [
@ -211,10 +133,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
],
]
},
},
"expected": {
{
"output": {
"mfgName": "Arista",
"modelName": "DCS-7280CR3-32P4-F",
@ -237,11 +159,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
},
"errors": [],
},
},
{
"name": "enable password",
"device": {"enable": True, "enable_password": "anta"},
"command": {
id="enable",
),
pytest.param(
{"enable": True, "enable_password": "anta"},
{
"command": "show version",
"patch_kwargs": {
"return_value": [
@ -266,10 +188,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
],
]
},
},
"expected": {
{
"output": {
"mfgName": "Arista",
"modelName": "DCS-7280CR3-32P4-F",
@ -292,11 +214,11 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
},
"errors": [],
},
},
{
"name": "revision",
"device": {},
"command": {
id="enable password",
),
pytest.param(
{},
{
"command": "show version",
"revision": 3,
"patch_kwargs": {
@ -322,10 +244,10 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
},
],
]
},
},
"expected": {
{
"output": {
"mfgName": "Arista",
"modelName": "DCS-7280CR3-32P4-F",
@ -348,77 +270,47 @@ ASYNCEAPI_COLLECT_DATA: list[dict[str, Any]] = [
},
"errors": [],
},
},
{
"name": "asynceapi.EapiCommandError",
"device": {},
"command": {
id="revision",
),
pytest.param(
{},
{
"command": "show version",
"patch_kwargs": {
"side_effect": asynceapi.EapiCommandError(
"side_effect": EapiCommandError(
passed=[],
failed="show version",
errors=["Authorization denied for command 'show version'"],
errmsg="Invalid command",
not_exec=[],
),
)
},
},
"expected": {"output": None, "errors": ["Authorization denied for command 'show version'"]},
},
{
"name": "httpx.HTTPError",
"device": {},
"command": {
"command": "show version",
"patch_kwargs": {"side_effect": httpx.HTTPError(message="404")},
},
"expected": {"output": None, "errors": ["HTTPError: 404"]},
},
{
"name": "httpx.ConnectError",
"device": {},
"command": {
"command": "show version",
"patch_kwargs": {"side_effect": httpx.ConnectError(message="Cannot open port")},
},
"expected": {"output": None, "errors": ["ConnectError: Cannot open port"]},
},
{"output": None, "errors": ["Authorization denied for command 'show version'"]},
id="asynceapi.EapiCommandError",
),
pytest.param(
{},
{"command": "show version", "patch_kwargs": {"side_effect": HTTPError("404")}},
{"output": None, "errors": ["HTTPError: 404"]},
id="httpx.HTTPError",
),
pytest.param(
{},
{"command": "show version", "patch_kwargs": {"side_effect": ConnectError("Cannot open port")}},
{"output": None, "errors": ["ConnectError: Cannot open port"]},
id="httpx.ConnectError",
),
]
ASYNCEAPI_COPY_DATA: list[dict[str, Any]] = [
{
"name": "from",
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
"destination": Path(),
"direction": "from",
},
},
{
"name": "to",
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
"destination": Path(),
"direction": "to",
},
},
{
"name": "wrong",
"device": {},
"copy": {
"sources": [Path("/mnt/flash"), Path("/var/log/agents")],
"destination": Path(),
"direction": "wrong",
},
},
ASYNCEAPI_COPY_PARAMS: list[ParameterSet] = [
pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "from"}, id="from"),
pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "to"}, id="to"),
pytest.param({}, {"sources": [Path("/mnt/flash"), Path("/var/log/agents")], "destination": Path(), "direction": "wrong"}, id="wrong"),
]
REFRESH_DATA: list[dict[str, Any]] = [
{
"name": "established",
"device": {},
"patch_kwargs": (
REFRESH_PARAMS: list[ParameterSet] = [
pytest.param(
{},
(
{"return_value": True},
{
"return_value": [
@ -442,15 +334,15 @@ REFRESH_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
}
],
]
},
),
"expected": {"is_online": True, "established": True, "hw_model": "DCS-7280CR3-32P4-F"},
},
{
"name": "is not online",
"device": {},
"patch_kwargs": (
{"is_online": True, "established": True, "hw_model": "DCS-7280CR3-32P4-F"},
id="established",
),
pytest.param(
{},
(
{"return_value": False},
{
"return_value": {
@ -472,15 +364,15 @@ REFRESH_DATA: list[dict[str, Any]] = [
"memTotal": 8099732,
"memFree": 4989568,
"isIntlVersion": False,
},
}
},
),
"expected": {"is_online": False, "established": False, "hw_model": None},
},
{
"name": "cannot parse command",
"device": {},
"patch_kwargs": (
{"is_online": False, "established": False, "hw_model": None},
id="is not online",
),
pytest.param(
{},
(
{"return_value": True},
{
"return_value": [
@ -503,108 +395,87 @@ REFRESH_DATA: list[dict[str, Any]] = [
"memFree": 4989568,
"isIntlVersion": False,
}
],
]
},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
},
{
"name": "asynceapi.EapiCommandError",
"device": {},
"patch_kwargs": (
{"is_online": True, "established": False, "hw_model": None},
id="cannot parse command",
),
pytest.param(
{},
(
{"return_value": True},
{
"side_effect": asynceapi.EapiCommandError(
"side_effect": EapiCommandError(
passed=[],
failed="show version",
errors=["Authorization denied for command 'show version'"],
errmsg="Invalid command",
not_exec=[],
),
)
},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
},
{
"name": "httpx.HTTPError",
"device": {},
"patch_kwargs": (
{"is_online": True, "established": False, "hw_model": None},
id="asynceapi.EapiCommandError",
),
pytest.param(
{},
({"return_value": True}, {"side_effect": HTTPError("404")}),
{"is_online": True, "established": False, "hw_model": None},
id="httpx.HTTPError",
),
pytest.param(
{},
({"return_value": True}, {"side_effect": ConnectError("Cannot open port")}),
{"is_online": True, "established": False, "hw_model": None},
id="httpx.ConnectError",
),
pytest.param(
{},
(
{"return_value": True},
{"side_effect": httpx.HTTPError(message="404")},
{
"return_value": [
{
"mfgName": "Arista",
"modelName": "",
}
]
},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
},
{
"name": "httpx.ConnectError",
"device": {},
"patch_kwargs": (
{"return_value": True},
{"side_effect": httpx.ConnectError(message="Cannot open port")},
),
"expected": {"is_online": True, "established": False, "hw_model": None},
},
{"is_online": True, "established": False, "hw_model": ""},
id="modelName empty string",
),
]
COLLECT_DATA: list[dict[str, Any]] = [
{
"name": "device cache enabled, command cache enabled, no cache hit",
"device": {"disable_cache": False},
"command": {
"command": "show version",
"use_cache": True,
},
"expected": {"cache_hit": False},
},
{
"name": "device cache enabled, command cache enabled, cache hit",
"device": {"disable_cache": False},
"command": {
"command": "show version",
"use_cache": True,
},
"expected": {"cache_hit": True},
},
{
"name": "device cache disabled, command cache enabled",
"device": {"disable_cache": True},
"command": {
"command": "show version",
"use_cache": True,
},
"expected": {},
},
{
"name": "device cache enabled, command cache disabled, cache has command",
"device": {"disable_cache": False},
"command": {
"command": "show version",
"use_cache": False,
},
"expected": {"cache_hit": True},
},
{
"name": "device cache enabled, command cache disabled, cache does not have data",
"device": {
"disable_cache": False,
},
"command": {
"command": "show version",
"use_cache": False,
},
"expected": {"cache_hit": False},
},
{
"name": "device cache disabled, command cache disabled",
"device": {
"disable_cache": True,
},
"command": {
"command": "show version",
"use_cache": False,
},
"expected": {},
},
COLLECT_PARAMS: list[ParameterSet] = [
pytest.param(
{"disable_cache": False},
{"command": "show version", "use_cache": True},
{"cache_hit": False},
id="device cache enabled, command cache enabled, no cache hit",
),
pytest.param(
{"disable_cache": False},
{"command": "show version", "use_cache": True},
{"cache_hit": True},
id="device cache enabled, command cache enabled, cache hit",
),
pytest.param({"disable_cache": True}, {"command": "show version", "use_cache": True}, {}, id="device cache disabled, command cache enabled"),
pytest.param(
{"disable_cache": False},
{"command": "show version", "use_cache": False},
{"cache_hit": True},
id="device cache enabled, command cache disabled, cache has command",
),
pytest.param(
{"disable_cache": False},
{"command": "show version", "use_cache": False},
{"cache_hit": False},
id="device cache enabled, command cache disabled, cache does not have data",
),
pytest.param({"disable_cache": True}, {"command": "show version", "use_cache": False}, {}, id="device cache disabled, command cache disabled"),
]
CACHE_STATS_DATA: list[ParameterSet] = [
CACHE_STATS_PARAMS: list[ParameterSet] = [
pytest.param({"disable_cache": False}, {"total_commands_sent": 0, "cache_hits": 0, "cache_hit_ratio": "0.00%"}, id="with_cache"),
pytest.param({"disable_cache": True}, None, id="without_cache"),
]
@ -613,48 +484,42 @@ CACHE_STATS_DATA: list[ParameterSet] = [
class TestAntaDevice:
"""Test for anta.device.AntaDevice Abstract class."""
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("device", "command_data", "expected_data"),
((d["device"], d["command"], d["expected"]) for d in COLLECT_DATA),
indirect=["device"],
ids=generate_test_ids_list(COLLECT_DATA),
)
async def test_collect(self, device: AntaDevice, command_data: dict[str, Any], expected_data: dict[str, Any]) -> None:
@pytest.mark.parametrize(("device", "command", "expected"), COLLECT_PARAMS, indirect=["device"])
async def test_collect(self, device: AntaDevice, command: dict[str, Any], expected: dict[str, Any]) -> None:
"""Test AntaDevice.collect behavior."""
command = AntaCommand(command=command_data["command"], use_cache=command_data["use_cache"])
cmd = AntaCommand(command=command["command"], use_cache=command["use_cache"])
# Dummy output for cache hit
cached_output = "cached_value"
if device.cache is not None and expected_data["cache_hit"] is True:
await device.cache.set(command.uid, cached_output)
if device.cache is not None and expected["cache_hit"] is True:
await device.cache.set(cmd.uid, cached_output)
await device.collect(command)
await device.collect(cmd)
if device.cache is not None: # device_cache is enabled
current_cached_data = await device.cache.get(command.uid)
if command.use_cache is True: # command is allowed to use cache
if expected_data["cache_hit"] is True:
assert command.output == cached_output
current_cached_data = await device.cache.get(cmd.uid)
if cmd.use_cache is True: # command is allowed to use cache
if expected["cache_hit"] is True:
assert cmd.output == cached_output
assert current_cached_data == cached_output
assert device.cache.hit_miss_ratio["hits"] == 2
else:
assert command.output == COMMAND_OUTPUT
assert cmd.output == COMMAND_OUTPUT
assert current_cached_data == COMMAND_OUTPUT
assert device.cache.hit_miss_ratio["hits"] == 1
else: # command is not allowed to use cache
device._collect.assert_called_once_with(command=command, collection_id=None) # type: ignore[attr-defined] # pylint: disable=protected-access
assert command.output == COMMAND_OUTPUT
if expected_data["cache_hit"] is True:
device._collect.assert_called_once_with(command=cmd, collection_id=None) # type: ignore[attr-defined]
assert cmd.output == COMMAND_OUTPUT
if expected["cache_hit"] is True:
assert current_cached_data == cached_output
else:
assert current_cached_data is None
else: # device is disabled
assert device.cache is None
device._collect.assert_called_once_with(command=command, collection_id=None) # type: ignore[attr-defined] # pylint: disable=protected-access
device._collect.assert_called_once_with(command=cmd, collection_id=None) # type: ignore[attr-defined]
@pytest.mark.parametrize(("device", "expected"), CACHE_STATS_DATA, indirect=["device"])
@pytest.mark.parametrize(("device", "expected"), CACHE_STATS_PARAMS, indirect=["device"])
def test_cache_statistics(self, device: AntaDevice, expected: dict[str, Any] | None) -> None:
"""Verify that when cache statistics attribute does not exist.
@ -666,42 +531,39 @@ class TestAntaDevice:
class TestAsyncEOSDevice:
"""Test for anta.device.AsyncEOSDevice."""
@pytest.mark.parametrize("data", INIT_DATA, ids=generate_test_ids_list(INIT_DATA))
def test__init__(self, data: dict[str, Any]) -> None:
@pytest.mark.parametrize(("device", "expected"), INIT_PARAMS)
def test__init__(self, device: dict[str, Any], expected: dict[str, Any]) -> None:
"""Test the AsyncEOSDevice constructor."""
device = AsyncEOSDevice(**data["device"])
dev = AsyncEOSDevice(**device)
assert device.name == data["expected"]["name"]
if data["device"].get("disable_cache") is True:
assert device.cache is None
assert device.cache_locks is None
assert dev.name == expected["name"]
if device.get("disable_cache") is True:
assert dev.cache is None
assert dev.cache_locks is None
else: # False or None
assert device.cache is not None
assert device.cache_locks is not None
hash(device)
assert dev.cache is not None
assert dev.cache_locks is not None
hash(dev)
with patch("anta.device.__DEBUG__", new=True):
rprint(device)
rprint(dev)
@pytest.mark.parametrize("data", EQUALITY_DATA, ids=generate_test_ids_list(EQUALITY_DATA))
def test__eq(self, data: dict[str, Any]) -> None:
@pytest.mark.parametrize(("device1", "device2", "expected"), EQUALITY_PARAMS)
def test__eq(self, device1: dict[str, Any], device2: dict[str, Any], expected: bool) -> None:
"""Test the AsyncEOSDevice equality."""
device1 = AsyncEOSDevice(**data["device1"])
device2 = AsyncEOSDevice(**data["device2"])
if data["expected"]:
assert device1 == device2
dev1 = AsyncEOSDevice(**device1)
dev2 = AsyncEOSDevice(**device2)
if expected:
assert dev1 == dev2
else:
assert device1 != device2
assert dev1 != dev2
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("async_device", "patch_kwargs", "expected"),
((d["device"], d["patch_kwargs"], d["expected"]) for d in REFRESH_DATA),
ids=generate_test_ids_list(REFRESH_DATA),
REFRESH_PARAMS,
indirect=["async_device"],
)
async def test_refresh(self, async_device: AsyncEOSDevice, patch_kwargs: list[dict[str, Any]], expected: dict[str, Any]) -> None:
# pylint: disable=protected-access
"""Test AsyncEOSDevice.refresh()."""
with patch.object(async_device._session, "check_connection", **patch_kwargs[0]), patch.object(async_device._session, "cli", **patch_kwargs[1]):
await async_device.refresh()
@ -712,15 +574,12 @@ class TestAsyncEOSDevice:
assert async_device.established == expected["established"]
assert async_device.hw_model == expected["hw_model"]
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("async_device", "command", "expected"),
((d["device"], d["command"], d["expected"]) for d in ASYNCEAPI_COLLECT_DATA),
ids=generate_test_ids_list(ASYNCEAPI_COLLECT_DATA),
ASYNCEAPI_COLLECT_PARAMS,
indirect=["async_device"],
)
async def test__collect(self, async_device: AsyncEOSDevice, command: dict[str, Any], expected: dict[str, Any]) -> None:
# pylint: disable=protected-access
"""Test AsyncEOSDevice._collect()."""
cmd = AntaCommand(command=command["command"], revision=command["revision"]) if "revision" in command else AntaCommand(command=command["command"])
with patch.object(async_device._session, "cli", **command["patch_kwargs"]):
@ -741,15 +600,13 @@ class TestAsyncEOSDevice:
commands.append({"cmd": cmd.command, "revision": cmd.revision})
else:
commands.append({"cmd": cmd.command})
async_device._session.cli.assert_called_once_with(commands=commands, ofmt=cmd.ofmt, version=cmd.version, req_id=f"ANTA-{collection_id}-{id(cmd)}") # type: ignore[attr-defined] # asynceapi.Device.cli is patched # pylint: disable=line-too-long
async_device._session.cli.assert_called_once_with(commands=commands, ofmt=cmd.ofmt, version=cmd.version, req_id=f"ANTA-{collection_id}-{id(cmd)}") # type: ignore[attr-defined] # asynceapi.Device.cli is patched
assert cmd.output == expected["output"]
assert cmd.errors == expected["errors"]
@pytest.mark.asyncio()
@pytest.mark.parametrize(
("async_device", "copy"),
((d["device"], d["copy"]) for d in ASYNCEAPI_COPY_DATA),
ids=generate_test_ids_list(ASYNCEAPI_COPY_DATA),
ASYNCEAPI_COPY_PARAMS,
indirect=["async_device"],
)
async def test_copy(self, async_device: AsyncEOSDevice, copy: dict[str, Any]) -> None: