Skip to content

Commit

Permalink
feat(anta.cli): Add an --enable flag (#259)
Browse files Browse the repository at this point in the history
* Fix(anta.device): Make enable a flag for AsyncEOSDevice

* Feat: Add an --enable flag

* Feat: Make --enable mandatory when --enable-password is set

* Feat: Add enable feature to anta exec collect-show-tech

* Add tests to check the feature

* doc: Bring back snippets for anta --help

* Fix: Missing enable-passwords

* Test: Add tests for subcommand help

* doc: Adjust doc for feature

* refactor: Run pre-commit post rebase
  • Loading branch information
gmuloc authored Jul 26, 2023
1 parent 93905ad commit 3017f46
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 120 deletions.
13 changes: 11 additions & 2 deletions anta/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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 check_commands
from anta.cli.utils import IgnoreRequiredWithHelp, parse_catalog, parse_inventory, setup_logging
from anta.cli.utils import IgnoreRequiredWithHelp, parse_catalog, parse_inventory, requires_enable, setup_logging
from anta.result_manager.models import TestResult


Expand Down Expand Up @@ -50,10 +50,19 @@
help="Disable SSH Host Key validation",
show_default=True,
)
@click.option(
"--enable",
show_envvar=True,
is_flag=True,
default=False,
help="Add enable mode towards the devices if required to connect",
show_default=True,
)
@click.option(
"--enable-password",
show_envvar=True,
help="Enable password if required to connect",
help="Enable password if required to connect, --enable MUST be set",
callback=requires_enable,
)
@click.option(
"--inventory",
Expand Down
23 changes: 16 additions & 7 deletions anta/cli/exec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from aioeapi import EapiCommandError

from anta.device import AntaDevice
from anta.device import AntaDevice, AsyncEOSDevice
from anta.inventory import AntaInventory
from anta.models import AntaCommand
from anta.tools.misc import anta_log_exception, exc_to_str
Expand Down Expand Up @@ -120,14 +120,23 @@ async def collect(device: AntaDevice) -> None:
if command.collected and not command.text_output:
logger.debug(f"'aaa authorization exec default local' is not configured on device {device.name}")
if configure:
# Otherwise mypy complains about enable
assert isinstance(device, AsyncEOSDevice)
# TODO - @mtache - add `config` field to `AntaCommand` object to handle this use case.
commands = [
{"cmd": "enable", "input": device._enable_password}, # type: ignore[attr-defined] # pylint: disable=protected-access
"configure terminal",
"aaa authorization exec default local",
]
commands = []
if device.enable and device._enable_password is not None: # type: ignore[attr-defined] # pylint: disable=protected-access
commands.append({"cmd": "enable", "input": device._enable_password}) # type: ignore[attr-defined] # pylint: disable=protected-access
elif device.enable:
commands.append({"cmd": "enable"})
commands.extend(
[
{"cmd": "configure terminal"},
{"cmd": "aaa authorization exec default local"},
]
)
logger.warning(f"Configuring 'aaa authorization exec default local' on device {device.name}")
await device._session.cli(commands=commands) # type: ignore[attr-defined] # pylint: disable=protected-access
command = AntaCommand(command="show running-config | include aaa authorization exec default local", ofmt="text")
await device.session.cli(commands=commands) # type: ignore[attr-defined]
logger.info(f"Configured 'aaa authorization exec default local' on device {device.name}")
else:
logger.error(f"Unable to collect tech-support on {device.name}: configuration 'aaa authorization exec default local' is not present")
Expand Down
27 changes: 21 additions & 6 deletions anta/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
import enum
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple

import click
from click import Option
from yaml import safe_load

import anta.loader
Expand All @@ -22,6 +21,8 @@
logger = logging.getLogger(__name__)

if TYPE_CHECKING: # pragma: no_cover
from click import Option

from anta.result_manager import ResultManager


Expand Down Expand Up @@ -56,6 +57,7 @@ def parse_inventory(ctx: click.Context, path: Path) -> AntaInventory:
inventory_file=str(path),
username=ctx.params["username"],
password=ctx.params["password"],
enable=ctx.params["enable"],
enable_password=ctx.params["enable_password"],
timeout=ctx.params["timeout"],
insecure=ctx.params["insecure"],
Expand All @@ -67,7 +69,7 @@ def parse_inventory(ctx: click.Context, path: Path) -> AntaInventory:
return inventory


def parse_tags(ctx: click.Context, param: Option, value: str) -> List[str]:
def parse_tags(ctx: click.Context, param: Option, value: str) -> Optional[List[str]]:
# pylint: disable=unused-argument
"""
Click option callback to parse an ANTA inventory tags
Expand All @@ -77,6 +79,16 @@ def parse_tags(ctx: click.Context, param: Option, value: str) -> List[str]:
return None


def requires_enable(ctx: click.Context, param: Option, value: Optional[str]) -> Optional[str]:
# pylint: disable=unused-argument
"""
Click option callback to ensure that enable is True when the option is set
"""
if value is not None and ctx.params.get("enable") is not True:
raise click.BadParameter(f"'{param.opts[0]}' requires '--enable' (or setting associated env variable)")
return value


def parse_catalog(ctx: click.Context, param: Option, value: str) -> List[Tuple[Callable[..., TestResult], Dict[Any, Any]]]:
# pylint: disable=unused-argument
"""
Expand Down Expand Up @@ -158,6 +170,11 @@ def parse_args(self, ctx: click.Context, args: List[str]) -> List[str]:
Ignore MissingParameter exception when parsing arguments if `--help`
is present for a subcommand
"""
# Adding a flag for potential callbacks
ctx.ensure_object(dict)
if "--help" in args:
ctx.obj["_anta_help"] = True

try:
return super().parse_args(ctx, args)
except click.MissingParameter:
Expand All @@ -167,7 +184,5 @@ def parse_args(self, ctx: click.Context, args: List[str]) -> List[str]:
# remove the required params so that help can display
for param in self.params:
param.required = False
# Adding a flag for potential callbacks
ctx.ensure_object(dict)
ctx.obj["_anta_help"] = True

return super().parse_args(ctx, args)
11 changes: 8 additions & 3 deletions anta/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def __init__( # pylint: disable=R0913
username: str,
password: str,
name: Optional[str] = None,
enable: bool = False,
enable_password: Optional[str] = None,
port: Optional[int] = None,
ssh_port: Optional[int] = 22,
Expand All @@ -188,6 +189,7 @@ def __init__( # pylint: disable=R0913
username: Username to connect to eAPI and SSH
password: Password to connect to eAPI and SSH
name: Device name
enable: Device needs privileged access
enable_password: Password used to gain privileged access on EOS
port: eAPI port. Defaults to 80 is proto is 'http' or 443 if proto is 'https'.
ssh_port: SSH port
Expand All @@ -199,6 +201,7 @@ def __init__( # pylint: disable=R0913
if name is None:
name = f"{host}:{port}"
super().__init__(name, tags)
self.enable = enable
self._enable_password = enable_password
self._session: Device = Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
ssh_params: Dict[str, Any] = {}
Expand All @@ -216,6 +219,7 @@ def __rich_repr__(self) -> Iterator[Tuple[str, Any]]:
yield "eapi_port", self._session.port
yield "username", self._ssh_opts.username
yield "password", self._ssh_opts.password
yield "enable", self.enable
yield "enable_password", self._enable_password
yield "insecure", self._ssh_opts.known_hosts is None
if __DEBUG__:
Expand Down Expand Up @@ -244,14 +248,15 @@ async def collect(self, command: AntaCommand) -> None:
"""
try:
commands = []
if self._enable_password is not None:
if self.enable and self._enable_password is not None:
commands.append(
{
"cmd": "enable",
"input": str(self._enable_password),
}
)
else:
elif self.enable:
# No password
commands.append({"cmd": "enable"})
if command.revision:
commands.append({"cmd": command.command, "revision": command.revision})
Expand All @@ -266,7 +271,7 @@ async def collect(self, command: AntaCommand) -> None:
# only applicable to json output
if command.ofmt in ["json", "text"]:
# selecting only our command output
response = response[1]
response = response[-1]
command.output = response
logger.debug(f"{self.name}: {command}")

Expand Down
18 changes: 16 additions & 2 deletions anta/inventory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,13 @@ def _parse_ranges(inventory_input: AntaInventoryInput, inventory: AntaInventory,

@staticmethod
def parse(
inventory_file: str, username: str, password: str, enable_password: Optional[str] = None, timeout: Optional[float] = None, insecure: bool = False
inventory_file: str,
username: str,
password: str,
enable: bool = False,
enable_password: Optional[str] = None,
timeout: Optional[float] = None,
insecure: bool = False,
) -> AntaInventory:
# pylint: disable=too-many-arguments
"""
Expand All @@ -129,6 +135,7 @@ def parse(
inventory_file (str): Path to inventory YAML file where user has described his inputs
username (str): Username to use to connect to devices
password (str): Password to use to connect to devices
enable (bool): Whether or not the commands need to be run in enable mode towards the devices
timeout (float, optional): timeout in seconds for every API call.
Raises:
Expand All @@ -138,7 +145,14 @@ def parse(
"""

inventory = AntaInventory()
kwargs: Dict[str, Any] = {"username": username, "password": password, "enable_password": enable_password, "timeout": timeout, "insecure": insecure}
kwargs: Dict[str, Any] = {
"username": username,
"password": password,
"enable": enable,
"enable_password": enable_password,
"timeout": timeout,
"insecure": insecure,
}
kwargs = {k: v for k, v in kwargs.items() if v is not None}

with open(inventory_file, "r", encoding="UTF-8") as file:
Expand Down
8 changes: 7 additions & 1 deletion anta/tools/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ def exc_to_str(exception: Exception) -> str:
"""
Helper function that returns a human readable string from an Exception object
"""
return f"{type(exception).__name__}{f' ({str(exception)})' if str(exception) else ''}"
res = f"{type(exception).__name__}"
if str(exception):
res += f" ({str(exception)})"
elif hasattr(exception, "errmsg"):
# TODO - remove when we bump aio-eapi once our PR is merged there
res += f" ({exception.errmsg})"
return res


def tb_to_str(exception: Exception) -> str:
Expand Down
37 changes: 5 additions & 32 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,13 @@ ANTA is Python framework that automates tests for Arista devices.
$ pip install anta

# Run ANTA CLI
$ anta
Usage: anta [OPTIONS] COMMAND [ARGS]...

Arista Network Test Automation (ANTA) CLI
$ anta --help
--8<-- "anta_help.txt"
```

Options:
--version Show the version and exit.
--username TEXT Username to connect to EOS [env var:
ANTA_USERNAME; required]
--password TEXT Password to connect to EOS [env var:
ANTA_PASSWORD; required]
--timeout INTEGER Global connection timeout [env var:
ANTA_TIMEOUT; default: 5]
--insecure Disable SSH Host Key validation [env var:
ANTA_INSECURE]
--enable-password TEXT Enable password if required to connect [env
var: ANTA_ENABLE_PASSWORD]
-i, --inventory FILE Path to the inventory YAML file [env var:
ANTA_INVENTORY; required]
--log-level, --log [CRITICAL|ERROR|WARNING|INFO|DEBUG]
ANTA logging level [env var:
ANTA_LOG_LEVEL; default: INFO]
--ignore-status Always exit with success [env var:
ANTA_IGNORE_STATUS]
--ignore-error Only report failures and not errors [env
var: ANTA_IGNORE_ERROR]
--help Show this message and exit.
!!! info
`username`, `password`, `enable`, and `enable-password` values are the same for all devices

Commands:
debug Debug commands for building ANTA
exec Execute commands to inventory devices
get Get data from/to ANTA
nrfu Run NRFU against inventory devices
```

## Documentation

Expand Down
4 changes: 4 additions & 0 deletions docs/cli/get-inventory-information.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Current inventory content is:
eapi_port=443,
username='arista',
password='arista',
enable=True,
enable_password='arista',
insecure=False
),
Expand All @@ -152,6 +153,7 @@ Current inventory content is:
eapi_port=443,
username='arista',
password='arista',
enable=True,
enable_password='arista',
insecure=False
),
Expand All @@ -165,6 +167,7 @@ Current inventory content is:
eapi_port=443,
username='arista',
password='arista',
enable=True,
enable_password='arista',
insecure=False
),
Expand All @@ -178,6 +181,7 @@ Current inventory content is:
eapi_port=443,
username='arista',
password='arista',
enable=True,
enable_password='arista',
insecure=False
)
Expand Down
35 changes: 2 additions & 33 deletions docs/cli/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,8 @@ To start using the ANTA CLI, open your terminal and type `anta`.
## Invoking ANTA CLI

```bash
anta --help
Usage: anta [OPTIONS] COMMAND [ARGS]...

Arista Network Test Automation (ANTA) CLI

Options:
--version Show the version and exit.
--username TEXT Username to connect to EOS [env var:
ANTA_USERNAME; required]
--password TEXT Password to connect to EOS [env var:
ANTA_PASSWORD; required]
--timeout INTEGER Global connection timeout [env var:
ANTA_TIMEOUT; default: 5]
--insecure Disable SSH Host Key validation [env var:
ANTA_INSECURE]
--enable-password TEXT Enable password if required to connect [env
var: ANTA_ENABLE_PASSWORD]
-i, --inventory FILE Path to the inventory YAML file [env var:
ANTA_INVENTORY; required]
--log-level, --log [CRITICAL|ERROR|WARNING|INFO|DEBUG]
ANTA logging level [env var:
ANTA_LOG_LEVEL; default: INFO]
--ignore-status Always exit with success [env var:
ANTA_IGNORE_STATUS]
--ignore-error Only report failures and not errors [env
var: ANTA_IGNORE_ERROR]
--help Show this message and exit.

Commands:
debug Debug commands for building ANTA
exec Execute commands to inventory devices
get Get data from/to ANTA
nrfu Run NRFU against inventory devices
$ anta --help
--8<-- "anta_help.txt"
```

## ANTA Global Parameters
Expand Down
Loading

0 comments on commit 3017f46

Please sign in to comment.