diff options
Diffstat (limited to 'docs')
76 files changed, 4034 insertions, 0 deletions
diff --git a/docs/README.md b/docs/README.md new file mode 100755 index 0000000..d47d3fe --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ +<!-- + ~ 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. + --> + +[![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/arista-netdevops-community/anta/blob/main/LICENSE) +[![Linting and Testing Anta](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml/badge.svg)](https://github.com/arista-netdevops-community/anta/actions/workflows/code-testing.yml) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/arista-netdevops-community/anta) +[![github release](https://img.shields.io/github/release/arista-netdevops-community/anta.svg)](https://github.com/arista-netdevops-community/anta/releases/) +![PyPI - Downloads](https://img.shields.io/pypi/dm/anta) +![coverage](https://raw.githubusercontent.com/arista-netdevops-community/anta/coverage-badge/latest-release-coverage.svg) + +# Arista Network Test Automation (ANTA) Framework + +ANTA is Python framework that automates tests for Arista devices. + +- ANTA provides a [set of tests](api/tests.md) to validate the state of your network +- ANTA can be used to: + - Automate NRFU (Network Ready For Use) test on a preproduction network + - Automate tests on a live network (periodically or on demand) +- ANTA can be used with: + - The [ANTA CLI](cli/overview.md) + - As a [Python library](advanced_usages/as-python-lib.md) in your own application + +![anta nrfu](https://raw.githubusercontent.com/arista-netdevops-community/anta/main/docs/imgs/anta-nrfu.svg) + +```bash +# Install ANTA CLI +$ pip install anta + +# Run ANTA CLI +$ anta --help +Usage: anta [OPTIONS] COMMAND [ARGS]... + + Arista Network Test Automation (ANTA) CLI + +Options: + --version Show the version and exit. + --log-file FILE Send the logs to a file. If logging level is + DEBUG, only INFO or higher will be sent to + stdout. [env var: ANTA_LOG_FILE] + -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG] + ANTA logging level [env var: + ANTA_LOG_LEVEL; default: INFO] + --help Show this message and exit. + +Commands: + check Commands to validate configuration files + debug Commands to execute EOS commands on remote devices + exec Commands to execute various scripts on EOS devices + get Commands to get information from or generate inventories + nrfu Run ANTA tests on devices +``` + +> [!WARNING] +> The ANTA CLI options have changed after version 0.11 and have moved away from the top level `anta` and are now required at their respective commands (e.g. `anta nrfu`). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables. + +## Documentation + +The documentation is published on [ANTA package website](https://www.anta.ninja). Also, a [demo repository](https://github.com/titom73/atd-anta-demo) is available to facilitate your journey with ANTA. + +## Contribution guide + +Contributions are welcome. Please refer to the [contribution guide](contribution.md) + +## Credits + +Thank you to [Angélique Phillipps](https://github.com/aphillipps), [Colin MacGiollaEáin](https://github.com/colinmacgiolla), [Khelil Sator](https://github.com/ksator), [Matthieu Tache](https://github.com/mtache), [Onur Gashi](https://github.com/onurgashi), [Paul Lavelle](https://github.com/paullavelle), [Guillaume Mulocher](https://github.com/gmuloc) and [Thomas Grimonet](https://github.com/titom73) for their contributions and guidances. diff --git a/docs/advanced_usages/as-python-lib.md b/docs/advanced_usages/as-python-lib.md new file mode 100644 index 0000000..b4ce654 --- /dev/null +++ b/docs/advanced_usages/as-python-lib.md @@ -0,0 +1,315 @@ +<!-- + ~ 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. + --> + +ANTA is a Python library that can be used in user applications. This section describes how you can leverage ANTA Python modules to help you create your own NRFU solution. + +!!! tip + If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html + +## [AntaDevice](../api/device.md#anta.device.AntaDevice) Abstract Class + +A device is represented in ANTA as a instance of a subclass of the [AntaDevice](../api/device.md### ::: anta.device.AntaDevice) abstract class. +There are few abstract methods that needs to be implemented by child classes: + +- The [collect()](../api/device.md#anta.device.AntaDevice.collect) coroutine is in charge of collecting outputs of [AntaCommand](../api/models.md#anta.models.AntaCommand) instances. +- The [refresh()](../api/device.md#anta.device.AntaDevice.refresh) coroutine is in charge of updating attributes of the [AntaDevice](../api/device.md### ::: anta.device.AntaDevice) instance. These attributes are used by [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) to filter out unreachable devices or by [AntaTest](../api/models.md#anta.models.AntaTest) to skip devices based on their hardware models. + +The [copy()](../api/device.md#anta.device.AntaDevice.copy) coroutine is used to copy files to and from the device. It does not need to be implemented if tests are not using it. + +### [AsyncEOSDevice](../api/device.md#anta.device.AsyncEOSDevice) Class + +The [AsyncEOSDevice](../api/device.md#anta.device.AsyncEOSDevice) class is an implementation of [AntaDevice](../api/device.md#anta.device.AntaDevice) for Arista EOS. +It uses the [aio-eapi](https://github.com/jeremyschulman/aio-eapi) eAPI client and the [AsyncSSH](https://github.com/ronf/asyncssh) library. + +- The [collect()](../api/device.md#anta.device.AsyncEOSDevice.collect) coroutine collects [AntaCommand](../api/models.md#anta.models.AntaCommand) outputs using eAPI. +- The [refresh()](../api/device.md#anta.device.AsyncEOSDevice.refresh) coroutine tries to open a TCP connection on the eAPI port and update the `is_online` attribute accordingly. If the TCP connection succeeds, it sends a `show version` command to gather the hardware model of the device and updates the `established` and `hw_model` attributes. +- The [copy()](../api/device.md#anta.device.AsyncEOSDevice.copy) coroutine copies files to and from the device using the SCP protocol. + +## [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) Class + +The [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) class is a subclass of the standard Python type [dict](https://docs.python.org/3/library/stdtypes.html#dict). The keys of this dictionary are the device names, the values are [AntaDevice](../api/device.md#anta.device.AntaDevice) instances. + + +[AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) provides methods to interact with the ANTA inventory: + +- The [add_device()](../api/inventory.md#anta.inventory.AntaInventory.add_device) method adds an [AntaDevice](../api/device.md### ::: anta.device.AntaDevice) instance to the inventory. Adding an entry to [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) with a key different from the device name is not allowed. +- The [get_inventory()](../api/inventory.md#anta.inventory.AntaInventory.get_inventory) returns a new [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) instance with filtered out devices based on the method inputs. +- The [connect_inventory()](../api/inventory.md#anta.inventory.AntaInventory.connect_inventory) coroutine will execute the [refresh()](../api/device.md#anta.device.AntaDevice.refresh) coroutines of all the devices in the inventory. +- The [parse()](../api/inventory.md#anta.inventory.AntaInventory.parse) static method creates an [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) instance from a YAML file and returns it. The devices are [AsyncEOSDevice](../api/device.md#anta.device.AsyncEOSDevice) instances. + + +To parse a YAML inventory file and print the devices connection status: + +```python +""" +Example +""" +import asyncio + +from anta.inventory import AntaInventory + + +async def main(inv: AntaInventory) -> None: + """ + Take an AntaInventory and: + 1. try to connect to every device in the inventory + 2. print a message for every device connection status + """ + await inv.connect_inventory() + + for device in inv.values(): + if device.established: + print(f"Device {device.name} is online") + else: + print(f"Could not connect to device {device.name}") + +if __name__ == "__main__": + # Create the AntaInventory instance + inventory = AntaInventory.parse( + filename="inv.yml", + username="arista", + password="@rista123", + timeout=15, + ) + + # Run the main coroutine + res = asyncio.run(main(inventory)) +``` + +??? note "How to create your inventory file" + Please visit this [dedicated section](../usage-inventory-catalog.md) for how to use inventory and catalog files. + +To run an EOS commands list on the reachable devices from the inventory: +```python +""" +Example +""" +# This is needed to run the script for python < 3.10 for typing annotations +from __future__ import annotations + +import asyncio +from pprint import pprint + +from anta.inventory import AntaInventory +from anta.models import AntaCommand + + +async def main(inv: AntaInventory, commands: list[str]) -> dict[str, list[AntaCommand]]: + """ + Take an AntaInventory and a list of commands as string and: + 1. try to connect to every device in the inventory + 2. collect the results of the commands from each device + + Returns: + a dictionary where key is the device name and the value is the list of AntaCommand ran towards the device + """ + await inv.connect_inventory() + + # Make a list of coroutine to run commands towards each connected device + coros = [] + # dict to keep track of the commands per device + result_dict = {} + for name, device in inv.get_inventory(established_only=True).items(): + anta_commands = [AntaCommand(command=command, ofmt="json") for command in commands] + result_dict[name] = anta_commands + coros.append(device.collect_commands(anta_commands)) + + # Run the coroutines + await asyncio.gather(*coros) + + return result_dict + + +if __name__ == "__main__": + # Create the AntaInventory instance + inventory = AntaInventory.parse( + filename="inv.yml", + username="arista", + password="@rista123", + timeout=15, + ) + + # Create a list of commands with json output + commands = ["show version", "show ip bgp summary"] + + # Run the main asyncio entry point + res = asyncio.run(main(inventory, commands)) + + pprint(res) +``` + + +## Use tests from ANTA + +All the test classes inherit from the same abstract Base Class AntaTest. The Class definition indicates which commands are required for the test and the user should focus only on writing the `test` function with optional keywords argument. The instance of the class upon creation instantiates a TestResult object that can be accessed later on to check the status of the test ([unset, skipped, success, failure, error]). + +### Test structure + +All tests are built on a class named `AntaTest` which provides a complete toolset for a test: + +- Object creation +- Test definition +- TestResult definition +- Abstracted method to collect data + +This approach means each time you create a test it will be based on this `AntaTest` class. Besides that, you will have to provide some elements: + +- `name`: Name of the test +- `description`: A human readable description of your test +- `categories`: a list of categories to sort test. +- `commands`: a list of command to run. This list _must_ be a list of `AntaCommand` which is described in the next part of this document. + +Here is an example of a hardware test related to device temperature: + +```python +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional, cast + +from anta.models import AntaTest, AntaCommand + + +class VerifyTemperature(AntaTest): + """ + Verifies device temparture is currently OK. + """ + + # The test name + name = "VerifyTemperature" + # A small description of the test, usually the first line of the class docstring + description = "Verifies device temparture is currently OK" + # The category of the test, usually the module name + categories = ["hardware"] + # The command(s) used for the test. Could be a template instead + commands = [AntaCommand(command="show system environment temperature", ofmt="json")] + + # Decorator + @AntaTest.anta_test + # abstract method that must be defined by the child Test class + def test(self) -> None: + """Run VerifyTemperature validation""" + command_output = cast(Dict[str, Dict[Any, Any]], self.instance_commands[0].output) + temperature_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" + if temperature_status == "temperatureOk": + self.result.is_success() + else: + self.result.is_failure(f"Device temperature is not OK, systemStatus: {temperature_status }") +``` + +When you run the test, object will automatically call its `anta.models.AntaTest.collect()` method to get device output for each command if no pre-collected data was given to the test. This method does a loop to call `anta.inventory.models.InventoryDevice.collect()` methods which is in charge of managing device connection and how to get data. + +??? info "run test offline" + You can also pass eos data directly to your test if you want to validate data collected in a different workflow. An example is provided below just for information: + + ```python + test = VerifyTemperature(device, eos_data=test_data["eos_data"]) + asyncio.run(test.test()) + ``` + +The `test` function is always the same and __must__ be defined with the `@AntaTest.anta_test` decorator. This function takes at least one argument which is a `anta.inventory.models.InventoryDevice` object. +In some cases a test would rely on some additional inputs from the user, for instance the number of expected peers or some expected numbers. All parameters __must__ come with a default value and the test function __should__ validate the parameters values (at this stage this is the only place where validation can be done but there are future plans to make this better). + +```python +class VerifyTemperature(AntaTest): + ... + @AntaTest.anta_test + def test(self) -> None: + pass + +class VerifyTransceiversManufacturers(AntaTest): + ... + @AntaTest.anta_test + def test(self, manufacturers: Optional[List[str]] = None) -> None: + # validate the manufactures parameter + pass +``` + +The test itself does not return any value, but the result is directly availble from your AntaTest object and exposes a `anta.result_manager.models.TestResult` object with result, name of the test and optional messages: + + +- `name` (str): Device name where the test has run. +- `test` (str): Test name runs on the device. +- `categories` (List[str]): List of categories the TestResult belongs to, by default the AntaTest categories. +- `description` (str): TestResult description, by default the AntaTest description. +- `results` (str): Result of the test. Can be one of ["unset", "success", "failure", "error", "skipped"]. +- `message` (str, optional): Message to report after the test if any. +- `custom_field` (str, optional): Custom field to store a string for flexibility in integrating with ANTA + +```python +from anta.tests.hardware import VerifyTemperature + +test = VerifyTemperature(device, eos_data=test_data["eos_data"]) +asyncio.run(test.test()) +assert test.result.result == "success" +``` + +### Classes for commands + +To make it easier to get data, ANTA defines 2 different classes to manage commands to send to devices: + +#### [AntaCommand](../api/models.md#anta.models.AntaCommand) Class + +Represent a command with following information: + +- Command to run +- Ouput format expected +- eAPI version +- Output of the command + +Usage example: + +```python +from anta.models import AntaCommand + +cmd1 = AntaCommand(command="show zerotouch") +cmd2 = AntaCommand(command="show running-config diffs", ofmt="text") +``` + +!!! tip "Command revision and version" + * Most of EOS commands return a JSON structure according to a model (some commands may not be modeled hence the necessity to use `text` outformat sometimes. + * The model can change across time (adding feature, ... ) and when the model is changed in a non backward-compatible way, the __revision__ number is bumped. The initial model starts with __revision__ 1. + * A __revision__ applies to a particular CLI command whereas a __version__ is global to an eAPI call. The __version__ is internally translated to a specific __revision__ for each CLI command in the RPC call. The currently supported __version__ vaues are `1` and `latest`. + * A __revision takes precedence over a version__ (e.g. if a command is run with version="latest" and revision=1, the first revision of the model is returned) + * By default eAPI returns the first revision of each model to ensure that when upgrading, intergation with existing tools is not broken. This is done by using by default `version=1` in eAPI calls. + + ANTA uses by default `version="latest"` in AntaCommand. For some commands, you may want to run them with a different revision or version. + + For instance the `VerifyRoutingTableSize` test leverages the first revision of `show bfd peers`: + + ``` + # revision 1 as later revision introduce additional nesting for type + commands = [AntaCommand(command="show bfd peers", revision=1)] + ``` + +#### [AntaTemplate](../api/models.md#anta.models.AntaTemplate) Class + +Because some command can require more dynamic than just a command with no parameter provided by user, ANTA supports command template: you define a template in your test class and user provide parameters when creating test object. + +```python + +class RunArbitraryTemplateCommand(AntaTest): + """ + Run an EOS command and return result + Based on AntaTest to build relevant output for pytest + """ + + name = "Run aributrary EOS command" + description = "To be used only with anta debug commands" + template = AntaTemplate(template="show interfaces {ifd}") + categories = ["debug"] + + @AntaTest.anta_test + def test(self) -> None: + errdisabled_interfaces = [interface for interface, value in response["interfaceStatuses"].items() if value["linkStatus"] == "errdisabled"] + ... + + +params = [{"ifd": "Ethernet2"}, {"ifd": "Ethernet49/1"}] +run_command1 = RunArbitraryTemplateCommand(device_anta, params) +``` + +In this example, test waits for interfaces to check from user setup and will only check for interfaces in `params` diff --git a/docs/advanced_usages/caching.md b/docs/advanced_usages/caching.md new file mode 100644 index 0000000..cec2467 --- /dev/null +++ b/docs/advanced_usages/caching.md @@ -0,0 +1,87 @@ +<!-- + ~ 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. + --> + +ANTA is a streamlined Python framework designed for efficient interaction with network devices. This section outlines how ANTA incorporates caching mechanisms to collect command outputs from network devices. + +## Configuration + +By default, ANTA utilizes [aiocache](https://github.com/aio-libs/aiocache)'s memory cache backend, also called [`SimpleMemoryCache`](https://aiocache.aio-libs.org/en/v0.12.2/caches.html#simplememorycache). This library aims for simplicity and supports asynchronous operations to go along with Python `asyncio` used in ANTA. + +The `_init_cache()` method of the [AntaDevice](../advanced_usages/as-python-lib.md#antadevice-abstract-class) abstract class initializes the cache. Child classes can override this method to tweak the cache configuration: + +```python +def _init_cache(self) -> None: + """ + Initialize cache for the device, can be overridden by subclasses to manipulate how it works + """ + self.cache = Cache(cache_class=Cache.MEMORY, ttl=60, namespace=self.name, plugins=[HitMissRatioPlugin()]) + self.cache_locks = defaultdict(asyncio.Lock) +``` + +The cache is also configured with `aiocache`'s [`HitMissRatioPlugin`](https://aiocache.aio-libs.org/en/v0.12.2/plugins.html#hitmissratioplugin) plugin to calculate the ratio of hits the cache has and give useful statistics for logging purposes in ANTA. + +## Cache key design + +The cache is initialized per `AntaDevice` and uses the following cache key design: + +`<device_name>:<uid>` + +The `uid` is an attribute of [AntaCommand](../advanced_usages/as-python-lib.md#antacommand-class), which is a unique identifier generated from the command, version, revision and output format. + +Each UID has its own asyncio lock. This design allows coroutines that need to access the cache for different UIDs to do so concurrently. The locks are managed by the `self.cache_locks` dictionary. + +## Mechanisms + +By default, once the cache is initialized, it is used in the `collect()` method of `AntaDevice`. The `collect()` method prioritizes retrieving the output of the command from the cache. If the output is not in the cache, the private `_collect()` method will retrieve and then store it for future access. + +## How to disable caching + +Caching is enabled by default in ANTA following the previous configuration and mechanisms. + +There might be scenarios where caching is not wanted. You can disable caching in multiple ways in ANTA: + +1. Caching can be disabled globally, for **ALL** commands on **ALL** devices, using the `--disable-cache` global flag when invoking anta at the [CLI](../cli/overview.md#invoking-anta-cli): + ```bash + anta --disable-cache --username arista --password arista nrfu table + ``` +2. Caching can be disabled per device, network or range by setting the `disable_cache` key to `True` when definining the ANTA [Inventory](../usage-inventory-catalog.md#create-an-inventory-file) file: + ```yaml + anta_inventory: + hosts: + - host: 172.20.20.101 + name: DC1-SPINE1 + tags: ["SPINE", "DC1"] + disable_cache: True # Set this key to True + - host: 172.20.20.102 + name: DC1-SPINE2 + tags: ["SPINE", "DC1"] + disable_cache: False # Optional since it's the default + + networks: + - network: "172.21.21.0/24" + disable_cache: True + + ranges: + - start: 172.22.22.10 + end: 172.22.22.19 + disable_cache: True + ``` + This approach effectively disables caching for **ALL** commands sent to devices targeted by the `disable_cache` key. + +3. For tests developpers, caching can be disabled for a specific [`AntaCommand`](../advanced_usages/as-python-lib.md#antacommand-class) or [`AntaTemplate`](../advanced_usages/as-python-lib.md#antatemplate-class) by setting the `use_cache` attribute to `False`. That means the command output will always be collected on the device and therefore, never use caching. + +### Disable caching in a child class of `AntaDevice` + +Since caching is implemented at the `AntaDevice` abstract class level, all subclasses will inherit that default behavior. As a result, if you need to disable caching in any custom implementation of `AntaDevice` outside of the ANTA framework, you must initialize `AntaDevice` with `disable_cache` set to `True`: + +```python +class AnsibleEOSDevice(AntaDevice): + """ + Implementation of an AntaDevice using Ansible HttpApi plugin for EOS. + """ + def __init__(self, name: str, connection: ConnectionBase, tags: list = None) -> None: + super().__init__(name, tags, disable_cache=True) +``` diff --git a/docs/advanced_usages/custom-tests.md b/docs/advanced_usages/custom-tests.md new file mode 100644 index 0000000..87402c1 --- /dev/null +++ b/docs/advanced_usages/custom-tests.md @@ -0,0 +1,324 @@ +<!-- + ~ 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. + --> + +!!! info "" + This documentation applies for both creating tests in ANTA or creating your own test package. + +ANTA is not only a Python library with a CLI and a collection of built-in tests, it is also a framework you can extend by building your own tests. + +## Generic approach + +A test is a Python class where a test function is defined and will be run by the framework. + +ANTA provides an abstract class [AntaTest](../api/models.md#anta.models.AntaTest). This class does the heavy lifting and provide the logic to define, collect and test data. The code below is an example of a simple test in ANTA, which is an [AntaTest](../api/models.md#anta.models.AntaTest) subclass: + +```python +from anta.models import AntaTest, AntaCommand +from anta.decorators import skip_on_platforms + + +class VerifyTemperature(AntaTest): + """ + This test verifies if the device temperature is within acceptable limits. + + Expected Results: + * success: The test will pass if the device temperature is currently OK: 'temperatureOk'. + * failure: The test will fail if the device temperature is NOT OK. + """ + + name = "VerifyTemperature" + description = "Verifies if the device temperature is within the acceptable range." + categories = ["hardware"] + commands = [AntaCommand(command="show system environment temperature", ofmt="json")] + + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) + @AntaTest.anta_test + def test(self) -> None: + command_output = self.instance_commands[0].json_output + temperature_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" + if temperature_status == "temperatureOk": + self.result.is_success() + else: + self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'") +``` + +[AntaTest](../api/models.md#anta.models.AntaTest) also provide more advanced capabilities like [AntaCommand](../api/models.md#anta.models.AntaCommand) templating using the [AntaTemplate](../api/models.md#anta.models.AntaTemplate) class or test inputs definition and validation using [AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) [pydantic](https://docs.pydantic.dev/latest/) model. This will be discussed in the sections below. + +## [AntaTest](../api/models.md#anta.models.AntaTest) structure + +### Class Attributes + +- `name` (`str`): Name of the test. Used during reporting. +- `description` (`str`): A human readable description of your test. +- `categories` (`list[str]`): A list of categories in which the test belongs. +- `commands` (`list[Union[AntaTemplate, AntaCommand]]`): A list of command to collect from devices. This list __must__ be a list of [AntaCommand](../api/models.md#anta.models.AntaCommand) or [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances. Rendering [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances will be discussed later. + +!!! info + All these class attributes are mandatory. If any attribute is missing, a `NotImplementedError` exception will be raised during class instantiation. + +### Instance Attributes + +!!! info + You can access an instance attribute in your code using the `self` reference. E.g. you can access the test input values using `self.inputs`. + +::: anta.models.AntaTest + options: + show_docstring_attributes: true + show_root_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + members: false + show_source: false + show_root_toc_entry: false + heading_level: 10 + + +!!! note "Logger object" + ANTA already provides comprehensive logging at every steps of a test execution. The [AntaTest](../api/models.md#anta.models.AntaTest) class also provides a `logger` attribute that is a Python logger specific to the test instance. See [Python documentation](https://docs.python.org/3/library/logging.html) for more information. + +!!! note "AntaDevice object" + Even if `device` is not a private attribute, you should not need to access this object in your code. + +### Test Inputs + +[AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) is a [pydantic model](https://docs.pydantic.dev/latest/usage/models/) that allow test developers to define their test inputs. [pydantic](https://docs.pydantic.dev/latest/) provides out of the box [error handling](https://docs.pydantic.dev/latest/usage/models/#error-handling) for test input validation based on the type hints defined by the test developer. + +The base definition of [AntaTest.Input](../api/models.md#anta.models.AntaTest.Input) provides common test inputs for all [AntaTest](../api/models.md#anta.models.AntaTest) instances: + +#### [Input](../api/models.md#anta.models.AntaTest.Input) model + +::: anta.models.AntaTest.Input + options: + show_docstring_attributes: true + show_root_heading: false + show_category_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + show_source: false + members: false + show_root_toc_entry: false + heading_level: 10 + +#### [ResultOverwrite](../api/models.md#anta.models.AntaTest.Input.ResultOverwrite) model + +::: anta.models.AntaTest.Input.ResultOverwrite + options: + show_docstring_attributes: true + show_root_heading: false + show_category_heading: false + show_bases: false + show_docstring_description: false + show_docstring_examples: false + show_docstring_parameters: false + show_source: false + show_root_toc_entry: false + heading_level: 10 + +!!! note + The pydantic model is configured using the [`extra=forbid`](https://docs.pydantic.dev/latest/usage/model_config/#extra-attributes) that will fail input validation if extra fields are provided. + +### Methods + +- [test(self) -> None](../api/models.md#anta.models.AntaTest.test): This is an abstract method that __must__ be implemented. It contains the test logic that can access the collected command outputs using the `instance_commands` instance attribute, access the test inputs using the `inputs` instance attribute and __must__ set the `result` instance attribute accordingly. It must be implemented using the `AntaTest.anta_test` decorator that provides logging and will collect commands before executing the `test()` method. +- [render(self, template: AntaTemplate) -> list[AntaCommand]](../api/models.md#anta.models.AntaTest.render): This method only needs to be implemented if [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances are present in the `commands` class attribute. It will be called for every [AntaTemplate](../api/models.md#anta.models.AntaTemplate) occurence and __must__ return a list of [AntaCommand](../api/models.md#anta.models.AntaCommand) using the [AntaTemplate.render()](../api/models.md#anta.models.AntaTemplate.render) method. It can access test inputs using the `inputs` instance attribute. + +## Test execution + +Below is a high level description of the test execution flow in ANTA: + +1. ANTA will parse the test catalog to get the list of [AntaTest](../api/models.md#anta.models.AntaTest) subclasses to instantiate and their associated input values. We consider a single [AntaTest](../api/models.md#anta.models.AntaTest) subclass in the following steps. + +2. ANTA will instantiate the [AntaTest](../api/models.md#anta.models.AntaTest) subclass and a single device will be provided to the test instance. The `Input` model defined in the class will also be instantiated at this moment. If any [ValidationError](https://docs.pydantic.dev/latest/errors/errors/) is raised, the test execution will be stopped. + +3. If there is any [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instance in the `commands` class attribute, [render()](../api/models.md#anta.models.AntaTest.render) will be called for every occurrence. At this moment, the `instance_commands` attribute has been initialized. If any rendering error occurs, the test execution will be stopped. + +4. The `AntaTest.anta_test` decorator will collect the commands from the device and update the `instance_commands` attribute with the outputs. If any collection error occurs, the test execution will be stopped. + +5. The [test()](../api/models.md#anta.models.AntaTest.test) method is executed. + +## Writing an AntaTest subclass + +In this section, we will go into all the details of writing an [AntaTest](../api/models.md#anta.models.AntaTest) subclass. + +### Class definition + +Import [anta.models.AntaTest](../api/models.md#anta.models.AntaTest) and define your own class. +Define the mandatory class attributes using [anta.models.AntaCommand](../api/models.md#anta.models.AntaCommand), [anta.models.AntaTemplate](../api/models.md#anta.models.AntaTemplate) or both. + +!!! info + Caching can be disabled per `AntaCommand` or `AntaTemplate` by setting the `use_cache` argument to `False`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md). + +```python +from anta.models import AntaTest, AntaCommand, AntaTemplate + + +class <YourTestName>(AntaTest): + """ + <a docstring description of your test> + """ + + name = "YourTestName" # should be your class name + description = "<test description in human reading format>" + categories = ["<arbitrary category>", "<another arbitrary category>"] + commands = [ + AntaCommand( + command="<EOS command to run>", + ofmt="<command format output>", + version="<eAPI version to use>", + revision="<revision to use for the command>", # revision has precedence over version + use_cache="<Use cache for the command>", + ), + AntaTemplate( + template="<Python f-string to render an EOS command>", + ofmt="<command format output>", + version="<eAPI version to use>", + revision="<revision to use for the command>", # revision has precedence over version + use_cache="<Use cache for the command>", + ) + ] +``` + +### Inputs definition + +If the user needs to provide inputs for your test, you need to define a [pydantic model](https://docs.pydantic.dev/latest/usage/models/) that defines the schema of the test inputs: + +```python +class <YourTestName>(AntaTest): + ... + class Input(AntaTest.Input): # pylint: disable=missing-class-docstring + <input field name>: <input field type> + """<input field docstring>""" +``` + +To define an input field type, refer to the [pydantic documentation](https://docs.pydantic.dev/latest/usage/types/types/) about types. +You can also leverage [anta.custom_types](../api/types.md) that provides reusable types defined in ANTA tests. + +Regarding required, optional and nullable fields, refer to this [documentation](https://docs.pydantic.dev/latest/migration/#required-optional-and-nullable-fields) on how to define them. + +!!! note + All the `pydantic` features are supported. For instance you can define [validators](https://docs.pydantic.dev/latest/usage/validators/) for complex input validation. + +### Template rendering + +Define the `render()` method if you have [AntaTemplate](../api/models.md#anta.models.AntaTemplate) instances in your `commands` class attribute: + +```python +class <YourTestName>(AntaTest): + ... + def render(self, template: AntaTemplate) -> list[AntaCommand]: + return [template.render(<template param>=input_value) for input_value in self.inputs.<input_field>] +``` + +You can access test inputs and render as many [AntaCommand](../api/models.md#anta.models.AntaCommand) as desired. + +### Test definition + +Implement the `test()` method with your test logic: + +```python +class <YourTestName>(AntaTest): + ... + @AntaTest.anta_test + def test(self) -> None: + pass +``` + +The logic usually includes the following different stages: +1. Parse the command outputs using the `self.instance_commands` instance attribute. +2. If needed, access the test inputs using the `self.inputs` instance attribute and write your conditional logic. +3. Set the `result` instance attribute to reflect the test result by either calling `self.result.is_success()` or `self.result.is_failure("<FAILURE REASON>")`. Sometimes, setting the test result to `skipped` using `self.result.is_skipped("<SKIPPED REASON>")` can make sense (e.g. testing the OSPF neighbor states but no neighbor was found). However, you should not need to catch any exception and set the test result to `error` since the error handling is done by the framework, see below. + +The example below is based on the [VerifyTemperature](../api/tests.hardware.md#anta.tests.hardware.VerifyTemperature) test. + +```python +class VerifyTemperature(AntaTest): + ... + @AntaTest.anta_test + def test(self) -> None: + # Grab output of the collected command + command_output = self.instance_commands[0].json_output + + # Do your test: In this example we check a specific field of the JSON output from EOS + temperature_status = command_output["systemStatus"] if "systemStatus" in command_output.keys() else "" + if temperature_status == "temperatureOk": + self.result.is_success() + else: + self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'") +``` + +As you can see there is no error handling to do in your code. Everything is packaged in the `AntaTest.anta_tests` decorator and below is a simple example of error captured when trying to access a dictionary with an incorrect key: + +```python +class VerifyTemperature(AntaTest): + ... + @AntaTest.anta_test + def test(self) -> None: + # Grab output of the collected command + command_output = self.instance_commands[0].json_output + + # Access the dictionary with an incorrect key + command_output['incorrectKey'] +``` + +```bash +ERROR Exception raised for test VerifyTemperature (on device 192.168.0.10) - KeyError ('incorrectKey') +``` + +!!! info "Get stack trace for debugging" + If you want to access to the full exception stack, you can run ANTA in debug mode by setting the `ANTA_DEBUG` environment variable to `true`. Example: + ```bash + $ ANTA_DEBUG=true anta nrfu --catalog test_custom.yml text + ``` + +### Test decorators + +In addition to the required `AntaTest.anta_tests` decorator, ANTA offers a set of optional decorators for further test customization: + +- `anta.decorators.deprecated_test`: Use this to log a message of WARNING severity when a test is deprecated. +- `anta.decorators.skip_on_platforms`: Use this to skip tests for functionalities that are not supported on specific platforms. + +```python +from anta.decorators import skip_on_platforms + +class VerifyTemperature(AntaTest): + ... + @skip_on_platforms(["cEOSLab", "vEOS-lab"]) + @AntaTest.anta_test + def test(self) -> None: + pass +``` + +## Access your custom tests in the test catalog + +!!! warning "" + This section is required only if you are not merging your development into ANTA. Otherwise, just follow [contribution guide](../contribution.md). + +For that, you need to create your own Python package as described in this [hitchhiker's guide](https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/) to package Python code. We assume it is well known and we won't focus on this aspect. Thus, your package must be impartable by ANTA hence available in the module search path `sys.path` (you can use `PYTHONPATH` for example). + +It is very similar to what is documented in [catalog section](../usage-inventory-catalog.md) but you have to use your own package name.2 + +Let say the custom Python package is `anta_titom73` and the test is defined in `anta_titom73.dc_project` Python module, the test catalog would look like: + +```yaml +anta_titom73.dc_project: + - VerifyFeatureX: + minimum: 1 +``` +And now you can run your NRFU tests with the CLI: + +```bash +anta nrfu text --catalog test_custom.yml +spine01 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1) +spine02 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1) +leaf01 :: verify_dynamic_vlan :: SUCCESS +leaf02 :: verify_dynamic_vlan :: SUCCESS +leaf03 :: verify_dynamic_vlan :: SUCCESS +leaf04 :: verify_dynamic_vlan :: SUCCESS +``` diff --git a/docs/api/catalog.md b/docs/api/catalog.md new file mode 100644 index 0000000..fc719ea --- /dev/null +++ b/docs/api/catalog.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +### ::: anta.catalog.AntaCatalog + options: + filters: ["!^_[^_]", "!__str__"] + +### ::: anta.catalog.AntaTestDefinition + +### ::: anta.catalog.AntaCatalogFile diff --git a/docs/api/device.md b/docs/api/device.md new file mode 100644 index 0000000..03cff19 --- /dev/null +++ b/docs/api/device.md @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> + +# AntaDevice base class + +## UML representation + +![](../imgs/uml/anta.device.AntaDevice.jpeg) + +### ::: anta.device.AntaDevice + options: + filters: ["!^_[^_]", "!__(eq|rich_repr)__"] + +# Async EOS device class + +## UML representation + +![](../imgs/uml/anta.device.AsyncEOSDevice.jpeg) + +### ::: anta.device.AsyncEOSDevice + options: + filters: ["!^_[^_]", "!__(eq|rich_repr)__"] diff --git a/docs/api/inventory.md b/docs/api/inventory.md new file mode 100644 index 0000000..5e4400c --- /dev/null +++ b/docs/api/inventory.md @@ -0,0 +1,11 @@ +<!-- + ~ 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. + --> + +### ::: anta.inventory.AntaInventory + options: + filters: ["!^_[^_]", "!__str__"] + +### ::: anta.inventory.exceptions diff --git a/docs/api/inventory.models.input.md b/docs/api/inventory.models.input.md new file mode 100644 index 0000000..a15c20e --- /dev/null +++ b/docs/api/inventory.models.input.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +### ::: anta.inventory.models.AntaInventoryInput + +### ::: anta.inventory.models.AntaInventoryHost + +### ::: anta.inventory.models.AntaInventoryNetwork + +### ::: anta.inventory.models.AntaInventoryRange diff --git a/docs/api/models.md b/docs/api/models.md new file mode 100644 index 0000000..b0c1e91 --- /dev/null +++ b/docs/api/models.md @@ -0,0 +1,37 @@ +<!-- + ~ 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. + --> + +# Test definition + +## UML Diagram + +![](../imgs/uml/anta.models.AntaTest.jpeg) + +### ::: anta.models.AntaTest + options: + filters: ["!^_[^_]", "!__init_subclass__", "!update_progress"] + +# Command definition + +## UML Diagram + +![](../imgs/uml/anta.models.AntaCommand.jpeg) +### ::: anta.models.AntaCommand + +!!! warning + CLI commands are protected to avoid execution of critical commands such as `reload` or `write erase`. + + - Reload command: `^reload\s*\w*` + - Configure mode: `^conf\w*\s*(terminal|session)*` + - Write: `^wr\w*\s*\w+` + +# Template definition + +## UML Diagram + +![](../imgs/uml/anta.models.AntaTemplate.jpeg) + +### ::: anta.models.AntaTemplate diff --git a/docs/api/report_manager.md b/docs/api/report_manager.md new file mode 100644 index 0000000..f0e3818 --- /dev/null +++ b/docs/api/report_manager.md @@ -0,0 +1,7 @@ +<!-- + ~ 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. + --> + +### ::: anta.reporter.ReportTable diff --git a/docs/api/result_manager.md b/docs/api/result_manager.md new file mode 100644 index 0000000..72e05aa --- /dev/null +++ b/docs/api/result_manager.md @@ -0,0 +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. + --> + +# Result Manager definition + +## UML Diagram + +![](../imgs/uml/anta.result_manager.ResultManager.jpeg) + +### ::: anta.result_manager.ResultManager + options: + filters: ["!^_[^_]", "!^__len__"] diff --git a/docs/api/result_manager_models.md b/docs/api/result_manager_models.md new file mode 100644 index 0000000..096bd03 --- /dev/null +++ b/docs/api/result_manager_models.md @@ -0,0 +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. + --> + +# Test Result model + +## UML Diagram + +![](../imgs/uml/anta.result_manager.models.TestResult.jpeg) + +### ::: anta.result_manager.models.TestResult + options: + filters: ["!^_[^_]", "!__str__"] diff --git a/docs/api/tests.aaa.md b/docs/api/tests.aaa.md new file mode 100644 index 0000000..bdbe7ec --- /dev/null +++ b/docs/api/tests.aaa.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for interfaces tests + +::: anta.tests.aaa + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.bfd.md b/docs/api/tests.bfd.md new file mode 100644 index 0000000..d28521f --- /dev/null +++ b/docs/api/tests.bfd.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for bfd tests + +::: anta.tests.bfd + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.configuration.md b/docs/api/tests.configuration.md new file mode 100644 index 0000000..aaee1f4 --- /dev/null +++ b/docs/api/tests.configuration.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for configuration tests + +::: anta.tests.configuration + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.connectivity.md b/docs/api/tests.connectivity.md new file mode 100644 index 0000000..8a1b8a2 --- /dev/null +++ b/docs/api/tests.connectivity.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for connectivity tests + +::: anta.tests.connectivity + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.field_notices.md b/docs/api/tests.field_notices.md new file mode 100644 index 0000000..ed0e837 --- /dev/null +++ b/docs/api/tests.field_notices.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for Field Notices tests + +::: anta.tests.field_notices + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.hardware.md b/docs/api/tests.hardware.md new file mode 100644 index 0000000..6e84196 --- /dev/null +++ b/docs/api/tests.hardware.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for hardware tests + +::: anta.tests.hardware + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.interfaces.md b/docs/api/tests.interfaces.md new file mode 100644 index 0000000..b21da40 --- /dev/null +++ b/docs/api/tests.interfaces.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for interfaces tests + +::: anta.tests.interfaces + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.logging.md b/docs/api/tests.logging.md new file mode 100644 index 0000000..e9acc20 --- /dev/null +++ b/docs/api/tests.logging.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for logging tests + +::: anta.tests.logging + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.md b/docs/api/tests.md new file mode 100644 index 0000000..40c7d8a --- /dev/null +++ b/docs/api/tests.md @@ -0,0 +1,37 @@ +<!-- + ~ 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. + --> + +# ANTA Tests landing page + +This section describes all the available tests provided by ANTA package. + + +- [AAA](tests.aaa.md) +- [BFD](tests.bfd.md) +- [Configuration](tests.configuration.md) +- [Connectivity](tests.connectivity.md) +- [Field Notice](tests.field_notices.md) +- [Hardware](tests.hardware.md) +- [Interfaces](tests.interfaces.md) +- [Logging](tests.logging.md) +- [MLAG](tests.mlag.md) +- [Multicast](tests.multicast.md) +- [Profiles](tests.profiles.md) +- [Routing Generic](tests.routing.generic.md) +- [Routing BGP](tests.routing.bgp.md) +- [Routing OSPF](tests.routing.ospf.md) +- [Security](tests.security.md) +- [Services](tests.services.md) +- [SNMP](tests.snmp.md) +- [Software](tests.software.md) +- [STP](tests.stp.md) +- [System](tests.system.md) +- [VLAN](tests.vlan.md) +- [VXLAN](tests.vxlan.md) + + + +All these tests can be imported in a [catalog](../usage-inventory-catalog.md) to be used by [the anta cli](../cli/nrfu.md) or in your [own framework](../advanced_usages/as-python-lib.md) diff --git a/docs/api/tests.mlag.md b/docs/api/tests.mlag.md new file mode 100644 index 0000000..6ce419b --- /dev/null +++ b/docs/api/tests.mlag.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for mlag tests + +::: anta.tests.mlag + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.multicast.md b/docs/api/tests.multicast.md new file mode 100644 index 0000000..2b03420 --- /dev/null +++ b/docs/api/tests.multicast.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for multicast tests + +::: anta.tests.multicast + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.profiles.md b/docs/api/tests.profiles.md new file mode 100644 index 0000000..c6d06e7 --- /dev/null +++ b/docs/api/tests.profiles.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for profiles tests + +::: anta.tests.profiles + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.routing.bgp.md b/docs/api/tests.routing.bgp.md new file mode 100644 index 0000000..2346866 --- /dev/null +++ b/docs/api/tests.routing.bgp.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for BGP tests + +::: anta.tests.routing.bgp + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.routing.generic.md b/docs/api/tests.routing.generic.md new file mode 100644 index 0000000..3853fb0 --- /dev/null +++ b/docs/api/tests.routing.generic.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for routing-generic tests + +::: anta.tests.routing.generic + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.routing.ospf.md b/docs/api/tests.routing.ospf.md new file mode 100644 index 0000000..c4e6fed --- /dev/null +++ b/docs/api/tests.routing.ospf.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for routing-ospf tests + +::: anta.tests.routing.ospf + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.security.md b/docs/api/tests.security.md new file mode 100644 index 0000000..1186b31 --- /dev/null +++ b/docs/api/tests.security.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for security tests + +::: anta.tests.security + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.services.md b/docs/api/tests.services.md new file mode 100644 index 0000000..82a7b38 --- /dev/null +++ b/docs/api/tests.services.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for services tests + +::: anta.tests.services + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.snmp.md b/docs/api/tests.snmp.md new file mode 100644 index 0000000..a015d04 --- /dev/null +++ b/docs/api/tests.snmp.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for SNMP tests + +::: anta.tests.snmp + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.software.md b/docs/api/tests.software.md new file mode 100644 index 0000000..7a2f0ec --- /dev/null +++ b/docs/api/tests.software.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for software tests + +::: anta.tests.software + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.stp.md b/docs/api/tests.stp.md new file mode 100644 index 0000000..f86dac4 --- /dev/null +++ b/docs/api/tests.stp.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for STP tests + +::: anta.tests.stp + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.system.md b/docs/api/tests.system.md new file mode 100644 index 0000000..621c17b --- /dev/null +++ b/docs/api/tests.system.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for system tests + +::: anta.tests.system + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.vlan.md b/docs/api/tests.vlan.md new file mode 100644 index 0000000..0e1aa15 --- /dev/null +++ b/docs/api/tests.vlan.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for vlan tests + +::: anta.tests.vlan + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/tests.vxlan.md b/docs/api/tests.vxlan.md new file mode 100644 index 0000000..a4dcff3 --- /dev/null +++ b/docs/api/tests.vxlan.md @@ -0,0 +1,13 @@ +<!-- + ~ 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. + --> + +# ANTA catalog for VXLAN tests + +::: anta.tests.vxlan + options: + show_root_heading: false + show_root_toc_entry: false + merge_init_into_class: false diff --git a/docs/api/types.md b/docs/api/types.md new file mode 100644 index 0000000..806ab63 --- /dev/null +++ b/docs/api/types.md @@ -0,0 +1,10 @@ +<!-- + ~ 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. + --> + +### ::: anta.custom_types + options: + show_if_no_docstring: true + show_root_full_path: true diff --git a/docs/cli/check.md b/docs/cli/check.md new file mode 100644 index 0000000..d7dea62 --- /dev/null +++ b/docs/cli/check.md @@ -0,0 +1,36 @@ +<!-- + ~ 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. + --> + +# ANTA check commands + +The ANTA check command allow to execute some checks on the ANTA input files. +Only checking the catalog is currently supported. + +```bash +anta check --help +Usage: anta check [OPTIONS] COMMAND [ARGS]... + + Check commands for building ANTA + +Options: + --help Show this message and exit. + +Commands: + catalog Check that the catalog is valid +``` + +## Checking the catalog + +```bash +Usage: anta check catalog [OPTIONS] + + Check that the catalog is valid + +Options: + -c, --catalog FILE Path to the test catalog YAML file [env var: + ANTA_CATALOG; required] + --help Show this message and exit. +``` diff --git a/docs/cli/debug.md b/docs/cli/debug.md new file mode 100644 index 0000000..1743c7a --- /dev/null +++ b/docs/cli/debug.md @@ -0,0 +1,175 @@ +<!-- + ~ 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. + --> + +# ANTA debug commands + +The ANTA CLI includes a set of debugging tools, making it easier to build and test ANTA content. This functionality is accessed via the `debug` subcommand and offers the following options: + +- Executing a command on a device from your inventory and retrieving the result. +- Running a templated command on a device from your inventory and retrieving the result. + +These tools are especially helpful in building the tests, as they give a visual access to the output received from the eAPI. They also facilitate the extraction of output content for use in unit tests, as described in our [contribution guide](../contribution.md). + +!!! warning + The `debug` tools require a device from your inventory. Thus, you MUST use a valid [ANTA Inventory](../usage-inventory-catalog.md#create-an-inventory-file). + +## Executing an EOS command + +You can use the `run-cmd` entrypoint to run a command, which includes the following options: + +### Command overview + +```bash +$ anta debug run-cmd --help +Usage: anta debug run-cmd [OPTIONS] + + Run arbitrary command to an ANTA device + +Options: + -u, --username TEXT Username to connect to EOS [env var: + ANTA_USERNAME; required] + -p, --password TEXT Password to connect to EOS that must be provided. + It can be prompted using '--prompt' option. [env + var: ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It + can be prompted using '--prompt' option. Requires + '--enable' option. [env var: + ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC + mode. This option tries to access this mode before + sending a command to the device. [env var: + ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. + [env var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + --ofmt [json|text] EOS eAPI format to use. can be text or json + -v, --version [1|latest] EOS eAPI version + -r, --revision INTEGER eAPI command revision + -d, --device TEXT Device from inventory to use [required] + -c, --command TEXT Command to run [required] + --help Show this message and exit. +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +### Example + +This example illustrates how to run the `show interfaces description` command with a `JSON` format (default): + +```bash +anta debug run-cmd --command "show interfaces description" --device DC1-SPINE1 +Run command show interfaces description on DC1-SPINE1 +{ + 'interfaceDescriptions': { + 'Ethernet1': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1A_Ethernet1', 'interfaceStatus': 'up'}, + 'Ethernet2': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1B_Ethernet1', 'interfaceStatus': 'up'}, + 'Ethernet3': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL1_Ethernet1', 'interfaceStatus': 'up'}, + 'Ethernet4': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL2_Ethernet1', 'interfaceStatus': 'up'}, + 'Loopback0': {'lineProtocolStatus': 'up', 'description': 'EVPN_Overlay_Peering', 'interfaceStatus': 'up'}, + 'Management0': {'lineProtocolStatus': 'up', 'description': 'oob_management', 'interfaceStatus': 'up'} + } +} +``` + +## Executing an EOS command using templates + +The `run-template` entrypoint allows the user to provide an [`f-string`](https://realpython.com/python-f-strings/#f-strings-a-new-and-improved-way-to-format-strings-in-python) templated command. It is followed by a list of arguments (key-value pairs) that build a dictionary used as template parameters. + +### Command overview + +```bash +$ anta debug run-template --help +Usage: anta debug run-template [OPTIONS] PARAMS... + + Run arbitrary templated command to an ANTA device. + + Takes a list of arguments (keys followed by a value) to build a dictionary + used as template parameters. Example: + + anta debug run-template -d leaf1a -t 'show vlan {vlan_id}' vlan_id 1 + +Options: + -u, --username TEXT Username to connect to EOS [env var: + ANTA_USERNAME; required] + -p, --password TEXT Password to connect to EOS that must be provided. + It can be prompted using '--prompt' option. [env + var: ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It + can be prompted using '--prompt' option. Requires + '--enable' option. [env var: + ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC + mode. This option tries to access this mode before + sending a command to the device. [env var: + ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. + [env var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + --ofmt [json|text] EOS eAPI format to use. can be text or json + -v, --version [1|latest] EOS eAPI version + -r, --revision INTEGER eAPI command revision + -d, --device TEXT Device from inventory to use [required] + -t, --template TEXT Command template to run. E.g. 'show vlan + {vlan_id}' [required] + --help Show this message and exit. +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +### Example + +This example uses the `show vlan {vlan_id}` command in a `JSON` format: + +```bash +anta debug run-template --template "show vlan {vlan_id}" vlan_id 10 --device DC1-LEAF1A +Run templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A +{ + 'vlans': { + '10': { + 'name': 'VRFPROD_VLAN10', + 'dynamic': False, + 'status': 'active', + 'interfaces': { + 'Cpu': {'privatePromoted': False, 'blocked': None}, + 'Port-Channel11': {'privatePromoted': False, 'blocked': None}, + 'Vxlan1': {'privatePromoted': False, 'blocked': None} + } + } + }, + 'sourceDetail': '' +} +``` +!!! warning + If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. + +### Example of multiple arguments + +```bash +anta -log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 --device DC1-SPINE1 +> {'dst': '8.8.8.8', 'src': 'Loopback0'} + +anta -log DEBUG debug run-template --template "ping {dst} source {src}" dst "8.8.8.8" src Loopback0 dst "1.1.1.1" src Loopback1 --device DC1-SPINE1 +> {'dst': '1.1.1.1', 'src': 'Loopback1'} +# Notice how `src` and `dst` keep only the latest value +``` diff --git a/docs/cli/exec.md b/docs/cli/exec.md new file mode 100644 index 0000000..fe39c12 --- /dev/null +++ b/docs/cli/exec.md @@ -0,0 +1,298 @@ +<!-- + ~ 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. + --> + +# Executing Commands on Devices + +ANTA CLI provides a set of entrypoints to facilitate remote command execution on EOS devices. + +### EXEC Command overview +```bash +anta exec --help +Usage: anta exec [OPTIONS] COMMAND [ARGS]... + + Execute commands to inventory devices + +Options: + --help Show this message and exit. + +Commands: + clear-counters Clear counter statistics on EOS devices + collect-tech-support Collect scheduled tech-support from EOS devices + snapshot Collect commands output from devices in inventory +``` + +## Clear interfaces counters + +This command clears interface counters on EOS devices specified in your inventory. + +### Command overview + +```bash +anta exec clear-counters --help +Usage: anta exec clear-counters [OPTIONS] + + Clear counter statistics on EOS devices + +Options: + -u, --username TEXT Username to connect to EOS [env var: ANTA_USERNAME; + required] + -p, --password TEXT Password to connect to EOS that must be provided. It + can be prompted using '--prompt' option. [env var: + ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It can + be prompted using '--prompt' option. Requires '-- + enable' option. [env var: ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC mode. + This option tries to access this mode before sending + a command to the device. [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. [env + var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + --help Show this message and exit. +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +### Example + +```bash +anta exec clear-counters --tags SPINE +[20:19:13] INFO Connecting to devices... utils.py:43 + INFO Clearing counters on remote devices... utils.py:46 + INFO Cleared counters on DC1-SPINE2 (cEOSLab) utils.py:41 + INFO Cleared counters on DC2-SPINE1 (cEOSLab) utils.py:41 + INFO Cleared counters on DC1-SPINE1 (cEOSLab) utils.py:41 + INFO Cleared counters on DC2-SPINE2 (cEOSLab) +``` + +## Collect a set of commands + +This command collects all the commands specified in a commands-list file, which can be in either `json` or `text` format. + +### Command overview + +```bash +anta exec snapshot --help +Usage: anta exec snapshot [OPTIONS] + + Collect commands output from devices in inventory + +Options: + -u, --username TEXT Username to connect to EOS [env var: + ANTA_USERNAME; required] + -p, --password TEXT Password to connect to EOS that must be provided. + It can be prompted using '--prompt' option. [env + var: ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It + can be prompted using '--prompt' option. Requires + '--enable' option. [env var: + ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC + mode. This option tries to access this mode before + sending a command to the device. [env var: + ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. + [env var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + -c, --commands-list FILE File with list of commands to collect [env var: + ANTA_EXEC_SNAPSHOT_COMMANDS_LIST; required] + -o, --output DIRECTORY Directory to save commands output. [env var: + ANTA_EXEC_SNAPSHOT_OUTPUT; default: + anta_snapshot_2023-12-06_09_22_11] + --help Show this message and exit. +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +The commands-list file should follow this structure: + +```yaml +--- +json_format: + - show version +text_format: + - show bfd peers +``` +### Example + +```bash +anta exec snapshot --tags SPINE --commands-list ./commands.yaml --output ./ +[20:25:15] INFO Connecting to devices... utils.py:78 + INFO Collecting commands from remote devices utils.py:81 + INFO Collected command 'show version' from device DC2-SPINE1 (cEOSLab) utils.py:76 + INFO Collected command 'show version' from device DC2-SPINE2 (cEOSLab) utils.py:76 + INFO Collected command 'show version' from device DC1-SPINE1 (cEOSLab) utils.py:76 + INFO Collected command 'show version' from device DC1-SPINE2 (cEOSLab) utils.py:76 +[20:25:16] INFO Collected command 'show bfd peers' from device DC2-SPINE2 (cEOSLab) utils.py:76 + INFO Collected command 'show bfd peers' from device DC2-SPINE1 (cEOSLab) utils.py:76 + INFO Collected command 'show bfd peers' from device DC1-SPINE1 (cEOSLab) utils.py:76 + INFO Collected command 'show bfd peers' from device DC1-SPINE2 (cEOSLab) +``` + +The results of the executed commands will be stored in the output directory specified during command execution: + +```bash +tree _2023-07-14_20_25_15 +_2023-07-14_20_25_15 +├── DC1-SPINE1 +│ ├── json +│ │ └── show version.json +│ └── text +│ └── show bfd peers.log +├── DC1-SPINE2 +│ ├── json +│ │ └── show version.json +│ └── text +│ └── show bfd peers.log +├── DC2-SPINE1 +│ ├── json +│ │ └── show version.json +│ └── text +│ └── show bfd peers.log +└── DC2-SPINE2 + ├── json + │ └── show version.json + └── text + └── show bfd peers.log + +12 directories, 8 files +``` + +## Get Scheduled tech-support + +EOS offers a feature that automatically creates a tech-support archive every hour by default. These archives are stored under `/mnt/flash/schedule/tech-support`. + +```eos +leaf1#show schedule summary +Maximum concurrent jobs 1 +Prepend host name to logfile: Yes +Name At Time Last Interval Timeout Max Max Logfile Location Status + Time (mins) (mins) Log Logs + Files Size +----------------- ------------- ----------- -------------- ------------- ----------- ---------- --------------------------------- ------ +tech-support now 08:37 60 30 100 - flash:schedule/tech-support/ Success + + +leaf1#bash ls /mnt/flash/schedule/tech-support +leaf1_tech-support_2023-03-09.1337.log.gz leaf1_tech-support_2023-03-10.0837.log.gz leaf1_tech-support_2023-03-11.0337.log.gz +``` + +For Network Readiness for Use (NRFU) tests and to keep a comprehensive report of the system state before going live, ANTA provides a command-line interface that efficiently retrieves these files. + +### Command overview + +```bash +anta exec collect-tech-support --help +Usage: anta exec collect-tech-support [OPTIONS] + + Collect scheduled tech-support from EOS devices + +Options: + -u, --username TEXT Username to connect to EOS [env var: ANTA_USERNAME; + required] + -p, --password TEXT Password to connect to EOS that must be provided. It + can be prompted using '--prompt' option. [env var: + ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It can + be prompted using '--prompt' option. Requires '-- + enable' option. [env var: ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC mode. + This option tries to access this mode before sending + a command to the device. [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. [env + var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + -o, --output PATH Path for test catalog [default: ./tech-support] + --latest INTEGER Number of scheduled show-tech to retrieve + --configure Ensure devices have 'aaa authorization exec default + local' configured (required for SCP on EOS). THIS + WILL CHANGE THE CONFIGURATION OF YOUR NETWORK. + --help Show this message and exit. +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +When executed, this command fetches tech-support files and downloads them locally into a device-specific subfolder within the designated folder. You can specify the output folder with the `--output` option. + +ANTA uses SCP to download files from devices and will not trust unknown SSH hosts by default. Add the SSH public keys of your devices to your `known_hosts` file or use the `anta --insecure` option to ignore SSH host keys validation. + +The configuration `aaa authorization exec default` must be present on devices to be able to use SCP. +ANTA can automatically configure `aaa authorization exec default local` using the `anta exec collect-tech-support --configure` option. +If you require specific AAA configuration for `aaa authorization exec default`, like `aaa authorization exec default none` or `aaa authorization exec default group tacacs+`, you will need to configure it manually. + +The `--latest` option allows retrieval of a specific number of the most recent tech-support files. + +!!! warning + By default **all** the tech-support files present on the devices are retrieved. + +### Example + +```bash +anta --insecure exec collect-tech-support +[15:27:19] INFO Connecting to devices... +INFO Copying '/mnt/flash/schedule/tech-support/spine1_tech-support_2023-06-09.1315.log.gz' from device spine1 to 'tech-support/spine1' locally +INFO Copying '/mnt/flash/schedule/tech-support/leaf3_tech-support_2023-06-09.1315.log.gz' from device leaf3 to 'tech-support/leaf3' locally +INFO Copying '/mnt/flash/schedule/tech-support/leaf1_tech-support_2023-06-09.1315.log.gz' from device leaf1 to 'tech-support/leaf1' locally +INFO Copying '/mnt/flash/schedule/tech-support/leaf2_tech-support_2023-06-09.1315.log.gz' from device leaf2 to 'tech-support/leaf2' locally +INFO Copying '/mnt/flash/schedule/tech-support/spine2_tech-support_2023-06-09.1315.log.gz' from device spine2 to 'tech-support/spine2' locally +INFO Copying '/mnt/flash/schedule/tech-support/leaf4_tech-support_2023-06-09.1315.log.gz' from device leaf4 to 'tech-support/leaf4' locally +INFO Collected 1 scheduled tech-support from leaf2 +INFO Collected 1 scheduled tech-support from spine2 +INFO Collected 1 scheduled tech-support from leaf3 +INFO Collected 1 scheduled tech-support from spine1 +INFO Collected 1 scheduled tech-support from leaf1 +INFO Collected 1 scheduled tech-support from leaf4 +``` + +The output folder structure is as follows: + +```bash +tree tech-support/ +tech-support/ +├── leaf1 +│ └── leaf1_tech-support_2023-06-09.1315.log.gz +├── leaf2 +│ └── leaf2_tech-support_2023-06-09.1315.log.gz +├── leaf3 +│ └── leaf3_tech-support_2023-06-09.1315.log.gz +├── leaf4 +│ └── leaf4_tech-support_2023-06-09.1315.log.gz +├── spine1 +│ └── spine1_tech-support_2023-06-09.1315.log.gz +└── spine2 + └── spine2_tech-support_2023-06-09.1315.log.gz + +6 directories, 6 files +``` + +Each device has its own subdirectory containing the collected tech-support files. diff --git a/docs/cli/get-inventory-information.md b/docs/cli/get-inventory-information.md new file mode 100644 index 0000000..70100fe --- /dev/null +++ b/docs/cli/get-inventory-information.md @@ -0,0 +1,237 @@ +<!-- + ~ 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. + --> + +# Retrieving Inventory Information + +The ANTA CLI offers multiple entrypoints to access data from your local inventory. + +## Inventory used of examples + +Let's consider the following inventory: + +```yaml +--- +anta_inventory: + hosts: + - host: 172.20.20.101 + name: DC1-SPINE1 + tags: ["SPINE", "DC1"] + + - host: 172.20.20.102 + name: DC1-SPINE2 + tags: ["SPINE", "DC1"] + + - host: 172.20.20.111 + name: DC1-LEAF1A + tags: ["LEAF", "DC1"] + + - host: 172.20.20.112 + name: DC1-LEAF1B + tags: ["LEAF", "DC1"] + + - host: 172.20.20.121 + name: DC1-BL1 + tags: ["BL", "DC1"] + + - host: 172.20.20.122 + name: DC1-BL2 + tags: ["BL", "DC1"] + + - host: 172.20.20.201 + name: DC2-SPINE1 + tags: ["SPINE", "DC2"] + + - host: 172.20.20.202 + name: DC2-SPINE2 + tags: ["SPINE", "DC2"] + + - host: 172.20.20.211 + name: DC2-LEAF1A + tags: ["LEAF", "DC2"] + + - host: 172.20.20.212 + name: DC2-LEAF1B + tags: ["LEAF", "DC2"] + + - host: 172.20.20.221 + name: DC2-BL1 + tags: ["BL", "DC2"] + + - host: 172.20.20.222 + name: DC2-BL2 + tags: ["BL", "DC2"] +``` + +## Obtaining all configured tags + +As most of ANTA's commands accommodate tag filtering, this particular command is useful for enumerating all tags configured in the inventory. Running the `anta get tags` command will return a list of all tags that have been configured in the inventory. + +### Command overview + +```bash +anta get tags --help +Usage: anta get tags [OPTIONS] + + Get list of configured tags in user inventory. + +Options: + -u, --username TEXT Username to connect to EOS [env var: ANTA_USERNAME; + required] + -p, --password TEXT Password to connect to EOS that must be provided. It + can be prompted using '--prompt' option. [env var: + ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It can + be prompted using '--prompt' option. Requires '-- + enable' option. [env var: ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC mode. + This option tries to access this mode before sending + a command to the device. [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. [env + var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + --help Show this message and exit. +``` + +### Example + +To get the list of all configured tags in the inventory, run the following command: + +```bash +anta get tags +Tags found: +[ + "BL", + "DC1", + "DC2", + "LEAF", + "SPINE" +] + +* note that tag all has been added by anta +``` + +!!! note + Even if you haven't explicitly configured the `all` tag in the inventory, it is automatically added. This default tag allows to execute commands on all devices in the inventory when no tag is specified. + +## List devices in inventory + +This command will list all devices available in the inventory. Using the `--tags` option, you can filter this list to only include devices with specific tags. The `--connected` option allows to display only the devices where a connection has been established. + +### Command overview + +```bash +anta get inventory --help +Usage: anta get inventory [OPTIONS] + + Show inventory loaded in ANTA. + +Options: + -u, --username TEXT Username to connect to EOS [env var: + ANTA_USERNAME; required] + -p, --password TEXT Password to connect to EOS that must be + provided. It can be prompted using '--prompt' + option. [env var: ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. + It can be prompted using '--prompt' option. + Requires '--enable' option. [env var: + ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC + mode. This option tries to access this mode + before sending a command to the device. [env + var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not + provided. [env var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: + ANTA_TIMEOUT; default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + --connected / --not-connected Display inventory after connection has been + created + --help Show this message and exit. +``` + + +!!! tip + In its default mode, `anta get inventory` provides only information that doesn't rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, please use the `--connected` option. + +### Example + +To retrieve a comprehensive list of all devices along with their details, execute the following command. It will provide all the data loaded into the ANTA inventory from your [inventory file](../usage-inventory-catalog.md). + +```bash +anta get inventory --tags SPINE +Current inventory content is: +{ + 'DC1-SPINE1': AsyncEOSDevice( + name='DC1-SPINE1', + tags=['SPINE', 'DC1'], + hw_model=None, + is_online=False, + established=False, + disable_cache=False, + host='172.20.20.101', + eapi_port=443, + username='arista', + enable=True, + enable_password='arista', + insecure=False + ), + 'DC1-SPINE2': AsyncEOSDevice( + name='DC1-SPINE2', + tags=['SPINE', 'DC1'], + hw_model=None, + is_online=False, + established=False, + disable_cache=False, + host='172.20.20.102', + eapi_port=443, + username='arista', + enable=True, + insecure=False + ), + 'DC2-SPINE1': AsyncEOSDevice( + name='DC2-SPINE1', + tags=['SPINE', 'DC2'], + hw_model=None, + is_online=False, + established=False, + disable_cache=False, + host='172.20.20.201', + eapi_port=443, + username='arista', + enable=True, + insecure=False + ), + 'DC2-SPINE2': AsyncEOSDevice( + name='DC2-SPINE2', + tags=['SPINE', 'DC2'], + hw_model=None, + is_online=False, + established=False, + disable_cache=False, + host='172.20.20.202', + eapi_port=443, + username='arista', + enable=True, + insecure=False + ) +} +``` diff --git a/docs/cli/inv-from-ansible.md b/docs/cli/inv-from-ansible.md new file mode 100644 index 0000000..bb944d4 --- /dev/null +++ b/docs/cli/inv-from-ansible.md @@ -0,0 +1,72 @@ +<!-- + ~ 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. + --> + +# Create an Inventory from Ansible inventory + +In large setups, it might be beneficial to construct your inventory based on your Ansible inventory. The `from-ansible` entrypoint of the `get` command enables the user to create an ANTA inventory from Ansible. + +### Command overview + +```bash +$ anta get from-ansible --help +Usage: anta get from-ansible [OPTIONS] + + Build ANTA inventory from an ansible inventory YAML file + +Options: + -g, --ansible-group TEXT Ansible group to filter + --ansible-inventory FILENAME + Path to your ansible inventory file to read + -o, --output FILENAME Path to save inventory file + -d, --inventory-directory PATH Directory to save inventory file + --help Show this message and exit. +``` + +The output is an inventory where the name of the container is added as a tag for each host: + +```yaml +anta_inventory: + hosts: + - host: 10.73.252.41 + name: srv-pod01 + - host: 10.73.252.42 + name: srv-pod02 + - host: 10.73.252.43 + name: srv-pod03 +``` + +!!! warning + The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritence when using the `--ansible-group` option. + +By default, if user does not provide `--output` file, anta will save output to configured anta inventory (`anta --inventory`). If the output file has content, anta will ask user to overwrite when running in interactive console. This mechanism can be controlled by triggers in case of CI usage: `--overwrite` to force anta to overwrite file. If not set, anta will exit + + +### Command output + +`host` value is coming from the `ansible_host` key in your inventory while `name` is the name you defined for your host. Below is an ansible inventory example used to generate previous inventory: + +```yaml +--- +tooling: + children: + endpoints: + hosts: + srv-pod01: + ansible_httpapi_port: 9023 + ansible_port: 9023 + ansible_host: 10.73.252.41 + type: endpoint + srv-pod02: + ansible_httpapi_port: 9024 + ansible_port: 9024 + ansible_host: 10.73.252.42 + type: endpoint + srv-pod03: + ansible_httpapi_port: 9025 + ansible_port: 9025 + ansible_host: 10.73.252.43 + type: endpoint +``` diff --git a/docs/cli/inv-from-cvp.md b/docs/cli/inv-from-cvp.md new file mode 100644 index 0000000..8897370 --- /dev/null +++ b/docs/cli/inv-from-cvp.md @@ -0,0 +1,72 @@ +<!-- + ~ 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. + --> + +# Create an Inventory from CloudVision + +In large setups, it might be beneficial to construct your inventory based on CloudVision. The `from-cvp` entrypoint of the `get` command enables the user to create an ANTA inventory from CloudVision. + +### Command overview + +```bash +anta get from-cvp --help +Usage: anta get from-cvp [OPTIONS] + + Build ANTA inventory from Cloudvision + +Options: + -ip, --cvp-ip TEXT CVP IP Address [required] + -u, --cvp-username TEXT CVP Username [required] + -p, --cvp-password TEXT CVP Password / token [required] + -c, --cvp-container TEXT Container where devices are configured + -d, --inventory-directory PATH Path to save inventory file + --help Show this message and exit. +``` + +The output is an inventory where the name of the container is added as a tag for each host: + +```yaml +anta_inventory: + hosts: + - host: 192.168.0.13 + name: leaf2 + tags: + - pod1 + - host: 192.168.0.15 + name: leaf4 + tags: + - pod2 +``` + +!!! warning + The current implementation only considers devices directly attached to a specific container when using the `--cvp-container` option. + +### Creating an inventory from multiple containers + +If you need to create an inventory from multiple containers, you can use a bash command and then manually concatenate files to create a single inventory file: + +```bash +$ for container in pod01 pod02 spines; do anta get from-cvp -ip <cvp-ip> -u cvpadmin -p cvpadmin -c $container -d test-inventory; done + +[12:25:35] INFO Getting auth token from cvp.as73.inetsix.net for user tom +[12:25:36] INFO Creating inventory folder /home/tom/Projects/arista/network-test-automation/test-inventory + WARNING Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters + are for the same use case and api_token is more generic + INFO Connected to CVP cvp.as73.inetsix.net + + +[12:25:37] INFO Getting auth token from cvp.as73.inetsix.net for user tom +[12:25:38] WARNING Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters + are for the same use case and api_token is more generic + INFO Connected to CVP cvp.as73.inetsix.net + + +[12:25:38] INFO Getting auth token from cvp.as73.inetsix.net for user tom +[12:25:39] WARNING Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters + are for the same use case and api_token is more generic + INFO Connected to CVP cvp.as73.inetsix.net + + INFO Inventory file has been created in /home/tom/Projects/arista/network-test-automation/test-inventory/inventory-spines.yml +``` diff --git a/docs/cli/nrfu.md b/docs/cli/nrfu.md new file mode 100644 index 0000000..6dcc393 --- /dev/null +++ b/docs/cli/nrfu.md @@ -0,0 +1,247 @@ +<!-- + ~ 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. + --> + +# Execute Network Readiness For Use (NRFU) Testing + +ANTA provides a set of commands for performing NRFU tests on devices. These commands are under the `anta nrfu` namespace and offer multiple output format options: + +- [Text view](#performing-nrfu-with-text-rendering) +- [Table view](#performing-nrfu-with-table-rendering) +- [JSON view](#performing-nrfu-with-json-rendering) +- [Custom template view](#performing-nrfu-with-custom-reports) + +### NRFU Command overview + +```bash +anta nrfu --help +Usage: anta nrfu [OPTIONS] COMMAND [ARGS]... + + Run NRFU against inventory devices + +Options: + -u, --username TEXT Username to connect to EOS [env var: ANTA_USERNAME; + required] + -p, --password TEXT Password to connect to EOS that must be provided. It + can be prompted using '--prompt' option. [env var: + ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It can + be prompted using '--prompt' option. Requires '-- + enable' option. [env var: ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC mode. + This option tries to access this mode before sending + a command to the device. [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. [env + var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + -c, --catalog FILE Path to the test catalog YAML file [env var: + ANTA_CATALOG; required] + --ignore-status Always exit with success [env var: + ANTA_NRFU_IGNORE_STATUS] + --ignore-error Only report failures and not errors [env var: + ANTA_NRFU_IGNORE_ERROR] + --help Show this message and exit. + +Commands: + json ANTA command to check network state with JSON result + table ANTA command to check network states with table result + text ANTA command to check network states with text result + tpl-report ANTA command to check network state with templated report +``` + +> `username`, `password`, `enable-password`, `enable`, `timeout` and `insecure` values are the same for all devices + +All commands under the `anta nrfu` namespace require a catalog yaml file specified with the `--catalog` option and a device inventory file specified with the `--inventory` option. + +!!! info + Issuing the command `anta nrfu` will run `anta nrfu table` without any option. + +## Tag management + +The `--tags` option can be used to target specific devices in your inventory and run only tests configured with this specific tags from your catalog. The default tag is set to `all` and is implicit. Expected behaviour is provided below: + +| Command | Description | +| ------- | ----------- | +| `none` | Run all tests on all devices according `tag` definition in your inventory and test catalog. And tests with no tag are executed on all devices| +| `--tags leaf` | Run all tests marked with `leaf` tag on all devices configured with `leaf` tag.<br/> All other tags are ignored | +| `--tags leaf,spine` | Run all tests marked with `leaf` tag on all devices configured with `leaf` tag.<br/>Run all tests marked with `spine` tag on all devices configured with `spine` tag.<br/> All other tags are ignored | + +!!! info + [More examples](tag-management.md) available on this dedicated page. + +## Performing NRFU with text rendering + +The `text` subcommand provides a straightforward text report for each test executed on all devices in your inventory. + +### Command overview + +```bash +anta nrfu text --help +Usage: anta nrfu text [OPTIONS] + + ANTA command to check network states with text result + +Options: + -s, --search TEXT Regular expression to search in both name and test + --skip-error Hide tests in errors due to connectivity issue + --help Show this message and exit. +``` + +The `--search` option permits filtering based on a regular expression pattern in both the hostname and the test name. + +The `--skip-error` option can be used to exclude tests that failed due to connectivity issues or unsupported commands. + +### Example + +```bash +anta nrfu text --tags LEAF --search DC1-LEAF1A +``` +[![anta nrfu text results](../imgs/anta-nrfu-text-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-text-output.png) + +## Performing NRFU with table rendering + +The `table` command under the `anta nrfu` namespace offers a clear and organized table view of the test results, suitable for filtering. It also has its own set of options for better control over the output. + +### Command overview + +```bash +anta nrfu table --help +Usage: anta nrfu table [OPTIONS] + + ANTA command to check network states with table result + +Options: + -d, --device TEXT Show a summary for this device + -t, --test TEXT Show a summary for this test + --group-by [device|test] Group result by test or host. default none + --help Show this message and exit. +``` + +The `--device` and `--test` options show a summarized view of the test results for a specific host or test case, respectively. + +The `--group-by` option show a summarized view of the test results per host or per test. + +### Examples + +```bash +anta nrfu --tags LEAF table +``` +[![anta nrfu table results](../imgs/anta-nrfu-table-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-table-output.png) + +For larger setups, you can also group the results by host or test to get a summarized view: + +```bash +anta nrfu table --group-by device +``` +[![anta nrfu table group_by_host_output](../imgs/anta-nrfu-table-group-by-host-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-table-group-by-host-output.png) + +```bash +anta nrfu table --group-by test +``` +[![anta nrfu table group_by_test_output](../imgs/anta-nrfu-table-group-by-test-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-table-group-by-test-output.png) + +To get more specific information, it is possible to filter on a single device or a single test: + +```bash +anta nrfu table --device spine1 +``` +[![anta nrfu table filter_host_output](../imgs/anta-nrfu-table-filter-host-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-table-filter-host-output.png) + +```bash +anta nrfu table --test VerifyZeroTouch +``` +[![anta nrfu table filter_test_output](../imgs/anta-nrfu-table-filter-test-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-table-filter-test-output.png) + +## Performing NRFU with JSON rendering + +The JSON rendering command in NRFU testing is useful in generating a JSON output that can subsequently be passed on to another tool for reporting purposes. + +### Command overview + +```bash +anta nrfu json --help +Usage: anta nrfu json [OPTIONS] + + ANTA command to check network state with JSON result + +Options: + -o, --output FILE Path to save report as a file [env var: + ANTA_NRFU_JSON_OUTPUT] + --help Show this message and exit. +``` + +The `--output` option allows you to save the JSON report as a file. + +### Example + +```bash +anta nrfu --tags LEAF json +``` +[![anta nrfu json results](../imgs/anta-nrfu-json-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-json-output.png) + +## Performing NRFU with custom reports + +ANTA offers a CLI option for creating custom reports. This leverages the Jinja2 template system, allowing you to tailor reports to your specific needs. + +### Command overview + +```bash +anta nrfu tpl-report --help +Usage: anta nrfu tpl-report [OPTIONS] + + ANTA command to check network state with templated report + +Options: + -tpl, --template FILE Path to the template to use for the report [env var: + ANTA_NRFU_TPL_REPORT_TEMPLATE; required] + -o, --output FILE Path to save report as a file [env var: + ANTA_NRFU_TPL_REPORT_OUTPUT] + --help Show this message and exit. +``` +The `--template` option is used to specify the Jinja2 template file for generating the custom report. + +The `--output` option allows you to choose the path where the final report will be saved. + +### Example + +```bash +anta nrfu --tags LEAF tpl-report --template ./custom_template.j2 +``` +[![anta nrfu json results](../imgs/anta-nrfu-tpl-report-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-tpl-report-output.png) + +The template `./custom_template.j2` is a simple Jinja2 template: + +```j2 +{% for d in data %} +* {{ d.test }} is [green]{{ d.result | upper}}[/green] for {{ d.name }} +{% endfor %} +``` + +The Jinja2 template has access to all `TestResult` elements and their values, as described in this [documentation](../api/result_manager_models.md#testresult-entry). + +You can also save the report result to a file using the `--output` option: + +```bash +anta nrfu --tags LEAF tpl-report --template ./custom_template.j2 --output nrfu-tpl-report.txt +``` + +The resulting output might look like this: + +```bash +cat nrfu-tpl-report.txt +* VerifyMlagStatus is [green]SUCCESS[/green] for DC1-LEAF1A +* VerifyMlagInterfaces is [green]SUCCESS[/green] for DC1-LEAF1A +* VerifyMlagConfigSanity is [green]SUCCESS[/green] for DC1-LEAF1A +* VerifyMlagReloadDelay is [green]SUCCESS[/green] for DC1-LEAF1A +``` diff --git a/docs/cli/overview.md b/docs/cli/overview.md new file mode 100644 index 0000000..90e70a5 --- /dev/null +++ b/docs/cli/overview.md @@ -0,0 +1,103 @@ +<!-- + ~ 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. + --> + +# Overview of ANTA's Command-Line Interface (CLI) + +ANTA provides a powerful Command-Line Interface (CLI) to perform a wide range of operations. This document provides a comprehensive overview of ANTA CLI usage and its commands. + +ANTA can also be used as a Python library, allowing you to build your own tools based on it. Visit this [page](../advanced_usages/as-python-lib.md) for more details. + +To start using the ANTA CLI, open your terminal and type `anta`. + +!!! warning + The ANTA CLI options have changed after version 0.11 and have moved away from the top level `anta` and are now required at their respective commands (e.g. `anta nrfu`). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables. + +## Invoking ANTA CLI + +```bash +$ anta --help +--8<-- "anta_help.txt" +``` + +## ANTA environement variables + +Certain parameters are required and can be either passed to the ANTA CLI or set as an environment variable (ENV VAR). + +To pass the parameters via the CLI: + +```bash +anta nrfu -u admin -p arista123 -i inventory.yaml -c tests.yaml +``` + +To set them as environment variables: + +```bash +export ANTA_USERNAME=admin +export ANTA_PASSWORD=arista123 +export ANTA_INVENTORY=inventory.yml +export ANTA_INVENTORY=tests.yml +``` + +Then, run the CLI without options: + +```bash +anta nrfu +``` + +!!! note + All environement variables may not be needed for every commands. + Refer to `<command> --help` for the comprehensive environment varibles names. + +Below are the environement variables usable with the `anta nrfu` command: + +| Variable Name | Purpose | Required | +| ------------- | ------- |----------| +| ANTA_USERNAME | The username to use in the inventory to connect to devices. | Yes | +| ANTA_PASSWORD | The password to use in the inventory to connect to devices. | Yes | +| ANTA_INVENTORY | The path to the inventory file. | Yes | +| ANTA_CATALOG | The path to the catalog file. | Yes | +| ANTA_PROMPT | The value to pass to the prompt for password is password is not provided | No | +| ANTA_INSECURE | Whether or not using insecure mode when connecting to the EOS devices HTTP API. | No | +| ANTA_DISABLE_CACHE | A variable to disable caching for all ANTA tests (enabled by default). | No | +| ANTA_ENABLE | Whether it is necessary to go to enable mode on devices. | No | +| ANTA_ENABLE_PASSWORD | The optional enable password, when this variable is set, ANTA_ENABLE or `--enable` is required. | No | + +!!! info + Caching can be disabled with the global parameter `--disable-cache`. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](../advanced_usages/caching.md). + +## ANTA Exit Codes + +ANTA CLI utilizes the following exit codes: + +- `Exit code 0` - All tests passed successfully. +- `Exit code 1` - An internal error occurred while executing ANTA. +- `Exit code 2` - A usage error was raised. +- `Exit code 3` - Tests were run, but at least one test returned an error. +- `Exit code 4` - Tests were run, but at least one test returned a failure. + +To ignore the test status, use `anta nrfu --ignore-status`, and the exit code will always be 0. + +To ignore errors, use `anta nrfu --ignore-error`, and the exit code will be 0 if all tests succeeded or 1 if any test failed. + +## Shell Completion + +You can enable shell completion for the ANTA CLI: + +=== "ZSH" + + If you use ZSH shell, add the following line in your `~/.zshrc`: + + ```bash + eval "$(_ANTA_COMPLETE=zsh_source anta)" > /dev/null + ``` + +=== "BASH" + + With bash, add the following line in your `~/.bashrc`: + + ```bash + eval "$(_ANTA_COMPLETE=bash_source anta)" > /dev/null + ``` diff --git a/docs/cli/tag-management.md b/docs/cli/tag-management.md new file mode 100644 index 0000000..8c043d7 --- /dev/null +++ b/docs/cli/tag-management.md @@ -0,0 +1,165 @@ +<!-- + ~ 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. + --> + +# Tag management + +## Overview + +Some of the ANTA commands like `anta nrfu` command come with a `--tags` option. + +For `nrfu`, this allows users to specify a set of tests, marked with a given tag, to be run on devices marked with the same tag. For instance, you can run tests dedicated to leaf devices on your leaf devices only and not on other devices. + +Tags are string defined by the user and can be anything considered as a string by Python. A [default one](#default-tags) is present for all tests and devices. + +The next table provides a short summary of the scope of tags using CLI + +| Command | Description | +| ------- | ----------- | +| `none` | Run all tests on all devices according `tag` definition in your inventory and test catalog. And tests with no tag are executed on all devices| +| `--tags leaf` | Run all tests marked with `leaf` tag on all devices configured with `leaf` tag.<br/> All other tags are ignored | +| `--tags leaf,spine` | Run all tests marked with `leaf` tag on all devices configured with `leaf` tag.<br/>Run all tests marked with `spine` tag on all devices configured with `spine` tag.<br/> All other tags are ignored | + +## Inventory and Catalog for tests + +All commands in this page are based on the following inventory and test catalog. + +=== "Inventory" + + ```yaml + --- + anta_inventory: + hosts: + - host: 192.168.0.10 + name: spine01 + tags: ['fabric', 'spine'] + - host: 192.168.0.11 + name: spine02 + tags: ['fabric', 'spine'] + - host: 192.168.0.12 + name: leaf01 + tags: ['fabric', 'leaf'] + - host: 192.168.0.13 + name: leaf02 + tags: ['fabric', 'leaf'] + - host: 192.168.0.14 + name: leaf03 + tags: ['fabric', 'leaf'] + - host: 192.168.0.15 + name: leaf04 + tags: ['fabric', 'leaf' + ``` + +=== "Test Catalog" + + ```yaml + anta.tests.system: + - VerifyUptime: + minimum: 10 + filters: + tags: ['fabric'] + - VerifyReloadCause: + tags: ['leaf', spine'] + - VerifyCoredump: + - VerifyAgentLogs: + - VerifyCPUUtilization: + filters: + tags: ['spine', 'leaf'] + - VerifyMemoryUtilization: + - VerifyFileSystemUtilization: + - VerifyNTP: + + anta.tests.mlag: + - VerifyMlagStatus: + + + anta.tests.interfaces: + - VerifyL3MTU: + mtu: 1500 + filters: + tags: ['demo'] + ``` + +## Default tags + +By default, ANTA uses a default tag for both devices and tests. This default tag is `all` and it can be explicit if you want to make it visible in your inventory and also implicit since the framework injects this tag if it is not defined. + +So this command will run all tests from your catalog on all devices. With a mapping for `tags` defined in your inventory and catalog. If no `tags` configured, then tests are executed against all devices. + +```bash +$ anta nrfu -c .personal/catalog-class.yml table --group-by device + +╭────────────────────── Settings ──────────────────────╮ +│ Running ANTA tests: │ +│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │ +│ - Tests catalog contains 10 tests │ +╰──────────────────────────────────────────────────────╯ + +┏━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Device ┃ # of success ┃ # of skipped ┃ # of failure ┃ # of errors ┃ List of failed or error test cases ┃ +┡━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ +│ spine01 │ 5 │ 1 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +│ spine02 │ 5 │ 1 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +│ leaf01 │ 6 │ 0 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +│ leaf02 │ 6 │ 0 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +│ leaf03 │ 6 │ 0 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +│ leaf04 │ 6 │ 0 │ 1 │ 0 │ ['VerifyCPUUtilization'] │ +└─────────┴──────────────┴──────────────┴──────────────┴─────────────┴────────────────────────────────────┘ +``` + +## Use a single tag in CLI + +The most used approach is to use a single tag in your CLI to filter tests & devices configured with this one. + +In such scenario, ANTA will run tests marked with `$tag` only on devices marked with `$tag`. All other tests and devices will be ignored + +```bash +$ anta nrfu -c .personal/catalog-class.yml --tags leaf text +╭────────────────────── Settings ──────────────────────╮ +│ Running ANTA tests: │ +│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │ +│ - Tests catalog contains 10 tests │ +╰──────────────────────────────────────────────────────╯ + +leaf01 :: VerifyUptime :: SUCCESS +leaf01 :: VerifyReloadCause :: SUCCESS +leaf01 :: VerifyCPUUtilization :: SUCCESS +leaf02 :: VerifyUptime :: SUCCESS +leaf02 :: VerifyReloadCause :: SUCCESS +leaf02 :: VerifyCPUUtilization :: SUCCESS +leaf03 :: VerifyUptime :: SUCCESS +leaf03 :: VerifyReloadCause :: SUCCESS +leaf03 :: VerifyCPUUtilization :: SUCCESS +leaf04 :: VerifyUptime :: SUCCESS +leaf04 :: VerifyReloadCause :: SUCCESS +leaf04 :: VerifyCPUUtilization :: SUCCESS +``` + +In this case, only `leaf` devices defined in your [inventory](#inventory-and-catalog-for-tests) are used to run tests marked with `leaf` in your [test catalog](#inventory-and-catalog-for-tests) + +## Use multiple tags in CLI + +A more advanced usage of the tag feature is to list multiple tags in your CLI using `--tags $tag1,$tag2` syntax. + +In such scenario, all devices marked with `$tag1` will be selected and ANTA will run tests with `$tag1`, then devices with `$tag2` will be selected and will be tested with tests marked with `$tag2` + +```bash +anta nrfu -c .personal/catalog-class.yml --tags leaf,fabric text + +spine01 :: VerifyUptime :: SUCCESS +spine02 :: VerifyUptime :: SUCCESS +leaf01 :: VerifyUptime :: SUCCESS +leaf01 :: VerifyReloadCause :: SUCCESS +leaf01 :: VerifyCPUUtilization :: SUCCESS +leaf02 :: VerifyUptime :: SUCCESS +leaf02 :: VerifyReloadCause :: SUCCESS +leaf02 :: VerifyCPUUtilization :: SUCCESS +leaf03 :: VerifyUptime :: SUCCESS +leaf03 :: VerifyReloadCause :: SUCCESS +leaf03 :: VerifyCPUUtilization :: SUCCESS +leaf04 :: VerifyUptime :: SUCCESS +leaf04 :: VerifyReloadCause :: SUCCESS +leaf04 :: VerifyCPUUtilization :: SUCCESS +``` diff --git a/docs/contribution.md b/docs/contribution.md new file mode 100644 index 0000000..49df256 --- /dev/null +++ b/docs/contribution.md @@ -0,0 +1,227 @@ +<!-- + ~ 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. + --> + +# How to contribute to ANTA + +Contribution model is based on a fork-model. Don't push to arista-netdevops-community/anta directly. Always do a branch in your forked repository and create a PR. + +To help development, open your PR as soon as possible even in draft mode. It helps other to know on what you are working on and avoid duplicate PRs. + +## Create a development environement + +Run the following commands to create an ANTA development environement: + +```bash +# Clone repository +$ git clone https://github.com/arista-netdevops-community/anta.git +$ cd anta + +# Install ANTA in editable mode and its development tools +$ pip install -e .[dev] + +# Verify installation +$ pip list -e +Package Version Editable project location +------- ------- ------------------------- +anta 0.13.0 /mnt/lab/projects/anta +``` + +Then, [`tox`](https://tox.wiki/) is configued with few environments to run CI locally: + +```bash +$ tox list -d +default environments: +clean -> Erase previous coverage reports +lint -> Check the code style +type -> Check typing +py38 -> Run pytest with py38 +py39 -> Run pytest with py39 +py310 -> Run pytest with py310 +py311 -> Run pytest with py311 +report -> Generate coverage report +``` + +### Code linting + +```bash +tox -e lint +[...] +lint: commands[0]> black --check --diff --color . +All done! ✨ 🍰 ✨ +104 files would be left unchanged. +lint: commands[1]> isort --check --diff --color . +Skipped 7 files +lint: commands[2]> flake8 --max-line-length=165 --config=/dev/null anta +lint: commands[3]> flake8 --max-line-length=165 --config=/dev/null tests +lint: commands[4]> pylint anta + +-------------------------------------------------------------------- +Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00) + +.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta + lint: OK (19.26=setup[5.83]+cmd[1.50,0.76,1.19,1.20,8.77] seconds) + congratulations :) (19.56 seconds) +``` + +### Code Typing + +```bash +tox -e type + +[...] +type: commands[0]> mypy --config-file=pyproject.toml anta +Success: no issues found in 52 source files +.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta + type: OK (46.66=setup[24.20]+cmd[22.46] seconds) + congratulations :) (47.01 seconds) +``` + +> NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares. + +## Unit tests + +To keep high quality code, we require to provide a Pytest for every tests implemented in ANTA. + +All submodule should have its own pytest section under `tests/units/anta_tests/<submodule-name>.py`. + +### How to write a unit test for an AntaTest subclass + +The Python modules in the `tests/units/anta_tests` folder define test parameters for AntaTest subclasses unit tests. +A generic test function is written for all unit tests in `tests.lib.anta` module. +The `pytest_generate_tests` function definition in `conftest.py` is called during test collection. +The `pytest_generate_tests` function will parametrize the generic test function based on the `DATA` data structure defined in `tests.units.anta_tests` modules. +See https://docs.pytest.org/en/7.3.x/how-to/parametrize.html#basic-pytest-generate-tests-example + +The `DATA` structure is a list of dictionaries used to parametrize the test. +The list elements have the following keys: +- `name` (str): Test name as displayed by Pytest. +- `test` (AntaTest): An AntaTest subclass imported in the test module - e.g. VerifyUptime. +- `eos_data` (list[dict]): List of data mocking EOS returned data to be passed to the test. +- `inputs` (dict): Dictionary to instantiate the `test` inputs as defined in the class from `test`. +- `expected` (dict): Expected test result structure, a dictionary containing a key + `result` containing one of the allowed status (`Literal['success', 'failure', 'unset', 'skipped', 'error']`) and optionally a key `messages` which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object. + + +In order for your unit tests to be correctly collected, you need to import the generic test function even if not used in the Python module. + +Test example for `anta.tests.system.VerifyUptime` AntaTest. + +``` python +# Import the generic test function +from tests.lib.anta import test # noqa: F401 + +# Import your AntaTest +from anta.tests.system import VerifyUptime + +# Define test parameters +DATA: list[dict[str, Any]] = [ + { + # Arbitrary test name + "name": "success", + # Must be an AntaTest definition + "test": VerifyUptime, + # Data returned by EOS on which the AntaTest is tested + "eos_data": [{"upTime": 1186689.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], + # Dictionary to instantiate VerifyUptime.Input + "inputs": {"minimum": 666}, + # Expected test result + "expected": {"result": "success"}, + }, + { + "name": "failure", + "test": VerifyUptime, + "eos_data": [{"upTime": 665.15, "loadAvg": [0.13, 0.12, 0.09], "users": 1, "currentTime": 1683186659.139859}], + "inputs": {"minimum": 666}, + # If the test returns messages, it needs to be expected otherwise test will fail. + # NB: expected messages only needs to be included in messages returned by the test. Exact match is not required. + "expected": {"result": "failure", "messages": ["Device uptime is 665.15 seconds"]}, + }, +] +``` + +## Git Pre-commit hook + +```bash +pip install pre-commit +pre-commit install +``` + +When running a commit or a pre-commit check: + +``` bash +❯ echo "import foobaz" > test.py && git add test.py +❯ pre-commit +pylint...................................................................Failed +- hook id: pylint +- exit code: 22 + +************* Module test +test.py:1:0: C0114: Missing module docstring (missing-module-docstring) +test.py:1:0: E0401: Unable to import 'foobaz' (import-error) +test.py:1:0: W0611: Unused import foobaz (unused-import) +``` + +> NOTE: It could happen that pre-commit and tox disagree on something, in that case please open an issue on Github so we can take a look.. It is most probably wrong configuration on our side. + +## Configure MYPYPATH + +In some cases, mypy can complain about not having `MYPYPATH` configured in your shell. It is especially the case when you update both an anta test and its unit test. So you can configure this environment variable with: + +```bash +# Option 1: use local folder +export MYPYPATH=. + +# Option 2: use absolute path +export MYPYPATH=/path/to/your/local/anta/repository +``` + +## Documentation + +[`mkdocs`](https://www.mkdocs.org/) is used to generate the documentation. A PR should always update the documentation to avoid documentation debt. + +### Install documentation requirements + +Run pip to install the documentation requirements from the root of the repo: + +```bash +pip install -e .[doc] +``` + +### Testing documentation + +You can then check locally the documentation using the following command from the root of the repo: + +```bash +mkdocs serve +``` + +By default, `mkdocs` listens to http://127.0.0.1:8000/, if you need to expose the documentation to another IP or port (for instance all IPs on port 8080), use the following command: + +```bash +mkdocs serve --dev-addr=0.0.0.0:8080 +``` + +### Build class diagram + +To build class diagram to use in API documentation, you can use `pyreverse` part of `pylint` with [`graphviz`](https://graphviz.org/) installed for jpeg generation. + +```bash +pyreverse anta --colorized -a1 -s1 -o jpeg -m true -k --output-directory docs/imgs/uml/ -c <FQDN anta class> +``` + +Image will be generated under `docs/imgs/uml/` and can be inserted in your documentation. + +### Checking links + +Writing documentation is crucial but managing links can be cumbersome. To be sure there is no dead links, you can use [`muffet`](https://github.com/raviqqe/muffet) with the following command: + +```bash +muffet -c 2 --color=always http://127.0.0.1:8000 -e fonts.gstatic.com +``` + +## Continuous Integration + +GitHub actions is used to test git pushes and pull requests. The workflows are defined in this [directory](https://github.com/arista-netdevops-community/anta/tree/main/.github/workflows). We can view the results [here](https://github.com/arista-netdevops-community/anta/actions). diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..7c995ac --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,67 @@ +<!-- + ~ 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. + --> + +# Frequently Asked Questions (FAQ) + +## Why am I seeing an `ImportError` related to `urllib3` when running ANTA? + +When running the `anta --help` command, some users might encounter the following error: + +```bash +ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'OpenSSL 1.0.2k-fips 26 Jan 2017'. See: https://github.com/urllib3/urllib3/issues/2168 +``` + +This error arises due to a compatibility issue between `urllib3` v2.0 and older versions of OpenSSL. + +#### How can I resolve this error? + +1. _Workaround_: Downgrade `urllib3` + + If you need a quick fix, you can temporarily downgrade the `urllib3` package: + + ```bash + pip3 uninstall urllib3 + + pip3 install urllib3==1.26.15 + ``` + +2. _Recommended_: Upgrade System or Libraries: + + As per the [urllib3 v2 migration guide](https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html), the root cause of this error is an incompatibility with older OpenSSL versions. For example, users on RHEL7 might consider upgrading to RHEL8, which supports the required OpenSSL version. + +## Why am I seeing `AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'` when running ANTA + +When running the `anta` commands after installation, some users might encounter the following error: + +```bash +AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms' +``` + +The error is a result of incompatibility between `cryptography` and `pyopenssl` when installing `asyncssh` which is a requirement of ANTA. + +#### How can I resolve this error? + +1. Upgrade `pyopenssl` + + ```bash + pip install -U pyopenssl>22.0 + ``` + +## `__NSCFConstantString initialize` error on OSX + +This error occurs because of added security to restrict multithreading in macOS High Sierra and later versions of macOS. https://www.wefearchange.org/2018/11/forkmacos.rst.html + +#### How can I resolve this error? + +1. Set the following environment variable + + ```bash + export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES + ``` + +## Still facing issues? + +If you've tried the above solutions and continue to experience problems, please report the issue in our [GitHub repository](https://github.com/arista-netdevops-community/anta). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..fd147e7 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,301 @@ +<!-- + ~ 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. + --> + +# Getting Started + +This section shows how to use ANTA with basic configuration. All examples are based on Arista Test Drive (ATD) topology you can access by reaching out to your preferred SE. + +## Installation + +The easiest way to intall ANTA package is to run Python (`>=3.8`) and its pip package to install: + +```bash +pip install anta +``` + +For more details about how to install package, please see the [requirements and intallation](./requirements-and-installation.md) section. + +## Configure Arista EOS devices + +For ANTA to be able to connect to your target devices, you need to configure your management interface + +```eos +vrf instance MGMT +! +interface Management0 + description oob_management + vrf MGMT + ip address 192.168.0.10/24 +! +``` + +Then, configure access to eAPI: + +```eos +! +management api http-commands + protocol https port 443 + no shutdown + vrf MGMT + no shutdown + ! +! +``` + +## Create your inventory + +ANTA uses an inventory to list the target devices for the tests. You can create a file manually with this format: + +```yaml +anta_inventory: + hosts: + - host: 192.168.0.10 + name: spine01 + tags: ['fabric', 'spine'] + - host: 192.168.0.11 + name: spine02 + tags: ['fabric', 'spine'] + - host: 192.168.0.12 + name: leaf01 + tags: ['fabric', 'leaf'] + - host: 192.168.0.13 + name: leaf02 + tags: ['fabric', 'leaf'] + - host: 192.168.0.14 + name: leaf03 + tags: ['fabric', 'leaf'] + - host: 192.168.0.15 + name: leaf04 + tags: ['fabric', 'leaf'] +``` + +> You can read more details about how to build your inventory [here](usage-inventory-catalog.md#create-an-inventory-file) + +## Test Catalog + +To test your network, ANTA relies on a test catalog to list all the tests to run against your inventory. A test catalog references python functions into a yaml file. + +The structure to follow is like: + +```yaml +<anta_tests_submodule>: + - <anta_tests_submodule function name>: + <test function option>: + <test function option value> +``` + +> You can read more details about how to build your catalog [here](usage-inventory-catalog.md#test-catalog) + +Here is an example for basic tests: + +```yaml +# Load anta.tests.software +anta.tests.software: + - VerifyEOSVersion: # Verifies the device is running one of the allowed EOS version. + versions: # List of allowed EOS versions. + - 4.25.4M + - 4.26.1F + - '4.28.3M-28837868.4283M (engineering build)' + - VerifyTerminAttrVersion: + versions: + - v1.22.1 + +anta.tests.system: + - VerifyUptime: # Verifies the device uptime is higher than a value. + minimum: 1 + - VerifyNTP: + - VerifySyslog: + +anta.tests.mlag: + - VerifyMlagStatus: + - VerifyMlagInterfaces: + - VerifyMlagConfigSanity: + +anta.tests.configuration: + - VerifyZeroTouch: # Verifies ZeroTouch is disabled. + - VerifyRunningConfigDiffs: +``` + +## Test your network + +ANTA comes with a generic CLI entrypoint to run tests in your network. It requires an inventory file as well as a test catalog. + +This entrypoint has multiple options to manage test coverage and reporting. + +```bash +# Generic ANTA options +$ anta +--8<-- "anta_help.txt" +``` + +```bash +# NRFU part of ANTA +Usage: anta nrfu [OPTIONS] COMMAND [ARGS]... + + Run ANTA tests on devices + +Options: + -u, --username TEXT Username to connect to EOS [env var: ANTA_USERNAME; + required] + -p, --password TEXT Password to connect to EOS that must be provided. It + can be prompted using '--prompt' option. [env var: + ANTA_PASSWORD] + --enable-password TEXT Password to access EOS Privileged EXEC mode. It can + be prompted using '--prompt' option. Requires '-- + enable' option. [env var: ANTA_ENABLE_PASSWORD] + --enable Some commands may require EOS Privileged EXEC mode. + This option tries to access this mode before sending + a command to the device. [env var: ANTA_ENABLE] + -P, --prompt Prompt for passwords if they are not provided. [env + var: ANTA_PROMPT] + --timeout INTEGER Global connection timeout [env var: ANTA_TIMEOUT; + default: 30] + --insecure Disable SSH Host Key validation [env var: + ANTA_INSECURE] + --disable-cache Disable cache globally [env var: + ANTA_DISABLE_CACHE] + -i, --inventory FILE Path to the inventory YAML file [env var: + ANTA_INVENTORY; required] + -t, --tags TEXT List of tags using comma as separator: + tag1,tag2,tag3 [env var: ANTA_TAGS] + -c, --catalog FILE Path to the test catalog YAML file [env var: + ANTA_CATALOG; required] + --ignore-status Always exit with success [env var: + ANTA_NRFU_IGNORE_STATUS] + --ignore-error Only report failures and not errors [env var: + ANTA_NRFU_IGNORE_ERROR] + --help Show this message and exit. + +Commands: + json ANTA command to check network state with JSON result + table ANTA command to check network states with table result + text ANTA command to check network states with text result + tpl-report ANTA command to check network state with templated report +``` + +To run the NRFU, you need to select an output format amongst ["json", "table", "text", "tpl-report"]. For a first usage, `table` is recommended. By default all test results for all devices are rendered but it can be changed to a report per test case or per host + +### Default report using table + +```bash +anta nrfu \ + --username tom \ + --password arista123 \ + --enable \ + --enable-password t \ + --inventory .personal/inventory_atd.yml \ + --catalog .personal/tests-bases.yml \ + table --tags leaf + + +╭────────────────────── Settings ──────────────────────╮ +│ Running ANTA tests: │ +│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │ +│ - Tests catalog contains 10 tests │ +╰──────────────────────────────────────────────────────╯ +[10:17:24] INFO Running ANTA tests... runner.py:75 + • Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:02 • 0:00:00 + + All tests results +┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ +┃ Device IP ┃ Test Name ┃ Test Status ┃ Message(s) ┃ Test description ┃ Test category ┃ +┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ +│ leaf01 │ VerifyEOSVersion │ success │ │ Verifies the device is running one of the allowed EOS version. │ software │ +│ leaf01 │ VerifyTerminAttrVersion │ success │ │ Verifies the device is running one of the allowed TerminAttr │ software │ +│ │ │ │ │ version. │ │ +│ leaf01 │ VerifyUptime │ success │ │ Verifies the device uptime is higher than a value. │ system │ +│ leaf01 │ VerifyNTP │ success │ │ Verifies NTP is synchronised. │ system │ +│ leaf01 │ VerifySyslog │ success │ │ Verifies the device had no syslog message with a severity of warning │ system │ +│ │ │ │ │ (or a more severe message) during the last 7 days. │ │ +│ leaf01 │ VerifyMlagStatus │ skipped │ MLAG is disabled │ This test verifies the health status of the MLAG configuration. │ mlag │ +│ leaf01 │ VerifyMlagInterfaces │ skipped │ MLAG is disabled │ This test verifies there are no inactive or active-partial MLAG │ mlag │ +[...] +│ leaf04 │ VerifyMlagConfigSanity │ skipped │ MLAG is disabled │ This test verifies there are no MLAG config-sanity inconsistencies. │ mlag │ +│ leaf04 │ VerifyZeroTouch │ success │ │ Verifies ZeroTouch is disabled. │ configuration │ +│ leaf04 │ VerifyRunningConfigDiffs │ success │ │ │ configuration │ +└───────────┴──────────────────────────┴─────────────┴──────────────────┴──────────────────────────────────────────────────────────────────────┴───────────────┘ +``` + +### Report in text mode + +```bash +$ anta nrfu \ + --username tom \ + --password arista123 \ + --enable \ + --enable-password t \ + --inventory .personal/inventory_atd.yml \ + --catalog .personal/tests-bases.yml \ + text --tags leaf + +╭────────────────────── Settings ──────────────────────╮ +│ Running ANTA tests: │ +│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │ +│ - Tests catalog contains 10 tests │ +╰──────────────────────────────────────────────────────╯ +[10:20:47] INFO Running ANTA tests... runner.py:75 + • Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:01 • 0:00:00 +leaf01 :: VerifyEOSVersion :: SUCCESS +leaf01 :: VerifyTerminAttrVersion :: SUCCESS +leaf01 :: VerifyUptime :: SUCCESS +leaf01 :: VerifyNTP :: SUCCESS +leaf01 :: VerifySyslog :: SUCCESS +leaf01 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled) +leaf01 :: VerifyMlagInterfaces :: SKIPPED (MLAG is disabled) +leaf01 :: VerifyMlagConfigSanity :: SKIPPED (MLAG is disabled) +[...] +``` + +### Report in JSON format + +```bash +$ anta nrfu \ + --username tom \ + --password arista123 \ + --enable \ + --enable-password t \ + --inventory .personal/inventory_atd.yml \ + --catalog .personal/tests-bases.yml \ + json --tags leaf + +╭────────────────────── Settings ──────────────────────╮ +│ Running ANTA tests: │ +│ - ANTA Inventory contains 6 devices (AsyncEOSDevice) │ +│ - Tests catalog contains 10 tests │ +╰──────────────────────────────────────────────────────╯ +[10:21:51] INFO Running ANTA tests... runner.py:75 + • Running NRFU Tests...100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40/40 • 0:00:02 • 0:00:00 +╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ JSON results of all tests │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +[ + { + "name": "leaf01", + "test": "VerifyEOSVersion", + "categories": [ + "software" + ], + "description": "Verifies the device is running one of the allowed EOS version.", + "result": "success", + "messages": [], + "custom_field": "None", + }, + { + "name": "leaf01", + "test": "VerifyTerminAttrVersion", + "categories": [ + "software" + ], + "description": "Verifies the device is running one of the allowed TerminAttr version.", + "result": "success", + "messages": [], + "custom_field": "None", + }, +[...] +] +``` + +You can find more information under the __usage__ section of the website diff --git a/docs/imgs/animated-svg.md b/docs/imgs/animated-svg.md new file mode 100644 index 0000000..6a27a50 --- /dev/null +++ b/docs/imgs/animated-svg.md @@ -0,0 +1,8 @@ +<!-- + ~ 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. + --> + +Repository: https://github.com/marionebl/svg-term-cli +Command: `cat anta-nrfu.cast | svg-term --height 10 --window --out anta.svg` diff --git a/docs/imgs/anta-nrfu-json-output.png b/docs/imgs/anta-nrfu-json-output.png Binary files differnew file mode 100755 index 0000000..7ab22b9 --- /dev/null +++ b/docs/imgs/anta-nrfu-json-output.png diff --git a/docs/imgs/anta-nrfu-table-filter-host-output.png b/docs/imgs/anta-nrfu-table-filter-host-output.png Binary files differnew file mode 100644 index 0000000..bed3796 --- /dev/null +++ b/docs/imgs/anta-nrfu-table-filter-host-output.png diff --git a/docs/imgs/anta-nrfu-table-filter-test-output.png b/docs/imgs/anta-nrfu-table-filter-test-output.png Binary files differnew file mode 100644 index 0000000..c6ae155 --- /dev/null +++ b/docs/imgs/anta-nrfu-table-filter-test-output.png diff --git a/docs/imgs/anta-nrfu-table-group-by-host-output.png b/docs/imgs/anta-nrfu-table-group-by-host-output.png Binary files differnew file mode 100644 index 0000000..8329d2b --- /dev/null +++ b/docs/imgs/anta-nrfu-table-group-by-host-output.png diff --git a/docs/imgs/anta-nrfu-table-group-by-test-output.png b/docs/imgs/anta-nrfu-table-group-by-test-output.png Binary files differnew file mode 100644 index 0000000..f9f8115 --- /dev/null +++ b/docs/imgs/anta-nrfu-table-group-by-test-output.png diff --git a/docs/imgs/anta-nrfu-table-output.png b/docs/imgs/anta-nrfu-table-output.png Binary files differnew file mode 100644 index 0000000..1c9ff62 --- /dev/null +++ b/docs/imgs/anta-nrfu-table-output.png diff --git a/docs/imgs/anta-nrfu-table-per-host-output.png b/docs/imgs/anta-nrfu-table-per-host-output.png Binary files differnew file mode 100755 index 0000000..c82ce4d --- /dev/null +++ b/docs/imgs/anta-nrfu-table-per-host-output.png diff --git a/docs/imgs/anta-nrfu-text-output.png b/docs/imgs/anta-nrfu-text-output.png Binary files differnew file mode 100755 index 0000000..2a4d6be --- /dev/null +++ b/docs/imgs/anta-nrfu-text-output.png diff --git a/docs/imgs/anta-nrfu-tpl-report-output.png b/docs/imgs/anta-nrfu-tpl-report-output.png Binary files differnew file mode 100755 index 0000000..df1c30a --- /dev/null +++ b/docs/imgs/anta-nrfu-tpl-report-output.png diff --git a/docs/imgs/anta-nrfu.cast b/docs/imgs/anta-nrfu.cast new file mode 100644 index 0000000..dcad1ec --- /dev/null +++ b/docs/imgs/anta-nrfu.cast @@ -0,0 +1,64 @@ +{"version": 2, "width": 121, "height": 56, "timestamp": 1689845383, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} +[0.182046, "o", "\u001b[?2004h❯ "] +[0.930909, "o", "a"] +[1.124462, "o", "n"] +[1.296703, "o", "t"] +[1.467422, "o", "a"] +[1.63869, "o", " "] +[1.822924, "o", "n"] +[1.967039, "o", "r"] +[2.147509, "o", "f"] +[2.323682, "o", "u"] +[2.419445, "o", " "] +[2.638124, "o", "t"] +[2.795029, "o", "a"] +[3.006538, "o", "b"] +[3.151247, "o", "l"] +[3.286674, "o", "e"] +[3.697852, "o", "\r\n\u001b[?2004l\r"] +[3.989809, "o", "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[32mSettings\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\r\n\u001b[36m│\u001b[0m\u001b[36m \u001b[0m\u001b[36mRunning ANTA tests:\u001b[0m\u001b[36m \u001b[0m\u001b[36m \u001b[0m\u001b[36m│\u001b[0m\r\n\u001b[36m│\u001b[0m\u001b[36m \u001b[0m\u001b[36m- ANTA Inventory contains 6 devices (AsyncEOSDevice)\u001b[0m\u001b[36m \u001b[0m\u001b[36m│\u001b[0m\r\n\u001b[36m│\u001b[0m\u001b[36m \u001b[0m\u001b[36m- Tests catalog contains 89 tests\u001b[0m\u001b[36m \u001b[0m\u001b[36m \u001b[0m\u001b[36m│\u001b[0m\r\n\u001b[36m╰──────────────────────────────────────────────────────╯\u001b[0m\r\n"] +[3.989919, "o", "\u001b[?25l"] +[4.033083, "o", "\r\u001b[2K\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 0%\u001b[0m \u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 0/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m-:--:--\u001b[0m"] +[4.035615, "o", "\r\u001b[2K\u001b[2;36m[11:29:47]\u001b[0m\u001b[2;36m \u001b[0m\u001b[34mINFO \u001b[0m Running ANTA tests\u001b[33m...\u001b[0m \u001b]8;id=396032;file:///mnt/lab/projects/anta/anta/runner.py\u001b\\\u001b[2mrunner.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=993683;file:///mnt/lab/projects/anta/anta/runner.py#71\u001b\\\u001b[2m71\u001b[0m\u001b]8;;\u001b\\\r\n\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 0%\u001b[0m \u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 0/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m-:--:--\u001b[0m"] +[4.100083, "o", "\r\u001b[2K\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.20303, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.304638, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.407072, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.509771, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.612195, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.714541, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.850439, "o", "\r\u001b[2K\u001b[32m(🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 15%\u001b[0m \u001b[38;5;197m━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 78/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[4.954054, "o", "\r\u001b[2K\u001b[32m(🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 18%\u001b[0m \u001b[38;5;197m━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m 98/534\u001b[0m • \u001b[33m0:00:00\u001b[0m • \u001b[36m0:00:05\u001b[0m"] +[5.056909, "o", "\r\u001b[2K\u001b[32m(🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 35%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m189/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:02\u001b[0m"] +[5.158885, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 55%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m293/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.260648, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 58%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m311/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.361906, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 63%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m335/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.463151, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 67%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m360/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.56479, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 71%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m378/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.66611, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 72%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━\u001b[0m \u001b[32m383/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.767251, "o", "\r\u001b[2K\u001b[32m( 🐌)\u001b[0m • Running NRFU Tests...\u001b[35m 72%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━\u001b[0m \u001b[32m385/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.868443, "o", "\r\u001b[2K\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 76%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━\u001b[0m \u001b[32m407/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[5.969691, "o", "\r\u001b[2K\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 81%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━\u001b[0m \u001b[32m433/534\u001b[0m • \u001b[33m0:00:01\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.072113, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 84%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━━━\u001b[0m \u001b[32m447/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.173693, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 85%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━━━━━━━━\u001b[0m \u001b[32m456/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.275903, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 87%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━━\u001b[0m \u001b[32m467/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.377199, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 89%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━━\u001b[0m \u001b[32m476/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.478317, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 91%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━━━\u001b[0m \u001b[32m486/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.579526, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 95%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m\u001b[38;5;237m━━\u001b[0m \u001b[32m505/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.680856, "o", "\r\u001b[2K\u001b[32m(🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 98%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m\u001b[38;5;237m━\u001b[0m \u001b[32m521/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.782108, "o", "\r\u001b[2K\u001b[32m(🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;237m╺\u001b[0m \u001b[32m528/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.883283, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[6.98442, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:02\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.085424, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.186472, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.287535, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.388642, "o", "\r\u001b[2K\u001b[32m( 🐌 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.489818, "o", "\r\u001b[2K\u001b[32m( 🐌)\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.590902, "o", "\r\u001b[2K\u001b[32m( 🐌)\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.692031, "o", "\r\u001b[2K\u001b[32m( 🐜)\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.793107, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.894156, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[7.995198, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m 99%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m531/534\u001b[0m • \u001b[33m0:00:03\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[8.096303, "o", "\r\u001b[2K\u001b[32m( 🐜 )\u001b[0m • Running NRFU Tests...\u001b[35m100%\u001b[0m \u001b[38;5;197m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[38;5;197m╸\u001b[0m \u001b[32m533/534\u001b[0m • \u001b[33m0:00:04\u001b[0m • \u001b[36m0:00:01\u001b[0m"] +[8.146442, "o", "\r\u001b[2K • Running NRFU Tests...\u001b[35m100%\u001b[0m \u001b[38;5;70m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m534/534\u001b[0m • \u001b[33m0:00:04\u001b[0m • \u001b[36m0:00:00\u001b[0m\r\n\u001b[?25h"] +[8.22014, "o", "\u001b[?2004h❯ "] diff --git a/docs/imgs/anta-nrfu.svg b/docs/imgs/anta-nrfu.svg new file mode 100644 index 0000000..c01eabb --- /dev/null +++ b/docs/imgs/anta-nrfu.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1250" height="277.1"><rect width="1250" height="277.1" rx="5" ry="5" class="a"/><svg y="0%" x="0%"><circle cx="20" cy="20" r="6" fill="#ff5f58"/><circle cx="40" cy="20" r="6" fill="#ffbd2e"/><circle cx="60" cy="20" r="6" fill="#18c132"/></svg><svg height="217.1" viewBox="0 0 121 21.71" width="1210" x="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="50"><style>@keyframes q{0%{transform:translateX(0)}2.2%{transform:translateX(-121px)}11.3%{transform:translateX(-242px)}13.7%{transform:translateX(-363px)}15.8%{transform:translateX(-484px)}17.9%{transform:translateX(-605px)}19.9%{transform:translateX(-726px)}22.2%{transform:translateX(-847px)}23.9%{transform:translateX(-968px)}26.1%{transform:translateX(-1089px)}28.3%{transform:translateX(-1210px)}29.4%{transform:translateX(-1331px)}32.1%{transform:translateX(-1452px)}34%{transform:translateX(-1573px)}36.6%{transform:translateX(-1694px)}38.3%{transform:translateX(-1815px)}40%{transform:translateX(-1936px)}45%{transform:translateX(-2057px)}48.5%{transform:translateX(-2299px)}49.1%{transform:translateX(-2541px)}49.9%{transform:translateX(-2662px)}51.1%{transform:translateX(-2783px)}52.4%{transform:translateX(-2904px)}53.6%{transform:translateX(-3025px)}54.9%{transform:translateX(-3146px)}56.1%{transform:translateX(-3267px)}57.4%{transform:translateX(-3388px)}59%{transform:translateX(-3509px)}60.3%{transform:translateX(-3630px)}61.5%{transform:translateX(-3751px)}62.8%{transform:translateX(-3872px)}64%{transform:translateX(-3993px)}65.2%{transform:translateX(-4114px)}66.5%{transform:translateX(-4235px)}67.7%{transform:translateX(-4356px)}68.9%{transform:translateX(-4477px)}70.2%{transform:translateX(-4598px)}71.4%{transform:translateX(-4719px)}72.6%{transform:translateX(-4840px)}73.9%{transform:translateX(-4961px)}75.1%{transform:translateX(-5082px)}76.3%{transform:translateX(-5203px)}77.6%{transform:translateX(-5324px)}78.8%{transform:translateX(-5445px)}80%{transform:translateX(-5566px)}81.3%{transform:translateX(-5687px)}82.5%{transform:translateX(-5808px)}83.7%{transform:translateX(-5929px)}85%{transform:translateX(-6050px)}86.2%{transform:translateX(-6171px)}87.4%{transform:translateX(-6292px)}88.7%{transform:translateX(-6413px)}89.9%{transform:translateX(-6534px)}91.1%{transform:translateX(-6655px)}92.3%{transform:translateX(-6776px)}93.6%{transform:translateX(-6897px)}94.8%{transform:translateX(-7018px)}96%{transform:translateX(-7139px)}97.3%{transform:translateX(-7260px)}98.5%{transform:translateX(-7381px)}99.1%{transform:translateX(-7502px)}to{transform:translateX(-7623px)}}.a{fill:#282d35}.f{fill:#b9c0cb;white-space:pre}.g,.h,.i,.j,.k,.m{fill:#66c2cd;white-space:pre}.h,.i,.j,.k,.m{fill:#a8cc8c}.i,.j,.k,.m{fill:#d290e4}.j,.k,.m{fill:#3a3a3a}.k,.m{fill:#dbab79}.m{fill:#ff005f}</style><g font-family="Monaco,Consolas,Menlo,'Bitstream Vera Sans Mono','Powerline Symbols',monospace" font-size="1.67"><defs><symbol id="1"><text y="1.67" class="f">❯</text></symbol><symbol id="2"><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text></symbol><symbol id="3"><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text></symbol><symbol id="4"><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text><text x="12.024" y="1.67" class="f">table</text></symbol><symbol id="5"><text y="1.67" class="g">╭──────────────────────</text><text x="24.048" y="1.67" class="h">Settings</text><text x="33.066" y="1.67" class="g">──────────────────────╮</text></symbol><symbol id="6"><text y="1.67" class="g">│</text><text x="2.004" y="1.67" class="g">Running</text><text x="10.02" y="1.67" class="g">ANTA</text><text x="15.03" y="1.67" class="g">tests:</text><text x="55.11" y="1.67" class="g">│</text></symbol><symbol id="7"><text y="1.67" class="g">│</text><text x="2.004" y="1.67" class="g">-</text><text x="4.008" y="1.67" class="g">ANTA</text><text x="9.018" y="1.67" class="g">Inventory</text><text x="19.038" y="1.67" class="g">contains</text><text x="28.056" y="1.67" class="g">6</text><text x="30.06" y="1.67" class="g">devices</text><text x="38.076" y="1.67" class="g">(AsyncEOSDevice)</text><text x="55.11" y="1.67" class="g">│</text></symbol><symbol id="8"><text y="1.67" class="g">│</text><text x="2.004" y="1.67" class="g">-</text><text x="4.008" y="1.67" class="g">Tests</text><text x="10.02" y="1.67" class="g">catalog</text><text x="18.036" y="1.67" class="g">contains</text><text x="27.054" y="1.67" class="g">89</text><text x="30.06" y="1.67" class="g">tests</text><text x="55.11" y="1.67" class="g">│</text></symbol><symbol id="9"><text y="1.67" class="g">╰──────────────────────────────────────────────────────╯</text></symbol><symbol id="10"><text y="1.67" class="h">(</text><text x="6.012" y="1.67" class="h">🐜)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="35.07" y="1.67" class="i">0%</text><text x="38.076" y="1.67" class="j">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="96.192" y="1.67" class="h">0/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:00</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">-:--:--</text></symbol><symbol id="11"><text y="1.67" class="g">[11:29:47]</text><text x="11.022" y="1.67" style="white-space:pre" fill="#71bef2">INFO</text><text x="20.04" y="1.67" class="f">Running</text><text x="28.056" y="1.67" class="f">ANTA</text><text x="33.066" y="1.67" class="f">tests</text><text x="38.076" y="1.67" class="k">...</text><text x="109.218" y="1.67" class="f">runner.py:71</text></symbol><symbol id="12"><text y="1.67" class="h">(</text><text x="5.01" y="1.67" class="h">🐜</text><text x="8.016" y="1.67" class="h">)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">15%</text><text x="38.076" y="1.67" class="m">━━━━━━━━</text><text x="46.092" y="1.67" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="1.67" class="h">78/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:00</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="13"><text y="1.67" class="h">(</text><text x="3.006" y="1.67" class="h">🐜</text><text x="8.016" y="1.67" class="h">)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">15%</text><text x="38.076" y="1.67" class="m">━━━━━━━━</text><text x="46.092" y="1.67" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="1.67" class="h">78/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:00</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="14"><text y="1.67" class="h">(</text><text x="2.004" y="1.67" class="h">🐌</text><text x="8.016" y="1.67" class="h">)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">99%</text><text x="38.076" y="1.67" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="1.67" class="h">531/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:02</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="15"><text y="1.67" class="h">(</text><text x="4.008" y="1.67" class="h">🐌</text><text x="8.016" y="1.67" class="h">)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">99%</text><text x="38.076" y="1.67" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="1.67" class="h">531/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:03</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="16"><text y="1.67" class="h">(</text><text x="6.012" y="1.67" class="h">🐌)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">99%</text><text x="38.076" y="1.67" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="1.67" class="h">531/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:03</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="17"><text y="1.67" class="h">(</text><text x="5.01" y="1.67" class="h">🐜</text><text x="8.016" y="1.67" class="h">)</text><text x="10.02" y="1.67" class="f">•</text><text x="12.024" y="1.67" class="f">Running</text><text x="20.04" y="1.67" class="f">NRFU</text><text x="25.05" y="1.67" class="f">Tests...</text><text x="34.068" y="1.67" class="i">99%</text><text x="38.076" y="1.67" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="1.67" class="h">531/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:03</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:01</text></symbol><symbol id="18"><text x="2.004" y="1.67" class="f">•</text><text x="4.008" y="1.67" class="f">Running</text><text x="12.024" y="1.67" class="f">NRFU</text><text x="17.034" y="1.67" class="f">Tests...</text><text x="25.05" y="1.67" class="i">100%</text><text x="30.06" y="1.67" style="white-space:pre" fill="#5faf00">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="94.188" y="1.67" class="h">534/534</text><text x="102.204" y="1.67" class="f">•</text><text x="104.208" y="1.67" class="k">0:00:04</text><text x="112.224" y="1.67" class="f">•</text><text x="114.228" y="1.67" class="g">0:00:00</text></symbol><symbol id="a"><path fill="transparent" d="M0 0h121v11H0z"/></symbol><symbol id="b"><path fill="#6f7683" d="M0 0h1.102v2.171H0z"/></symbol></defs><path class="a" d="M0 0h121v21.71H0z"/><g style="animation-duration:8.22014s;animation-iteration-count:infinite;animation-name:q;animation-timing-function:steps(1,end)"><svg width="7744"><svg><use xlink:href="#a"/><use xlink:href="#b" x="-.004"/></svg><svg x="121"><use xlink:href="#a"/><use xlink:href="#b" x="1.996"/><use xlink:href="#1"/></svg><svg x="242"><use xlink:href="#a"/><use xlink:href="#b" x="2.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">a</text></svg><svg x="363"><use xlink:href="#a"/><use xlink:href="#b" x="3.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">an</text></svg><svg x="484"><use xlink:href="#a"/><use xlink:href="#b" x="4.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">ant</text></svg><svg x="605"><use xlink:href="#a"/><use xlink:href="#b" x="5.996"/><use xlink:href="#2"/></svg><svg x="726"><use xlink:href="#a"/><use xlink:href="#b" x="6.996"/><use xlink:href="#2"/></svg><svg x="847"><use xlink:href="#a"/><use xlink:href="#b" x="7.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">n</text></svg><svg x="968"><use xlink:href="#a"/><use xlink:href="#b" x="8.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nr</text></svg><svg x="1089"><use xlink:href="#a"/><use xlink:href="#b" x="9.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrf</text></svg><svg x="1210"><use xlink:href="#a"/><use xlink:href="#b" x="10.996"/><use xlink:href="#3"/></svg><svg x="1331"><use xlink:href="#a"/><use xlink:href="#b" x="11.996"/><use xlink:href="#3"/></svg><svg x="1452"><use xlink:href="#a"/><use xlink:href="#b" x="12.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text><text x="12.024" y="1.67" class="f">t</text></svg><svg x="1573"><use xlink:href="#a"/><use xlink:href="#b" x="13.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text><text x="12.024" y="1.67" class="f">ta</text></svg><svg x="1694"><use xlink:href="#a"/><use xlink:href="#b" x="14.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text><text x="12.024" y="1.67" class="f">tab</text></svg><svg x="1815"><use xlink:href="#a"/><use xlink:href="#b" x="15.996"/><text y="1.67" class="f">❯</text><text x="2.004" y="1.67" class="f">anta</text><text x="7.014" y="1.67" class="f">nrfu</text><text x="12.024" y="1.67" class="f">tabl</text></svg><svg x="1936"><use xlink:href="#a"/><use xlink:href="#b" x="16.996"/><use xlink:href="#4"/></svg><svg x="2057"><use xlink:href="#a"/><use xlink:href="#b" x="-.004" y="2.146"/><use xlink:href="#4"/></svg><svg x="2178"><use xlink:href="#a"/><use xlink:href="#b" x="-.004" y="13.001"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/></svg><svg x="2299"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/></svg><svg x="2420"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#10" y="13.026"/></svg><svg x="2541"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#10" y="15.197"/></svg><svg x="2662"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="6.012" y="16.867" class="h">🐜)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">15%</text><text x="38.076" y="16.867" class="m">━━━━━━━━</text><text x="46.092" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="16.867" class="h">78/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:00</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="2783"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#12" y="15.197"/></svg><svg x="2904"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#12" y="15.197"/></svg><svg x="3025"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="4.008" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">15%</text><text x="38.076" y="16.867" class="m">━━━━━━━━</text><text x="46.092" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="16.867" class="h">78/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:00</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="3146"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#13" y="15.197"/></svg><svg x="3267"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#13" y="15.197"/></svg><svg x="3388"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="2.004" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">15%</text><text x="38.076" y="16.867" class="m">━━━━━━━━</text><text x="46.092" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="16.867" class="h">78/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:00</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="3509"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">15%</text><text x="38.076" y="16.867" class="m">━━━━━━━━</text><text x="46.092" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="16.867" class="h">78/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:00</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="3630"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">18%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━</text><text x="48.096" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="95.19" y="16.867" class="h">98/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:00</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:05</text></svg><svg x="3751"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">35%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━</text><text x="57.114" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">189/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:02</text></svg><svg x="3872"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="2.004" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">55%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="68.136" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">293/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="3993"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="3.006" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">58%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="70.14" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">311/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4114"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="3.006" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">63%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="73.146" y="16.867" class="j">━━━━━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">335/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4235"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="4.008" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">67%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="75.15" y="16.867" class="j">╺━━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">360/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4356"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="5.01" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">71%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="77.154" y="16.867" class="j">━━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">378/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4477"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="5.01" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">72%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="77.154" y="16.867" class="j">╺━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">383/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4598"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="6.012" y="16.867" class="h">🐌)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">72%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="78.156" y="16.867" class="j">━━━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">385/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4719"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="6.012" y="16.867" class="h">🐜)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">76%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="80.16" y="16.867" class="j">━━━━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">407/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4840"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="6.012" y="16.867" class="h">🐜)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">81%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="83.166" y="16.867" class="j">━━━━━━━━━━</text><text x="94.188" y="16.867" class="h">433/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:01</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="4961"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="5.01" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">84%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="84.168" y="16.867" class="j">╺━━━━━━━━</text><text x="94.188" y="16.867" class="h">447/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5082"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="4.008" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">85%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="85.17" y="16.867" class="j">━━━━━━━━</text><text x="94.188" y="16.867" class="h">456/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5203"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="4.008" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">87%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="86.172" y="16.867" class="j">╺━━━━━━</text><text x="94.188" y="16.867" class="h">467/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5324"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="3.006" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">89%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="87.174" y="16.867" class="j">╺━━━━━</text><text x="94.188" y="16.867" class="h">476/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5445"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="2.004" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">91%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="88.176" y="16.867" class="j">╺━━━━</text><text x="94.188" y="16.867" class="h">486/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5566"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="2.004" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">95%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="90.18" y="16.867" class="j">╺━━</text><text x="94.188" y="16.867" class="h">505/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5687"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">98%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="92.184" y="16.867" class="j">━</text><text x="94.188" y="16.867" class="h">521/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5808"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">99%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text x="92.184" y="16.867" class="j">╺</text><text x="94.188" y="16.867" class="h">528/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:02</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="5929"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#14" y="15.197"/></svg><svg x="6050"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#14" y="15.197"/></svg><svg x="6171"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="3.006" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">99%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="16.867" class="h">531/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:03</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="6292"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#15" y="15.197"/></svg><svg x="6413"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#15" y="15.197"/></svg><svg x="6534"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="5.01" y="16.867" class="h">🐌</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">99%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="16.867" class="h">531/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:03</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="6655"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#16" y="15.197"/></svg><svg x="6776"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#16" y="15.197"/></svg><svg x="6897"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="6.012" y="16.867" class="h">🐜)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">99%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="16.867" class="h">531/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:03</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="7018"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#17" y="15.197"/></svg><svg x="7139"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#17" y="15.197"/></svg><svg x="7260"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="4.008" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="34.068" y="16.867" class="i">99%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="16.867" class="h">531/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:03</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="7381"><use xlink:href="#a"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><text y="16.867" class="h">(</text><text x="3.006" y="16.867" class="h">🐜</text><text x="8.016" y="16.867" class="h">)</text><text x="10.02" y="16.867" class="f">•</text><text x="12.024" y="16.867" class="f">Running</text><text x="20.04" y="16.867" class="f">NRFU</text><text x="25.05" y="16.867" class="f">Tests...</text><text x="33.066" y="16.867" class="i">100%</text><text x="38.076" y="16.867" class="m">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸</text><text x="94.188" y="16.867" class="h">533/534</text><text x="102.204" y="16.867" class="f">•</text><text x="104.208" y="16.867" class="k">0:00:04</text><text x="112.224" y="16.867" class="f">•</text><text x="114.228" y="16.867" class="g">0:00:01</text></svg><svg x="7502"><use xlink:href="#a"/><use xlink:href="#b" x="-.004" y="17.343"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#18" y="15.197"/></svg><svg x="7623"><use xlink:href="#a"/><use xlink:href="#b" x="1.996" y="17.343"/><use xlink:href="#4"/><use xlink:href="#5" y="2.171"/><use xlink:href="#6" y="4.342"/><use xlink:href="#7" y="6.513"/><use xlink:href="#8" y="8.684"/><use xlink:href="#9" y="10.855"/><use xlink:href="#11" y="13.026"/><use xlink:href="#18" y="15.197"/><use xlink:href="#1" y="17.368"/></svg></svg></g></g></svg></svg> diff --git a/docs/imgs/favicon.ico b/docs/imgs/favicon.ico Binary files differnew file mode 100644 index 0000000..cf55b0c --- /dev/null +++ b/docs/imgs/favicon.ico diff --git a/docs/imgs/uml/anta.device.AntaDevice.jpeg b/docs/imgs/uml/anta.device.AntaDevice.jpeg Binary files differnew file mode 100644 index 0000000..8d8f91e --- /dev/null +++ b/docs/imgs/uml/anta.device.AntaDevice.jpeg diff --git a/docs/imgs/uml/anta.device.AsyncEOSDevice.jpeg b/docs/imgs/uml/anta.device.AsyncEOSDevice.jpeg Binary files differnew file mode 100644 index 0000000..255b8e4 --- /dev/null +++ b/docs/imgs/uml/anta.device.AsyncEOSDevice.jpeg diff --git a/docs/imgs/uml/anta.models.AntaCommand.jpeg b/docs/imgs/uml/anta.models.AntaCommand.jpeg Binary files differnew file mode 100644 index 0000000..b73b87f --- /dev/null +++ b/docs/imgs/uml/anta.models.AntaCommand.jpeg diff --git a/docs/imgs/uml/anta.models.AntaTemplate.jpeg b/docs/imgs/uml/anta.models.AntaTemplate.jpeg Binary files differnew file mode 100644 index 0000000..2485cb7 --- /dev/null +++ b/docs/imgs/uml/anta.models.AntaTemplate.jpeg diff --git a/docs/imgs/uml/anta.models.AntaTest.jpeg b/docs/imgs/uml/anta.models.AntaTest.jpeg Binary files differnew file mode 100644 index 0000000..36abed4 --- /dev/null +++ b/docs/imgs/uml/anta.models.AntaTest.jpeg diff --git a/docs/imgs/uml/anta.result_manager.ResultManager.jpeg b/docs/imgs/uml/anta.result_manager.ResultManager.jpeg Binary files differnew file mode 100644 index 0000000..7f29943 --- /dev/null +++ b/docs/imgs/uml/anta.result_manager.ResultManager.jpeg diff --git a/docs/imgs/uml/anta.result_manager.models.TestResult.jpeg b/docs/imgs/uml/anta.result_manager.models.TestResult.jpeg Binary files differnew file mode 100644 index 0000000..25ad998 --- /dev/null +++ b/docs/imgs/uml/anta.result_manager.models.TestResult.jpeg diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000..2863221 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block outdated %} + <div id="current-version"> </div> + <script> + let message = "You're not viewing the latest version." + // There is probably a nicer method retrieving the value of the + // version element but it is loaded after the outdated block + let current = window.location.pathname.split("/")[1] + if (current == "main") { + message = "This is the documentation from the 'main' branch which is not a stable release." + } + message = message + '<a href="{{ "../" ~ base_url }}"> <strong>Click here to go to latest stable release.</strong> </a>' + document.getElementById("current-version").innerHTML = message; + </script> + {{app}} +{% endblock %} diff --git a/docs/requirements-and-installation.md b/docs/requirements-and-installation.md new file mode 100644 index 0000000..9885cbe --- /dev/null +++ b/docs/requirements-and-installation.md @@ -0,0 +1,105 @@ +<!-- + ~ 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. + --> + +# ANTA Requirements + +## Python version + +Python 3 (`>=3.8`) is required: + +```bash +python --version +Python 3.9.9 +``` + +## Install ANTA package + +This installation will deploy tests collection, scripts and all their Python requirements. + +The ANTA package and the cli require some packages that are not part of the Python standard library. They are indicated in the [pyproject.toml](https://github.com/arista-netdevops-community/anta/blob/main/pyproject.toml) file, under dependencies. + + +### Install from Pypi server + +```bash +pip install anta +``` + +### Install ANTA from github + + +```bash +pip install git+https://github.com/arista-netdevops-community/anta.git + +# You can even specify the branch, tag or commit: +pip install git+https://github.com/arista-netdevops-community/anta.git@<cool-feature-branch> +pip install git+https://github.com/arista-netdevops-community/anta.git@<cool-tag> +pip install git+https://github.com/arista-netdevops-community/anta.git@<more-or-less-cool-hash> +``` + + +### Check installation + +After installing ANTA, verify the installation with the following commands: + +```bash +# Check ANTA has been installed in your python path +pip list | grep anta + +# Check scripts are in your $PATH +# Path may differ but it means CLI is in your path +which anta +/home/tom/.pyenv/shims/anta +``` + +!!! warning + Before running the `anta --version` command, please be aware that some users have reported issues related to the `urllib3` package. If you encounter an error at this step, please refer to our [FAQ](faq.md) page for guidance on resolving it. + +```bash +# Check ANTA version +anta --version +anta, version v0.13.0 +``` + +## EOS Requirements + +To get ANTA working, the targetted Arista EOS devices must have the following configuration (assuming you connect to the device using Management interface in MGMT VRF): + +```eos +configure +! +vrf instance MGMT +! +interface Management1 + description oob_management + vrf MGMT + ip address 10.73.1.105/24 +! +end +``` + +Enable eAPI on the MGMT vrf: + +```eos +configure +! +management api http-commands + protocol https port 443 + no shutdown + vrf MGMT + no shutdown +! +end +``` + +Now the swicth accepts on port 443 in the MGMT VRF HTTPS requests containing a list of CLI commands. + +Run these EOS commands to verify: + +```eos +show management http-server +show management api http-commands +``` diff --git a/docs/scripts/generate_svg.py b/docs/scripts/generate_svg.py new file mode 100644 index 0000000..19177db --- /dev/null +++ b/docs/scripts/generate_svg.py @@ -0,0 +1,92 @@ +# 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. +""" +A script to generate svg files from anta command + +usage: + +python generate_svg.py anta ... +""" + +import io +import os +import pathlib +import sys +from contextlib import redirect_stdout, suppress +from importlib import import_module +from importlib.metadata import entry_points +from unittest.mock import patch + +from rich.console import Console + +from anta.cli.console import console +from anta.cli.nrfu.utils import anta_progress_bar + +OUTPUT_DIR = pathlib.Path(__file__).parent.parent / "imgs" + + +def custom_progress_bar() -> None: + """ + Set the console of progress_bar to main anta console + + Caveat: this capture all steps of the progress bar.. + Disabling refresh to only capture beginning and end + """ + progress = anta_progress_bar() + progress.live.auto_refresh = False + progress.live.console = console + return progress + + +if __name__ == "__main__": + # Sane rich size + os.environ["COLUMNS"] = "165" + + # stolen from https://github.com/ewels/rich-click/blob/main/src/rich_click/cli.py + args = sys.argv[1:] + script_name = args[0] + scripts = {script.name: script for script in entry_points().get("console_scripts")} + + if script_name in scripts: + # A VALID SCRIPT WAS passed + script = scripts[script_name] + module_path, function_name = script.value.split(":", 1) + prog = script_name + elif ":" in script_name: + # the path to a function was passed + module_path, function_name = args[0].split(":", 1) + prog = module_path.split(".", 1)[0] + else: + print("This is supposed to be used with anta only") + print("Usage: python generate_svg.py anta <options>") + sys.exit(1) + + sys.argv = [prog, *args[1:]] + module = import_module(module_path) + function = getattr(module, function_name) + + # Console to captur everything + new_console = Console(record=True) + + # tweaks to record and redirect to a dummy file + pipe = io.StringIO() + console.record = True + console.file = pipe + + # Redirect stdout of the program towards another StringIO to capture help + # that is not part or anta rich console + with redirect_stdout(io.StringIO()) as f: + # redirect potential progress bar output to console by patching + with patch("anta.cli.nrfu.commands.anta_progress_bar", custom_progress_bar): + with suppress(SystemExit): + function() + # print to our new console the output of anta console + new_console.print(console.export_text()) + # print the content of the stdout to our new_console + new_console.print(f.getvalue()) + + filename = f"{'_'.join(map(lambda x: x.replace('/', '_').replace('-', '_').replace('.', '_'), args))}.svg" + filename = f"{OUTPUT_DIR}/{filename}" + print(f"File saved at {filename}") + new_console.save_svg(filename, title=" ".join(args)) diff --git a/docs/snippets/anta_help.txt b/docs/snippets/anta_help.txt new file mode 100644 index 0000000..0c3302a --- /dev/null +++ b/docs/snippets/anta_help.txt @@ -0,0 +1,20 @@ +Usage: anta [OPTIONS] COMMAND [ARGS]... + + Arista Network Test Automation (ANTA) CLI + +Options: + --version Show the version and exit. + --log-file FILE Send the logs to a file. If logging level is + DEBUG, only INFO or higher will be sent to + stdout. [env var: ANTA_LOG_FILE] + -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG] + ANTA logging level [env var: + ANTA_LOG_LEVEL; default: INFO] + --help Show this message and exit. + +Commands: + check Commands to validate configuration files + debug Commands to execute EOS commands on remote devices + exec Commands to execute various scripts on EOS devices + get Commands to get information from or generate inventories + nrfu Run ANTA tests on devices diff --git a/docs/stylesheets/extra.material.css b/docs/stylesheets/extra.material.css new file mode 100644 index 0000000..b401c9a --- /dev/null +++ b/docs/stylesheets/extra.material.css @@ -0,0 +1,207 @@ +[data-md-color-scheme="slate"] { + --md-hue: 210; +} + +:root { + /* Color schema based on Arista Color Schema */ + /* Default color shades */ + --md-default-fg-color: #000000; + --md-default-fg-color--light: #a1a0a0; + --md-default-fg-color--lighter: #FFFFFF; + --md-default-fg-color--lightest: #FFFFFF; + --md-default-bg-color: #FFFFFF; + --md-default-bg-color--light: #FFFFFF; + --md-default-bg-color--lighter: #FFFFFF; + --md-default-bg-color--lightest: #FFFFFF; + + /* Primary color shades */ + --md-primary-fg-color: #27569B; + --md-primary-fg-color--light: #FFFFFF; + --md-primary-fg-color--dark: #27569B; + --md-primary-bg-color: #FFFFFF; + --md-primary-bg-color--light: #FFFFFF; + + /* Accent color shades */ + --md-accent-fg-color: #27569B; + --md-accent-bg-color: #27569B; + --md-accent-bg-color--light: #27569B; + + /* Link color */ + --md-typeset-a-color: #27569B; + --md-typeset-a-color-fg: #FFFFFF; + --md-typeset-a-color-bg: #27569B; + + /* Code block color shades */ + --md-code-bg-color: #E6E6E6; + --md-code-border-color: #0000004f; + --block-code-bg-color: #e4e4e4; + /* --md-code-fg-color: ...; */ + + font-size: 1.1rem; + /* min-height: 100%; + position: relative; + width: 100%; */ + font-feature-settings: "kern","liga"; + font-family: var(--md-text-font-family,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif; + -webkit-font-smoothing: antialiased; + +} + +[data-md-color-scheme="slate"] { + + /* Link color */ + --md-typeset-a-color: #75aaf8; + --md-typeset-a-color-fg: #FFFFFF; + --md-typeset-a-color-bg: #27569B; + + /* Code block color shades */ + /* --md-code-bg-color: #E6E6E6; */ + --md-code-border-color: #aec6db4f; + /* --block-code-bg-color: #e4e4e4; */ +} + +@media only screen and (min-width: 76.25em) { + .md-main__inner, .md-header__inner { + max-width: 85%; + } + .md-sidebar--primary { + left: 5%; + } + .md-sidebar--secondary { + right: 5%; + margin-left: 0; + -webkit-transform: none; + transform: none; + } +} + +@media only screen { + .md-typeset a:hover { + background-color: var(--md-typeset-a-color-bg); + color: var(--md-typeset-a-color-fg); + } + .md-footer-nav { + background-color: var(--md-default-bg-color--light); + color: var(--md-accent-fg-color--transparent) + } + .md-footer { + height: 2%; + } + .md-footer-nav__direction { + position: absolute; + right: 0; + left: 0; + margin-top: -1rem; + padding: 0 1rem; + color: var(--md-default-fg-color--light); + font-size: .64rem; + } + .md-footer-nav__title { + font-size: 1.2rem; + line-height: 10rem; + color: var(--md-default-fg-color--light); + } + + .md-typeset h4 h5 h6 { + font-size: 1.5rem; + margin: 1em 0; + /* font-weight: 700; */ + letter-spacing: -.01em; + line-height: 3em; + } + + .md-typeset table:not([class]) th { + min-width: 5rem; + padding: .6rem .8rem; + color: var(--md-default-fg-color); + vertical-align: top; + /* background-color: var(--md-accent-bg-color); */ + text-align: left; + /* min-width: 100%; */ + /* display: table; */ + } + .md-typeset table:not([class]) td { + /* padding: .9375em 1.25em; */ + border-collapse: collapse; + vertical-align: center; + text-align: left; + /* border-bottom: 1px solid var(--md-default-fg-color--light); */ + } + .md-typeset code { + padding: 0 .2941176471em; + font-size: 100%; + word-break: break-word; + background-color: var(--md-code-bg-color); + border-radius: .1rem; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + } + .highlight code { + background-color: var(--md-code-bg-color); + font-size: 90%; + border-radius: 2%; + } + .md-typeset .admonition, .md-typeset details { + margin: 1.5625em 0; + padding: 0 .6rem; + overflow: hidden; + font-size: 90%; + page-break-inside: avoid; + border-left: .2rem solid var(--md-accent-bg-color); + border-left-color: var(--md-accent-bg-color); + border-radius: .1rem; + box-shadow: 0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1); + } + /* .md-typeset .note > .admonition-title, .md-typeset .note > summary { + background-color: var(--md-accent-bg-color); + color: var(--md-default-fg-color--lighter) + } */ + .md-typeset__table { + min-width: 80%; + } + .md-typeset table:not([class]) { + display: table; + } + + .mdx-content__footer { + margin-top: 20px; + text-align: center; + } + .mdx-content__footer a { + display: inline-block; + transition: transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), color 125ms; + } + .mdx-content__footer a:focus, .mdx-content__footer a:hover { + transform: scale(1.2); + } + + .md-typeset table:not([class]) th { + min-width: 5rem; + padding: .6rem .8rem; + /* color: var(--md-primary-fg-color--light); */ + bg: var(--md-footer-fg-color--lighter); + } + + .md-footer-copyright { + color: var(--md-footer-fg-color--lighter); + font-size: .64rem; + margin: auto 0.6rem; + padding: 0.4rem; + width: 100%; + text-align: center; + } + .img_center { + display: block; + margin-left: auto; + margin-right: auto; + border-radius: 1%; + /* width: 50%; */ + } +} + +/* mkdocstrings css from official repo to indent sub-elements nicely */ +/* Indentation. */ +div.doc-contents { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} diff --git a/docs/stylesheets/highlight.js b/docs/stylesheets/highlight.js new file mode 100644 index 0000000..86e50b9 --- /dev/null +++ b/docs/stylesheets/highlight.js @@ -0,0 +1,3 @@ +document$.subscribe(() => { + hljs.highlightAll() +}) diff --git a/docs/stylesheets/tables.js b/docs/stylesheets/tables.js new file mode 100644 index 0000000..e848f07 --- /dev/null +++ b/docs/stylesheets/tables.js @@ -0,0 +1,6 @@ +document$.subscribe(function() { + var tables = document.querySelectorAll("article table") + tables.forEach(function(table) { + new Tablesort(table) + }) +}) diff --git a/docs/usage-inventory-catalog.md b/docs/usage-inventory-catalog.md new file mode 100644 index 0000000..39734f3 --- /dev/null +++ b/docs/usage-inventory-catalog.md @@ -0,0 +1,246 @@ +<!-- + ~ 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. + --> + +# Inventory and Catalog + +The ANTA framework needs 2 important inputs from the user to run: a **device inventory** and a **test catalog**. + +Both inputs can be defined in a file or programmatically. + +## Device Inventory + +A device inventory is an instance of the [AntaInventory](../api/inventory.md#anta.inventory.AntaInventory) class. + +### Device Inventory File + +The ANTA device inventory can easily be defined as a YAML file. +The file must comply with the following structure: + +```yaml +anta_inventory: + hosts: + - host: < ip address value > + port: < TCP port for eAPI. Default is 443 (Optional)> + name: < name to display in report. Default is host:port (Optional) > + tags: < list of tags to use to filter inventory during tests > + disable_cache: < Disable cache per hosts. Default is False. > + networks: + - network: < network using CIDR notation > + tags: < list of tags to use to filter inventory during tests > + disable_cache: < Disable cache per network. Default is False. > + ranges: + - start: < first ip address value of the range > + end: < last ip address value of the range > + tags: < list of tags to use to filter inventory during tests > + disable_cache: < Disable cache per range. Default is False. > +``` + +The inventory file must start with the `anta_inventory` key then define one or multiple methods: + +- `hosts`: define each device individually +- `networks`: scan a network for devices accesible via eAPI +- `ranges`: scan a range for devices accesible via eAPI + +A full description of the inventory model is available in [API documentation](api/inventory.models.input.md) + +!!! info + Caching can be disabled per device, network or range by setting the `disable_cache` key to `True` in the inventory file. For more details about how caching is implemented in ANTA, please refer to [Caching in ANTA](advanced_usages/caching.md). + +### Example + +```yaml +--- +anta_inventory: + hosts: + - host: 192.168.0.10 + name: spine01 + tags: ['fabric', 'spine'] + - host: 192.168.0.11 + name: spine02 + tags: ['fabric', 'spine'] + networks: + - network: '192.168.110.0/24' + tags: ['fabric', 'leaf'] + ranges: + - start: 10.0.0.9 + end: 10.0.0.11 + tags: ['fabric', 'l2leaf'] +``` + +## Test Catalog + +A test catalog is an instance of the [AntaCatalog](../api/catalog.md#anta.catalog.AntaCatalog) class. + +### Test Catalog File + +In addition to the inventory file, you also have to define a catalog of tests to execute against your devices. This catalog list all your tests, their inputs and their tags. + +A valid test catalog file must have the following structure: +```yaml +--- +<Python module>: + - <AntaTest subclass>: + <AntaTest.Input compliant dictionary> +``` + +### Example + +```yaml +--- +anta.tests.connectivity: + - VerifyReachability: + hosts: + - source: Management0 + destination: 1.1.1.1 + vrf: MGMT + - source: Management0 + destination: 8.8.8.8 + vrf: MGMT + filters: + tags: ['leaf'] + result_overwrite: + categories: + - "Overwritten category 1" + description: "Test with overwritten description" + custom_field: "Test run by John Doe" +``` + +It is also possible to nest Python module definition: +```yaml +anta.tests: + connectivity: + - VerifyReachability: + hosts: + - source: Management0 + destination: 1.1.1.1 + vrf: MGMT + - source: Management0 + destination: 8.8.8.8 + vrf: MGMT + filters: + tags: ['leaf'] + result_overwrite: + categories: + - "Overwritten category 1" + description: "Test with overwritten description" + custom_field: "Test run by John Doe" +``` + +[This test catalog example](https://github.com/arista-netdevops-community/anta/blob/main/examples/tests.yaml) is maintained with all the tests defined in the `anta.tests` Python module. + +### Test tags + +All tests can be defined with a list of user defined tags. These tags will be mapped with device tags: when at least one tag is defined for a test, this test will only be executed on devices with the same tag. If a test is defined in the catalog without any tags, the test will be executed on all devices. + +```yaml +anta.tests.system: + - VerifyUptime: + minimum: 10 + filters: + tags: ['demo', 'leaf'] + - VerifyReloadCause: + - VerifyCoredump: + - VerifyAgentLogs: + - VerifyCPUUtilization: + filters: + tags: ['leaf'] +``` + +!!! info + When using the CLI, you can filter the NRFU execution using tags. Refer to [this section](cli/tag-management.md) of the CLI documentation. + +### Tests available in ANTA + +All tests available as part of the ANTA framework are defined under the `anta.tests` Python module and are categorised per family (Python submodule). +The complete list of the tests and their respective inputs is available at the [tests section](api/tests.md) of this website. + + +To run test to verify the EOS software version, you can do: + +```yaml +anta.tests.software: + - VerifyEOSVersion: +``` + +It will load the test `VerifyEOSVersion` located in `anta.tests.software`. But since this test has mandatory inputs, we need to provide them as a dictionary in the YAML file: + +```yaml +anta.tests.software: + - VerifyEOSVersion: + # List of allowed EOS versions. + versions: + - 4.25.4M + - 4.26.1F +``` + +The following example is a very minimal test catalog: + +```yaml +--- +# Load anta.tests.software +anta.tests.software: + # Verifies the device is running one of the allowed EOS version. + - VerifyEOSVersion: + # List of allowed EOS versions. + versions: + - 4.25.4M + - 4.26.1F + +# Load anta.tests.system +anta.tests.system: + # Verifies the device uptime is higher than a value. + - VerifyUptime: + minimum: 1 + +# Load anta.tests.configuration +anta.tests.configuration: + # Verifies ZeroTouch is disabled. + - VerifyZeroTouch: + - VerifyRunningConfigDiffs: +``` + +### Catalog with custom tests + +In case you want to leverage your own tests collection, use your own Python package in the test catalog. +So for instance, if my custom tests are defined in the `titom73.tests.system` Python module, the test catalog will be: + +```yaml +titom73.tests.system: + - VerifyPlatform: + type: ['cEOS-LAB'] +``` + +!!! tip "How to create custom tests" + To create your custom tests, you should refer to this [documentation](advanced_usages/custom-tests.md) + +### Customize test description and categories + +It might be interesting to use your own categories and customized test description to build a better report for your environment. ANTA comes with a handy feature to define your own `categories` and `description` in the report. + +In your test catalog, use `result_overwrite` dictionary with `categories` and `description` to just overwrite this values in your report: + +```yaml +anta.tests.configuration: + - VerifyZeroTouch: # Verifies ZeroTouch is disabled. + result_overwrite: + categories: ['demo', 'pr296'] + description: A custom test + - VerifyRunningConfigDiffs: +anta.tests.interfaces: + - VerifyInterfaceUtilization: +``` + +Once you run `anta nrfu table`, you will see following output: + +```bash +┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ +┃ Device IP ┃ Test Name ┃ Test Status ┃ Message(s) ┃ Test description ┃ Test category ┃ +┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ +│ spine01 │ VerifyZeroTouch │ success │ │ A custom test │ demo, pr296 │ +│ spine01 │ VerifyRunningConfigDiffs │ success │ │ │ configuration │ +│ spine01 │ VerifyInterfaceUtilization │ success │ │ Verifies interfaces utilization is below 75%. │ interfaces │ +└───────────┴────────────────────────────┴─────────────┴────────────┴───────────────────────────────────────────────┴───────────────┘ +``` |