456 lines
14 KiB
Python
456 lines
14 KiB
Python
# 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.
|
|
"""Tests for anta.cli.get.commands."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import filecmp
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import ANY, patch
|
|
|
|
import pytest
|
|
import requests
|
|
from cvprac.cvp_client_errors import CvpApiError
|
|
|
|
from anta.cli._main import anta
|
|
from anta.cli.utils import ExitCode
|
|
|
|
if TYPE_CHECKING:
|
|
from click.testing import CliRunner
|
|
from cvprac.cvp_client import CvpClient
|
|
|
|
DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("cvp_container", "verify_cert", "cv_token_failure", "cvp_connect_failure"),
|
|
[
|
|
pytest.param(None, True, False, False, id="all devices - verify cert"),
|
|
pytest.param(None, True, True, False, id="all devices - fail SSL check"),
|
|
pytest.param(None, False, False, False, id="all devices - do not verify cert"),
|
|
pytest.param("custom_container", False, False, False, id="custom container"),
|
|
pytest.param(None, False, False, True, id="cvp connect failure"),
|
|
],
|
|
)
|
|
def test_from_cvp(
|
|
tmp_path: Path,
|
|
click_runner: CliRunner,
|
|
cvp_container: str | None,
|
|
verify_cert: bool,
|
|
cv_token_failure: bool,
|
|
cvp_connect_failure: bool,
|
|
) -> None:
|
|
# ruff: noqa: C901
|
|
"""Test `anta get from-cvp`.
|
|
|
|
This test verifies that username and password are NOT mandatory to run this command
|
|
"""
|
|
output: Path = tmp_path / "output.yml"
|
|
cli_args = [
|
|
"get",
|
|
"from-cvp",
|
|
"--output",
|
|
str(output),
|
|
"--host",
|
|
"42.42.42.42",
|
|
"--username",
|
|
"anta",
|
|
"--password",
|
|
"anta",
|
|
]
|
|
|
|
if cvp_container is not None:
|
|
cli_args.extend(["--container", cvp_container])
|
|
if not verify_cert:
|
|
cli_args.extend(["--ignore-cert"])
|
|
|
|
def mock_get_cv_token(*_args: str, **_kwargs: str) -> None:
|
|
if cv_token_failure:
|
|
raise requests.exceptions.SSLError
|
|
|
|
def mock_cvp_connect(_self: CvpClient, *_args: str, **_kwargs: str) -> None:
|
|
if cvp_connect_failure:
|
|
raise CvpApiError(msg="mocked CvpApiError")
|
|
|
|
# always get a token
|
|
with (
|
|
patch("anta.cli.get.commands.get_cv_token", autospec=True, side_effect=mock_get_cv_token),
|
|
patch(
|
|
"cvprac.cvp_client.CvpClient.connect",
|
|
autospec=True,
|
|
side_effect=mock_cvp_connect,
|
|
) as mocked_cvp_connect,
|
|
patch("cvprac.cvp_client.CvpApi.get_inventory", autospec=True, return_value=[]) as mocked_get_inventory,
|
|
patch(
|
|
"cvprac.cvp_client.CvpApi.get_devices_in_container",
|
|
autospec=True,
|
|
return_value=[],
|
|
) as mocked_get_devices_in_container,
|
|
):
|
|
result = click_runner.invoke(anta, cli_args)
|
|
|
|
if not cvp_connect_failure and not cv_token_failure:
|
|
assert output.exists()
|
|
|
|
if cv_token_failure:
|
|
assert "Authentication to CloudVison failed" in result.output
|
|
assert result.exit_code == ExitCode.USAGE_ERROR
|
|
return
|
|
|
|
mocked_cvp_connect.assert_called_once()
|
|
|
|
if cvp_connect_failure:
|
|
assert "Error connecting to CloudVision" in result.output
|
|
assert result.exit_code == ExitCode.USAGE_ERROR
|
|
return
|
|
|
|
assert "Connected to CloudVision" in result.output
|
|
if cvp_container is not None:
|
|
mocked_get_devices_in_container.assert_called_once_with(ANY, cvp_container)
|
|
else:
|
|
mocked_get_inventory.assert_called_once()
|
|
assert result.exit_code == ExitCode.OK
|
|
|
|
|
|
def test_from_cvp_os_error(tmp_path: Path, click_runner: CliRunner, caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Test from_cvp when an OSError occurs."""
|
|
output: Path = tmp_path / "output.yml"
|
|
cli_args = ["get", "from-cvp", "--output", str(output), "--host", "42.42.42.42", "--username", "anta", "--password", "anta"]
|
|
|
|
with (
|
|
patch("anta.cli.get.commands.get_cv_token", autospec=True, side_effect=None),
|
|
patch("cvprac.cvp_client.CvpClient.connect", autospec=True, side_effect=None) as mocked_cvp_connect,
|
|
patch("cvprac.cvp_client.CvpApi.get_inventory", autospec=True, return_value=[]) as mocked_get_inventory,
|
|
patch("cvprac.cvp_client.CvpApi.get_devices_in_container", autospec=True, return_value=[]),
|
|
patch("anta.cli.get.utils.Path.open", side_effect=OSError("Permission denied")),
|
|
):
|
|
result = click_runner.invoke(anta, cli_args)
|
|
|
|
mocked_cvp_connect.assert_called_once()
|
|
mocked_get_inventory.assert_called_once()
|
|
assert not output.exists()
|
|
assert "Could not write inventory to path" in caplog.text
|
|
assert result.exit_code == ExitCode.USAGE_ERROR
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("ansible_inventory", "ansible_group", "expected_exit", "expected_log"),
|
|
[
|
|
pytest.param("ansible_inventory.yml", None, ExitCode.OK, None, id="no group"),
|
|
pytest.param("ansible_inventory.yml", "ATD_LEAFS", ExitCode.OK, None, id="group found"),
|
|
pytest.param(
|
|
"ansible_inventory.yml",
|
|
"DUMMY",
|
|
ExitCode.USAGE_ERROR,
|
|
"Group DUMMY not found in Ansible inventory",
|
|
id="group not found",
|
|
),
|
|
pytest.param(
|
|
"empty_ansible_inventory.yml",
|
|
None,
|
|
ExitCode.USAGE_ERROR,
|
|
"is empty",
|
|
id="empty inventory",
|
|
),
|
|
],
|
|
)
|
|
def test_from_ansible(
|
|
tmp_path: Path,
|
|
click_runner: CliRunner,
|
|
ansible_inventory: Path,
|
|
ansible_group: str | None,
|
|
expected_exit: int,
|
|
expected_log: str | None,
|
|
) -> None:
|
|
"""Test `anta get from-ansible`.
|
|
|
|
This test verifies:
|
|
* the parsing of an ansible-inventory
|
|
* the ansible_group functionaliy
|
|
|
|
The output path is ALWAYS set to a non existing file.
|
|
"""
|
|
output: Path = tmp_path / "output.yml"
|
|
ansible_inventory_path = DATA_DIR / ansible_inventory
|
|
# Init cli_args
|
|
cli_args = [
|
|
"get",
|
|
"from-ansible",
|
|
"--output",
|
|
str(output),
|
|
"--ansible-inventory",
|
|
str(ansible_inventory_path),
|
|
]
|
|
|
|
# Set --ansible-group
|
|
if ansible_group is not None:
|
|
cli_args.extend(["--ansible-group", ansible_group])
|
|
|
|
result = click_runner.invoke(anta, cli_args)
|
|
|
|
assert result.exit_code == expected_exit
|
|
|
|
if expected_exit != ExitCode.OK:
|
|
assert expected_log
|
|
assert expected_log in result.output
|
|
else:
|
|
assert output.exists()
|
|
# TODO: check size of generated inventory to validate the group functionality!
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("env_set", "overwrite", "is_tty", "prompt", "expected_exit", "expected_log"),
|
|
[
|
|
pytest.param(
|
|
True,
|
|
False,
|
|
True,
|
|
"y",
|
|
ExitCode.OK,
|
|
"",
|
|
id="no-overwrite-tty-init-prompt-yes",
|
|
),
|
|
pytest.param(
|
|
True,
|
|
False,
|
|
True,
|
|
"N",
|
|
ExitCode.INTERNAL_ERROR,
|
|
"Aborted",
|
|
id="no-overwrite-tty-init-prompt-no",
|
|
),
|
|
pytest.param(
|
|
True,
|
|
False,
|
|
False,
|
|
None,
|
|
ExitCode.USAGE_ERROR,
|
|
"Conversion aborted since destination file is not empty (not running in interactive TTY)",
|
|
id="no-overwrite-no-tty-init",
|
|
),
|
|
pytest.param(False, False, True, None, ExitCode.OK, "", id="no-overwrite-tty-no-init"),
|
|
pytest.param(False, False, False, None, ExitCode.OK, "", id="no-overwrite-no-tty-no-init"),
|
|
pytest.param(True, True, True, None, ExitCode.OK, "", id="overwrite-tty-init"),
|
|
pytest.param(True, True, False, None, ExitCode.OK, "", id="overwrite-no-tty-init"),
|
|
pytest.param(False, True, True, None, ExitCode.OK, "", id="overwrite-tty-no-init"),
|
|
pytest.param(False, True, False, None, ExitCode.OK, "", id="overwrite-no-tty-no-init"),
|
|
],
|
|
)
|
|
def test_from_ansible_overwrite(
|
|
tmp_path: Path,
|
|
click_runner: CliRunner,
|
|
temp_env: dict[str, str | None],
|
|
env_set: bool,
|
|
overwrite: bool,
|
|
is_tty: bool,
|
|
prompt: str | None,
|
|
expected_exit: int,
|
|
expected_log: str | None,
|
|
) -> None:
|
|
"""Test `anta get from-ansible` overwrite mechanism.
|
|
|
|
The test uses a static ansible-inventory and output as these are tested in other functions
|
|
|
|
This test verifies:
|
|
* that overwrite is working as expected with or without init data in the target file
|
|
* that when the target file is not empty and a tty is present, the user is prompt with confirmation
|
|
* Check the behavior when the prompt is filled
|
|
|
|
The initial content of the ANTA inventory is set using init_anta_inventory, if it is None, no inventory is set.
|
|
|
|
* With overwrite True, the expectation is that the from-ansible command succeeds
|
|
* With no init (init_anta_inventory == None), the expectation is also that command succeeds
|
|
"""
|
|
ansible_inventory_path = DATA_DIR / "ansible_inventory.yml"
|
|
expected_anta_inventory_path = DATA_DIR / "expected_anta_inventory.yml"
|
|
tmp_output = tmp_path / "output.yml"
|
|
cli_args = [
|
|
"get",
|
|
"from-ansible",
|
|
"--ansible-inventory",
|
|
str(ansible_inventory_path),
|
|
]
|
|
|
|
if env_set:
|
|
tmp_inv = Path(str(temp_env["ANTA_INVENTORY"]))
|
|
else:
|
|
temp_env["ANTA_INVENTORY"] = None
|
|
tmp_inv = tmp_output
|
|
cli_args.extend(["--output", str(tmp_inv)])
|
|
if overwrite:
|
|
cli_args.append("--overwrite")
|
|
|
|
# Verify initial content is different
|
|
if tmp_inv.exists():
|
|
assert not filecmp.cmp(tmp_inv, expected_anta_inventory_path)
|
|
|
|
with patch("sys.stdin.isatty", return_value=is_tty):
|
|
result = click_runner.invoke(anta, cli_args, env=temp_env, input=prompt)
|
|
|
|
assert result.exit_code == expected_exit
|
|
if expected_exit == ExitCode.OK:
|
|
assert filecmp.cmp(tmp_inv, expected_anta_inventory_path)
|
|
elif expected_exit == ExitCode.INTERNAL_ERROR:
|
|
assert expected_log
|
|
assert expected_log in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("module", "test_name", "short", "count", "expected_output", "expected_exit_code"),
|
|
[
|
|
pytest.param(
|
|
None,
|
|
None,
|
|
False,
|
|
False,
|
|
"VerifyAcctConsoleMethods",
|
|
ExitCode.OK,
|
|
id="Get all tests",
|
|
),
|
|
pytest.param(
|
|
"anta.tests.aaa",
|
|
None,
|
|
False,
|
|
False,
|
|
"VerifyAcctConsoleMethods",
|
|
ExitCode.OK,
|
|
id="Get tests, filter on module",
|
|
),
|
|
pytest.param(
|
|
None,
|
|
"VerifyNTPAssociations",
|
|
False,
|
|
False,
|
|
"VerifyNTPAssociations",
|
|
ExitCode.OK,
|
|
id="Get tests, filter on exact test name",
|
|
),
|
|
pytest.param(
|
|
None,
|
|
"VerifyNTP",
|
|
False,
|
|
False,
|
|
"anta.tests.system",
|
|
ExitCode.OK,
|
|
id="Get tests, filter on included test name",
|
|
),
|
|
pytest.param(
|
|
None,
|
|
"VerifyNTP",
|
|
True,
|
|
False,
|
|
"VerifyNTPAssociations",
|
|
ExitCode.OK,
|
|
id="Get tests --short",
|
|
),
|
|
pytest.param(
|
|
"unknown_module",
|
|
None,
|
|
True,
|
|
False,
|
|
"Module `unknown_module` was not found!",
|
|
ExitCode.USAGE_ERROR,
|
|
id="Get tests wrong module",
|
|
),
|
|
pytest.param(
|
|
"unknown_module.unknown",
|
|
None,
|
|
True,
|
|
False,
|
|
"Module `unknown_module.unknown` was not found!",
|
|
ExitCode.USAGE_ERROR,
|
|
id="Get tests wrong submodule",
|
|
),
|
|
pytest.param(
|
|
".unknown_module",
|
|
None,
|
|
True,
|
|
False,
|
|
"`anta get tests --module <module>` does not support relative imports",
|
|
ExitCode.USAGE_ERROR,
|
|
id="Use relative module name",
|
|
),
|
|
pytest.param(
|
|
None,
|
|
"VerifySomething",
|
|
True,
|
|
False,
|
|
"No test 'VerifySomething' found in 'anta.tests'",
|
|
ExitCode.OK,
|
|
id="Get tests wrong test name",
|
|
),
|
|
pytest.param(
|
|
"anta.tests.aaa",
|
|
"VerifyNTP",
|
|
True,
|
|
False,
|
|
"No test 'VerifyNTP' found in 'anta.tests.aaa'",
|
|
ExitCode.OK,
|
|
id="Get tests test exists but not in module",
|
|
),
|
|
pytest.param(
|
|
"anta.tests.system",
|
|
"VerifyNTPAssociations",
|
|
False,
|
|
True,
|
|
"There is 1 test available in 'anta.tests.system'.",
|
|
ExitCode.OK,
|
|
id="Get single test count",
|
|
),
|
|
pytest.param(
|
|
"anta.tests.stun",
|
|
None,
|
|
False,
|
|
True,
|
|
"There are 3 tests available in 'anta.tests.stun'",
|
|
ExitCode.OK,
|
|
id="Get multiple test count",
|
|
),
|
|
],
|
|
)
|
|
def test_get_tests(
|
|
click_runner: CliRunner, module: str | None, test_name: str | None, *, short: bool, count: bool, expected_output: str, expected_exit_code: str
|
|
) -> None:
|
|
"""Test `anta get tests`."""
|
|
cli_args = [
|
|
"get",
|
|
"tests",
|
|
]
|
|
if module is not None:
|
|
cli_args.extend(["--module", module])
|
|
|
|
if test_name is not None:
|
|
cli_args.extend(["--test", test_name])
|
|
|
|
if short:
|
|
cli_args.append("--short")
|
|
|
|
if count:
|
|
cli_args.append("--count")
|
|
|
|
result = click_runner.invoke(anta, cli_args)
|
|
|
|
assert result.exit_code == expected_exit_code
|
|
assert expected_output in result.output
|
|
|
|
|
|
def test_get_tests_local_module(click_runner: CliRunner) -> None:
|
|
"""Test injecting CWD in sys.
|
|
|
|
The test overwrite CWD to return this file parents and local_module is located there.
|
|
"""
|
|
cli_args = ["get", "tests", "--module", "local_module"]
|
|
|
|
cwd = Path.cwd()
|
|
local_module_parent_path = Path(__file__).parent
|
|
with patch("anta.cli.get.utils.Path.cwd", return_value=local_module_parent_path):
|
|
result = click_runner.invoke(anta, cli_args)
|
|
|
|
assert result.exit_code == ExitCode.OK
|
|
|
|
# In the rare case where people would be running `pytest .` in this directory
|
|
if cwd != local_module_parent_path:
|
|
assert "injecting CWD in PYTHONPATH and retrying..." in result.output
|
|
assert "No test found in 'local_module'" in result.output
|