2025-03-17 07:33:51 +01:00
|
|
|
# Copyright (c) 2023-2025 Arista Networks, Inc.
|
2025-02-05 11:32:35 +01:00
|
|
|
# Use of this source code is governed by the Apache License 2.0
|
|
|
|
# that can be found in the LICENSE file.
|
|
|
|
# pylint: disable = redefined-outer-name
|
2025-02-05 11:39:09 +01:00
|
|
|
"""Click commands to get information from or generate inventories."""
|
|
|
|
|
2025-02-05 11:32:35 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from pathlib import Path
|
2025-02-05 11:39:09 +01:00
|
|
|
from typing import TYPE_CHECKING, Any
|
2025-02-05 11:32:35 +01:00
|
|
|
|
|
|
|
import click
|
2025-02-05 11:54:06 +01:00
|
|
|
import requests
|
2025-02-05 11:32:35 +01:00
|
|
|
from cvprac.cvp_client import CvpClient
|
|
|
|
from cvprac.cvp_client_errors import CvpApiError
|
|
|
|
from rich.pretty import pretty_repr
|
|
|
|
|
|
|
|
from anta.cli.console import console
|
|
|
|
from anta.cli.get.utils import inventory_output_options
|
|
|
|
from anta.cli.utils import ExitCode, inventory_options
|
|
|
|
|
2025-02-05 11:55:22 +01:00
|
|
|
from .utils import create_inventory_from_ansible, create_inventory_from_cvp, explore_package, get_cv_token
|
2025-02-05 11:32:35 +01:00
|
|
|
|
2025-02-05 11:39:09 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from anta.inventory import AntaInventory
|
|
|
|
|
2025-02-05 11:32:35 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
@click.command
|
|
|
|
@click.pass_context
|
|
|
|
@inventory_output_options
|
|
|
|
@click.option("--host", "-host", help="CloudVision instance FQDN or IP", type=str, required=True)
|
|
|
|
@click.option("--username", "-u", help="CloudVision username", type=str, required=True)
|
|
|
|
@click.option("--password", "-p", help="CloudVision password", type=str, required=True)
|
|
|
|
@click.option("--container", "-c", help="CloudVision container where devices are configured", type=str)
|
2025-02-05 11:54:06 +01:00
|
|
|
@click.option(
|
|
|
|
"--ignore-cert",
|
|
|
|
help="Ignore verifying the SSL certificate when connecting to CloudVision",
|
|
|
|
show_envvar=True,
|
|
|
|
is_flag=True,
|
|
|
|
default=False,
|
|
|
|
)
|
|
|
|
def from_cvp(ctx: click.Context, output: Path, host: str, username: str, password: str, container: str | None, *, ignore_cert: bool) -> None:
|
|
|
|
"""Build ANTA inventory from CloudVision.
|
2025-02-05 11:32:35 +01:00
|
|
|
|
2025-02-05 11:54:06 +01:00
|
|
|
NOTE: Only username/password authentication is supported for on-premises CloudVision instances.
|
|
|
|
Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.
|
2025-02-05 11:32:35 +01:00
|
|
|
"""
|
2025-02-05 11:54:06 +01:00
|
|
|
# TODO: - Handle get_cv_token, get_inventory and get_devices_in_container failures.
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.info("Getting authentication token for user '%s' from CloudVision instance '%s'", username, host)
|
2025-02-05 11:54:06 +01:00
|
|
|
try:
|
|
|
|
token = get_cv_token(cvp_ip=host, cvp_username=username, cvp_password=password, verify_cert=not ignore_cert)
|
|
|
|
except requests.exceptions.SSLError as error:
|
|
|
|
logger.error("Authentication to CloudVison failed: %s.", error)
|
|
|
|
ctx.exit(ExitCode.USAGE_ERROR)
|
2025-02-05 11:32:35 +01:00
|
|
|
|
|
|
|
clnt = CvpClient()
|
|
|
|
try:
|
|
|
|
clnt.connect(nodes=[host], username="", password="", api_token=token)
|
|
|
|
except CvpApiError as error:
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.error("Error connecting to CloudVision: %s", error)
|
2025-02-05 11:32:35 +01:00
|
|
|
ctx.exit(ExitCode.USAGE_ERROR)
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.info("Connected to CloudVision instance '%s'", host)
|
2025-02-05 11:32:35 +01:00
|
|
|
|
|
|
|
cvp_inventory = None
|
|
|
|
if container is None:
|
|
|
|
# Get a list of all devices
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.info("Getting full inventory from CloudVision instance '%s'", host)
|
2025-02-05 11:32:35 +01:00
|
|
|
cvp_inventory = clnt.api.get_inventory()
|
|
|
|
else:
|
|
|
|
# Get devices under a container
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.info("Getting inventory for container %s from CloudVision instance '%s'", container, host)
|
2025-02-05 11:32:35 +01:00
|
|
|
cvp_inventory = clnt.api.get_devices_in_container(container)
|
2025-02-05 11:55:22 +01:00
|
|
|
try:
|
|
|
|
create_inventory_from_cvp(cvp_inventory, output)
|
|
|
|
except OSError as e:
|
|
|
|
logger.error(str(e))
|
|
|
|
ctx.exit(ExitCode.USAGE_ERROR)
|
2025-02-05 11:32:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
@click.command
|
|
|
|
@click.pass_context
|
|
|
|
@inventory_output_options
|
|
|
|
@click.option("--ansible-group", "-g", help="Ansible group to filter", type=str, default="all")
|
|
|
|
@click.option(
|
|
|
|
"--ansible-inventory",
|
|
|
|
help="Path to your ansible inventory file to read",
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True, path_type=Path),
|
|
|
|
required=True,
|
|
|
|
)
|
|
|
|
def from_ansible(ctx: click.Context, output: Path, ansible_group: str, ansible_inventory: Path) -> None:
|
2025-02-05 11:39:50 +01:00
|
|
|
"""Build ANTA inventory from an ansible inventory YAML file.
|
|
|
|
|
|
|
|
NOTE: This command does not support inline vaulted variables. Make sure to comment them out.
|
|
|
|
|
|
|
|
"""
|
2025-02-05 11:39:09 +01:00
|
|
|
logger.info("Building inventory from ansible file '%s'", ansible_inventory)
|
2025-02-05 11:32:35 +01:00
|
|
|
try:
|
|
|
|
create_inventory_from_ansible(
|
|
|
|
inventory=ansible_inventory,
|
|
|
|
output=output,
|
|
|
|
ansible_group=ansible_group,
|
|
|
|
)
|
2025-02-05 11:55:22 +01:00
|
|
|
except (ValueError, OSError) as e:
|
2025-02-05 11:32:35 +01:00
|
|
|
logger.error(str(e))
|
|
|
|
ctx.exit(ExitCode.USAGE_ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
@click.command
|
|
|
|
@inventory_options
|
|
|
|
@click.option("--connected/--not-connected", help="Display inventory after connection has been created", default=False, required=False)
|
2025-02-05 11:39:09 +01:00
|
|
|
def inventory(inventory: AntaInventory, tags: set[str] | None, *, connected: bool) -> None:
|
2025-02-05 11:32:35 +01:00
|
|
|
"""Show inventory loaded in ANTA."""
|
2025-02-05 11:39:09 +01:00
|
|
|
# TODO: @gmuloc - tags come from context - we cannot have everything..
|
|
|
|
# ruff: noqa: ARG001
|
|
|
|
logger.debug("Requesting devices for tags: %s", tags)
|
2025-02-05 11:32:35 +01:00
|
|
|
console.print("Current inventory content is:", style="white on blue")
|
|
|
|
|
|
|
|
if connected:
|
|
|
|
asyncio.run(inventory.connect_inventory())
|
|
|
|
|
|
|
|
inventory_result = inventory.get_inventory(tags=tags)
|
|
|
|
console.print(pretty_repr(inventory_result))
|
|
|
|
|
|
|
|
|
|
|
|
@click.command
|
|
|
|
@inventory_options
|
2025-02-05 11:39:09 +01:00
|
|
|
def tags(inventory: AntaInventory, **kwargs: Any) -> None:
|
2025-02-05 11:32:35 +01:00
|
|
|
"""Get list of configured tags in user inventory."""
|
2025-02-05 11:39:09 +01:00
|
|
|
tags: set[str] = set()
|
2025-02-05 11:32:35 +01:00
|
|
|
for device in inventory.values():
|
2025-02-05 11:39:09 +01:00
|
|
|
tags.update(device.tags)
|
2025-02-05 11:32:35 +01:00
|
|
|
console.print("Tags found:")
|
2025-02-05 11:39:09 +01:00
|
|
|
console.print_json(json.dumps(sorted(tags), indent=2))
|
2025-02-05 11:55:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
@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)
|