Skip to content

Commit

Permalink
fix(anta.cli)!: Avoid requiring username, password, inventory for the…
Browse files Browse the repository at this point in the history
… get commands (#447)

* Test: Add tests for get from-ansible
* CI: Update pre-commit
* fix `tags` for nrfu foo commands
* add custom type for loglevel
* add error handling for None arguments
* add logs to anta.device
* Cleanup anta.cli
* add logs to anta.inventory
* update unit tests for anta.cli
* fix pallets/click#824
* update unit tests for anta.cli.nrfu
* refactor cli modules
* update anta.cli.check and anta.cli.debug
* update cli unit tests
* update unit tests for anta.cli
* small fix for anta get from-ansible
* refactor wrappers
* consume ignore_status and ignore_error
* remove shebangs
* test revision and version in unit tests
* fix anta nrfu --help
* Test: Fix tox coloring issues
* Test: Adjust pytest default logging level
* Refactor(anta): Make sure test name is not truncated in table
* Test: Set width of CliRunner
* Update documentation
* Add warning about CLI changes in documentation
* change -log option to -l

---------

Co-authored-by: Matthieu Tâche <[email protected]>
  • Loading branch information
gmuloc and mtache authored Dec 7, 2023
1 parent b62f2ac commit 6d4f37f
Show file tree
Hide file tree
Showing 61 changed files with 1,675 additions and 1,192 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ repos:
# - id: ruff

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.7.1
hooks:
- id: mypy
args:
Expand Down
4 changes: 2 additions & 2 deletions anta/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from pydantic.types import ImportString
from yaml import YAMLError, safe_load

from anta.logger import anta_log_exception
from anta.models import AntaTest
from anta.tools.misc import anta_log_exception

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -224,7 +224,7 @@ def parse(filename: str | Path) -> AntaCatalog:
try:
with open(file=filename, mode="r", encoding="UTF-8") as file:
data = safe_load(file)
except (YAMLError, OSError) as e:
except (TypeError, YAMLError, OSError) as e:
message = f"Unable to parse ANTA Test Catalog file '{filename}'"
anta_log_exception(e, message, logger)
raise
Expand Down
190 changes: 26 additions & 164 deletions anta/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,32 @@
# Copyright (c) 2023 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
# coding: utf-8 -*-
"""
ANTA CLI
"""
from __future__ import annotations

import logging
import pathlib
from typing import Any, Literal
import sys

import click

from anta import __version__
from anta.catalog import AntaCatalog
from anta.cli.check import commands as check_commands
from anta.cli.debug import commands as debug_commands
from anta.cli.exec import commands as exec_commands
from anta.cli.get import commands as get_commands
from anta.cli.nrfu import commands as nrfu_commands
from anta.cli.utils import AliasedGroup, IgnoreRequiredWithHelp, parse_catalog, parse_inventory
from anta.logger import setup_logging
from anta.result_manager import ResultManager
from anta import GITHUB_SUGGESTION, __version__
from anta.cli.check import check as check_command
from anta.cli.debug import debug as debug_command
from anta.cli.exec import exec as exec_command
from anta.cli.get import get as get_command
from anta.cli.nrfu import nrfu as nrfu_command
from anta.cli.utils import AliasedGroup, ExitCode
from anta.logger import Log, LogLevel, anta_log_exception, setup_logging

logger = logging.getLogger(__name__)

@click.group(cls=IgnoreRequiredWithHelp)

@click.group(cls=AliasedGroup)
@click.pass_context
@click.version_option(__version__)
@click.option(
"--username",
help="Username to connect to EOS",
show_envvar=True,
required=True,
)
@click.option("--password", help="Password to connect to EOS that must be provided. It can be prompted using '--prompt' option.", show_envvar=True)
@click.option(
"--enable-password",
help="Password to access EOS Privileged EXEC mode. It can be prompted using '--prompt' option. Requires '--enable' option.",
show_envvar=True,
)
@click.option(
"--enable",
help="Some commands may require EOS Privileged EXEC mode. This option tries to access this mode before sending a command to the device.",
default=False,
show_envvar=True,
is_flag=True,
show_default=True,
)
@click.option(
"--prompt",
"-P",
help="Prompt for passwords if they are not provided.",
default=False,
is_flag=True,
show_default=True,
)
@click.option(
"--timeout",
help="Global connection timeout",
default=30,
show_envvar=True,
show_default=True,
)
@click.option(
"--insecure",
help="Disable SSH Host Key validation",
default=False,
show_envvar=True,
is_flag=True,
show_default=True,
)
@click.option(
"--inventory",
"-i",
help="Path to the inventory YAML file",
show_envvar=True,
required=True,
type=click.Path(file_okay=True, dir_okay=False, exists=True, readable=True, path_type=pathlib.Path),
)
@click.option(
"--log-file",
help="Send the logs to a file. If logging level is DEBUG, only INFO or higher will be sent to stdout.",
Expand All @@ -88,122 +36,36 @@
)
@click.option(
"--log-level",
"--log",
"-l",
help="ANTA logging level",
default=logging.getLevelName(logging.INFO),
show_envvar=True,
show_default=True,
type=click.Choice(
[
logging.getLevelName(logging.CRITICAL),
logging.getLevelName(logging.ERROR),
logging.getLevelName(logging.WARNING),
logging.getLevelName(logging.INFO),
logging.getLevelName(logging.DEBUG),
],
[Log.CRITICAL, Log.ERROR, Log.WARNING, Log.INFO, Log.DEBUG],
case_sensitive=False,
),
)
@click.option("--ignore-status", help="Always exit with success", show_envvar=True, is_flag=True, default=False)
@click.option("--ignore-error", help="Only report failures and not errors", show_envvar=True, is_flag=True, default=False)
@click.option("--disable-cache", help="Disable cache globally", show_envvar=True, show_default=True, is_flag=True, default=False)
def anta(
ctx: click.Context, inventory: pathlib.Path, log_level: Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], log_file: pathlib.Path, **kwargs: Any
) -> None:
# pylint: disable=unused-argument
def anta(ctx: click.Context, log_level: LogLevel, log_file: pathlib.Path) -> None:
"""Arista Network Test Automation (ANTA) CLI"""
setup_logging(log_level, log_file)

if not ctx.obj.get("_anta_help"):
if ctx.params.get("prompt"):
# User asked for a password prompt
if ctx.params.get("password") is None:
ctx.params["password"] = click.prompt("Please enter a password to connect to EOS", type=str, hide_input=True, confirmation_prompt=True)
if ctx.params.get("enable"):
if ctx.params.get("enable_password") is None:
if click.confirm("Is a password required to enter EOS privileged EXEC mode?"):
ctx.params["enable_password"] = click.prompt(
"Please enter a password to enter EOS privileged EXEC mode", type=str, hide_input=True, confirmation_prompt=True
)
if ctx.params.get("password") is None:
raise click.BadParameter(
f"EOS password needs to be provided by using either the '{anta.params[2].opts[0]}' option or the '{anta.params[5].opts[0]}' option."
)
if not ctx.params.get("enable") and ctx.params.get("enable_password"):
raise click.BadParameter(f"Providing a password to access EOS Privileged EXEC mode requires '{anta.params[4].opts[0]}' option.")

ctx.ensure_object(dict)
ctx.obj["inventory"] = parse_inventory(ctx, inventory)
ctx.obj["inventory_path"] = ctx.params["inventory"]


@anta.group("nrfu", cls=IgnoreRequiredWithHelp)
@click.pass_context
@click.option(
"--catalog",
"-c",
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),
required=True,
callback=parse_catalog,
)
def _nrfu(ctx: click.Context, catalog: AntaCatalog) -> None:
"""Run NRFU against inventory devices"""
ctx.obj["catalog"] = catalog
ctx.obj["result_manager"] = ResultManager()


@anta.group("check", cls=AliasedGroup)
def _check() -> None:
"""Check commands for building ANTA"""


@anta.group("exec", cls=AliasedGroup)
def _exec() -> None:
"""Execute commands to inventory devices"""


@anta.group("get", cls=AliasedGroup)
def _get() -> None:
"""Get data from/to ANTA"""


@anta.group("debug", cls=AliasedGroup)
def _debug() -> None:
"""Debug commands for building ANTA"""


# Load group commands
# Prefixing with `_` for avoiding the confusion when importing anta.cli.debug.commands as otherwise the debug group has
# a commands attribute.
_check.add_command(check_commands.catalog)
# Inventory cannot be implemented for now as main 'anta' CLI is already parsing it
# _check.add_command(check_commands.inventory)

_exec.add_command(exec_commands.clear_counters)
_exec.add_command(exec_commands.snapshot)
_exec.add_command(exec_commands.collect_tech_support)

_get.add_command(get_commands.from_cvp)
_get.add_command(get_commands.from_ansible)
_get.add_command(get_commands.inventory)
_get.add_command(get_commands.tags)
setup_logging(log_level, log_file)

_debug.add_command(debug_commands.run_cmd)
_debug.add_command(debug_commands.run_template)

_nrfu.add_command(nrfu_commands.table)
_nrfu.add_command(nrfu_commands.json)
_nrfu.add_command(nrfu_commands.text)
_nrfu.add_command(nrfu_commands.tpl_report)
anta.add_command(nrfu_command)
anta.add_command(check_command)
anta.add_command(exec_command)
anta.add_command(get_command)
anta.add_command(debug_command)


# ANTA CLI Execution
def cli() -> None:
"""Entrypoint for pyproject.toml"""
anta(obj={}, auto_envvar_prefix="ANTA") # pragma: no cover
try:
anta(obj={}, auto_envvar_prefix="ANTA")
except Exception as e: # pylint: disable=broad-exception-caught
anta_log_exception(e, f"Uncaught Exception when running ANTA CLI\n{GITHUB_SUGGESTION}", logger)
sys.exit(ExitCode.INTERNAL_ERROR)


if __name__ == "__main__":
Expand Down
14 changes: 14 additions & 0 deletions anta/cli/check/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright (c) 2023 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""
Click commands to validate configuration files
"""
import click

from anta.cli.check import commands


@click.group
def check() -> None:
"""Commands to validate configuration files"""


check.add_command(commands.catalog)
19 changes: 5 additions & 14 deletions anta/cli/check/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# that can be found in the LICENSE file.
# pylint: disable = redefined-outer-name
"""
Commands for Anta CLI to run check commands.
Click commands to validate configuration files
"""
from __future__ import annotations

Expand All @@ -14,25 +14,16 @@

from anta.catalog import AntaCatalog
from anta.cli.console import console
from anta.cli.utils import parse_catalog
from anta.cli.utils import catalog_options

logger = logging.getLogger(__name__)


@click.command()
@click.option(
"--catalog",
"-c",
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, resolve_path=True),
required=True,
callback=parse_catalog,
)
@click.command
@catalog_options
def catalog(catalog: AntaCatalog) -> None:
"""
Check that the catalog is valid
"""
console.print(f"[bold][green]Catalog {catalog.filename} is valid")
console.print(f"[bold][green]Catalog is valid: {catalog.filename}")
console.print(pretty_repr(catalog.tests))
2 changes: 0 additions & 2 deletions anta/cli/console.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#!/usr/bin/env python
# Copyright (c) 2023 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
# coding: utf-8 -*-
"""
ANTA Top-level Console
https://rich.readthedocs.io/en/stable/console.html#console-api
Expand Down
15 changes: 15 additions & 0 deletions anta/cli/debug/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# Copyright (c) 2023 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""
Click commands to execute EOS commands on remote devices
"""
import click

from anta.cli.debug import commands


@click.group
def debug() -> None:
"""Commands to execute EOS commands on remote devices"""


debug.add_command(commands.run_cmd)
debug.add_command(commands.run_template)
Loading

0 comments on commit 6d4f37f

Please sign in to comment.