diff options
Diffstat (limited to 'anta/cli/utils.py')
-rw-r--r-- | anta/cli/utils.py | 125 |
1 files changed, 75 insertions, 50 deletions
diff --git a/anta/cli/utils.py b/anta/cli/utils.py index 97a0862..53a9719 100644 --- a/anta/cli/utils.py +++ b/anta/cli/utils.py @@ -1,16 +1,15 @@ # 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. -""" -Utils functions to use with anta.cli module. -""" +"""Utils functions to use with anta.cli module.""" + from __future__ import annotations import enum import functools import logging from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable import click from pydantic import ValidationError @@ -18,7 +17,7 @@ from yaml import YAMLError from anta.catalog import AntaCatalog from anta.inventory import AntaInventory -from anta.inventory.exceptions import InventoryIncorrectSchema, InventoryRootKeyError +from anta.inventory.exceptions import InventoryIncorrectSchemaError, InventoryRootKeyError if TYPE_CHECKING: from click import Option @@ -27,10 +26,7 @@ logger = logging.getLogger(__name__) class ExitCode(enum.IntEnum): - """ - Encodes the valid exit codes by anta - inspired from pytest - """ + """Encodes the valid exit codes by anta inspired from pytest.""" # Tests passed. OK = 0 @@ -44,19 +40,18 @@ class ExitCode(enum.IntEnum): TESTS_FAILED = 4 -def parse_tags(ctx: click.Context, param: Option, value: str) -> list[str] | None: +def parse_tags(ctx: click.Context, param: Option, value: str | None) -> set[str] | None: # pylint: disable=unused-argument - """ - Click option callback to parse an ANTA inventory tags - """ + # ruff: noqa: ARG001 + """Click option callback to parse an ANTA inventory tags.""" if value is not None: - return value.split(",") if "," in value else [value] + return set(value.split(",")) if "," in value else {value} return None def exit_with_code(ctx: click.Context) -> None: - """ - Exit the Click application with an exit code. + """Exit the Click application with an exit code. + This function determines the global test status to be either `unset`, `skipped`, `success` or `error` from the `ResultManger` instance. If flag `ignore_error` is set, the `error` status will be ignored in all the tests. @@ -64,10 +59,12 @@ def exit_with_code(ctx: click.Context) -> None: Exit the application with the following exit code: * 0 if `ignore_status` is `True` or global test status is `unset`, `skipped` or `success` * 1 if status is `failure` - * 2 if status is `error` + * 2 if status is `error`. Args: + ---- ctx: Click Context + """ if ctx.obj.get("ignore_status"): ctx.exit(ExitCode.OK) @@ -83,18 +80,19 @@ def exit_with_code(ctx: click.Context) -> None: ctx.exit(ExitCode.TESTS_ERROR) logger.error("Please gather logs and open an issue on Github.") - raise ValueError(f"Unknown status returned by the ResultManager: {status}. Please gather logs and open an issue on Github.") + msg = f"Unknown status returned by the ResultManager: {status}. Please gather logs and open an issue on Github." + raise ValueError(msg) class AliasedGroup(click.Group): - """ - Implements a subclass of Group that accepts a prefix for a command. + """Implements a subclass of Group that accepts a prefix for a command. + If there were a command called push, it would accept pus as an alias (so long as it was unique) - From Click documentation + From Click documentation. """ def get_command(self, ctx: click.Context, cmd_name: str) -> Any: - """Todo: document code""" + """Todo: document code.""" rv = click.Group.get_command(self, ctx, cmd_name) if rv is not None: return rv @@ -107,15 +105,16 @@ class AliasedGroup(click.Group): return None def resolve_command(self, ctx: click.Context, args: Any) -> Any: - """Todo: document code""" + """Todo: document code.""" # always return the full command name _, cmd, args = super().resolve_command(ctx, args) - return cmd.name, cmd, args # type: ignore + if not cmd: + return None, None, None + return cmd.name, cmd, args -# TODO: check code of click.pass_context that raise mypy errors for types and adapt this decorator -def inventory_options(f: Any) -> Any: - """Click common options when requiring an inventory to interact with devices""" +def inventory_options(f: Callable[..., Any]) -> Callable[..., Any]: + """Click common options when requiring an inventory to interact with devices.""" @click.option( "--username", @@ -159,26 +158,34 @@ def inventory_options(f: Any) -> Any: ) @click.option( "--timeout", - help="Global connection timeout", - default=30, + help="Global API timeout. This value will be used for all devices.", + default=30.0, show_envvar=True, envvar="ANTA_TIMEOUT", show_default=True, ) @click.option( "--insecure", - help="Disable SSH Host Key validation", + help="Disable SSH Host Key validation.", default=False, show_envvar=True, envvar="ANTA_INSECURE", is_flag=True, show_default=True, ) - @click.option("--disable-cache", help="Disable cache globally", show_envvar=True, envvar="ANTA_DISABLE_CACHE", show_default=True, is_flag=True, default=False) + @click.option( + "--disable-cache", + help="Disable cache globally.", + show_envvar=True, + envvar="ANTA_DISABLE_CACHE", + show_default=True, + is_flag=True, + default=False, + ) @click.option( "--inventory", "-i", - help="Path to the inventory YAML file", + help="Path to the inventory YAML file.", envvar="ANTA_INVENTORY", show_envvar=True, required=True, @@ -186,8 +193,7 @@ def inventory_options(f: Any) -> Any: ) @click.option( "--tags", - "-t", - help="List of tags using comma as separator: tag1,tag2,tag3", + help="List of tags using comma as separator: tag1,tag2,tag3.", show_envvar=True, envvar="ANTA_TAGS", type=str, @@ -200,13 +206,13 @@ def inventory_options(f: Any) -> Any: ctx: click.Context, *args: tuple[Any], inventory: Path, - tags: list[str] | None, + tags: set[str] | None, username: str, password: str | None, enable_password: str | None, enable: bool, prompt: bool, - timeout: int, + timeout: float, insecure: bool, disable_cache: bool, **kwargs: dict[str, Any], @@ -218,17 +224,25 @@ def inventory_options(f: Any) -> Any: if prompt: # User asked for a password prompt if password is None: - password = click.prompt("Please enter a password to connect to EOS", type=str, hide_input=True, confirmation_prompt=True) - if enable: - if enable_password is None: - if click.confirm("Is a password required to enter EOS privileged EXEC mode?"): - enable_password = click.prompt( - "Please enter a password to enter EOS privileged EXEC mode", type=str, hide_input=True, confirmation_prompt=True - ) + password = click.prompt( + "Please enter a password to connect to EOS", + type=str, + hide_input=True, + confirmation_prompt=True, + ) + if enable and enable_password is None and click.confirm("Is a password required to enter EOS privileged EXEC mode?"): + enable_password = click.prompt( + "Please enter a password to enter EOS privileged EXEC mode", + type=str, + hide_input=True, + confirmation_prompt=True, + ) if password is None: - raise click.BadParameter("EOS password needs to be provided by using either the '--password' option or the '--prompt' option.") + msg = "EOS password needs to be provided by using either the '--password' option or the '--prompt' option." + raise click.BadParameter(msg) if not enable and enable_password: - raise click.BadParameter("Providing a password to access EOS Privileged EXEC mode requires '--enable' option.") + msg = "Providing a password to access EOS Privileged EXEC mode requires '--enable' option." + raise click.BadParameter(msg) try: i = AntaInventory.parse( filename=inventory, @@ -240,15 +254,15 @@ def inventory_options(f: Any) -> Any: insecure=insecure, disable_cache=disable_cache, ) - except (ValidationError, TypeError, ValueError, YAMLError, OSError, InventoryIncorrectSchema, InventoryRootKeyError): + except (ValidationError, TypeError, ValueError, YAMLError, OSError, InventoryIncorrectSchemaError, InventoryRootKeyError): ctx.exit(ExitCode.USAGE_ERROR) return f(*args, inventory=i, tags=tags, **kwargs) return wrapper -def catalog_options(f: Any) -> Any: - """Click common options when requiring a test catalog to execute ANTA tests""" +def catalog_options(f: Callable[..., Any]) -> Callable[..., Any]: + """Click common options when requiring a test catalog to execute ANTA tests.""" @click.option( "--catalog", @@ -256,12 +270,23 @@ def catalog_options(f: Any) -> Any: envvar="ANTA_CATALOG", show_envvar=True, help="Path to the test catalog YAML file", - type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True, path_type=Path), + type=click.Path( + file_okay=True, + dir_okay=False, + exists=True, + readable=True, + path_type=Path, + ), required=True, ) @click.pass_context @functools.wraps(f) - def wrapper(ctx: click.Context, *args: tuple[Any], catalog: Path, **kwargs: dict[str, Any]) -> Any: + def wrapper( + ctx: click.Context, + *args: tuple[Any], + catalog: Path, + **kwargs: dict[str, Any], + ) -> Any: # If help is invoke somewhere, do not parse catalog if ctx.obj.get("_anta_help"): return f(*args, catalog=None, **kwargs) |