Merging upstream version 1.0.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
256a120fdd
commit
3ccac88507
36 changed files with 2108 additions and 153 deletions
10
.arista/secret_allowlist.yaml
Normal file
10
.arista/secret_allowlist.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Arista Secret Scanner allow list
|
||||
|
||||
version: v1.0
|
||||
allowed_secrets:
|
||||
- secret_pattern: "https://ansible:ansible@192.168.0.2"
|
||||
category: FALSE_POSITIVE
|
||||
reason: Used as example in documentation
|
||||
- secret_pattern: "https://ansible:ansible@192.168.0.17"
|
||||
category: FALSE_POSITIVE
|
||||
reason: Used as example in documentation
|
|
@ -21,7 +21,17 @@
|
|||
"ms-python.pylint",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"njqdev.vscode-python-typehint",
|
||||
"hbenl.vscode-test-explorer"
|
||||
"hbenl.vscode-test-explorer",
|
||||
"codezombiech.gitignore",
|
||||
"ms-python.isort",
|
||||
"eriklynd.json-tools",
|
||||
"ms-python.vscode-pylance",
|
||||
"tuxtina.json2yaml",
|
||||
"christian-kohler.path-intellisense",
|
||||
"ms-python.vscode-pylance",
|
||||
"njqdev.vscode-python-typehint",
|
||||
"LittleFoxTeam.vscode-python-test-adapter",
|
||||
"donjayamanne.python-environment-manager"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,5 +9,8 @@ pip install --upgrade pip
|
|||
echo "Installing ANTA package from git"
|
||||
pip install -e .
|
||||
|
||||
echo "Installing ANTA CLI package from git"
|
||||
pip install -e ".[cli]"
|
||||
|
||||
echo "Installing development tools"
|
||||
pip install -e ".[dev]"
|
||||
|
|
2
.github/release.md
vendored
2
.github/release.md
vendored
|
@ -83,7 +83,7 @@ This is to be executed at the top of the repo
|
|||
git push origin HEAD
|
||||
gh pr create --title 'bump: ANTA vx.x.x'
|
||||
```
|
||||
9. Merge PR after review and wait for [workflow](https://github.com/arista-netdevops-community/anta/actions/workflows/release.yml) to be executed.
|
||||
9. Merge PR after review and wait for [workflow](https://github.com/aristanetworks/anta/actions/workflows/release.yml) to be executed.
|
||||
|
||||
```bash
|
||||
gh pr merge --squash
|
||||
|
|
37
.github/workflows/code-testing.yml
vendored
37
.github/workflows/code-testing.yml
vendored
|
@ -59,30 +59,19 @@ jobs:
|
|||
pip install .
|
||||
- name: install dev requirements
|
||||
run: pip install .[dev]
|
||||
missing-documentation:
|
||||
name: "Warning documentation is missing"
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [file-changes]
|
||||
if: needs.file-changes.outputs.cli == 'true' && needs.file-changes.outputs.docs == 'false'
|
||||
steps:
|
||||
- name: Documentation is missing
|
||||
uses: GrantBirki/comment@v2.0.10
|
||||
with:
|
||||
body: |
|
||||
Please consider that documentation is missing under `docs/` folder.
|
||||
You should update documentation to reflect your change, or maybe not :)
|
||||
lint-yaml:
|
||||
name: Run linting for yaml files
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [file-changes, check-requirements]
|
||||
if: needs.file-changes.outputs.code == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: yaml-lint
|
||||
uses: ibiqlik/action-yamllint@v3
|
||||
with:
|
||||
config_file: .yamllint.yml
|
||||
file_or_dir: .
|
||||
# @gmuloc: commenting this out for now
|
||||
#missing-documentation:
|
||||
# name: "Warning documentation is missing"
|
||||
# runs-on: ubuntu-20.04
|
||||
# needs: [file-changes]
|
||||
# if: needs.file-changes.outputs.cli == 'true' && needs.file-changes.outputs.docs == 'false'
|
||||
# steps:
|
||||
# - name: Documentation is missing
|
||||
# uses: GrantBirki/comment@v2.0.10
|
||||
# with:
|
||||
# body: |
|
||||
# Please consider that documentation is missing under `docs/` folder.
|
||||
# You should update documentation to reflect your change, or maybe not :)
|
||||
lint-python:
|
||||
name: Check the code style
|
||||
runs-on: ubuntu-20.04
|
||||
|
|
30
.github/workflows/secret-scanner.yml
vendored
Normal file
30
.github/workflows/secret-scanner.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Secret-scanner workflow from Arista Networks.
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Secret Scanner (go/secret-scanner)
|
||||
jobs:
|
||||
scan_secret:
|
||||
name: Scan incoming changes
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/aristanetworks/secret-scanner-service:main
|
||||
options: --name sss-scanner
|
||||
steps:
|
||||
- name: Checkout ${{ github.ref }}
|
||||
# Hitting https://github.com/actions/checkout/issues/334 so trying v1
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run scanner
|
||||
run: |
|
||||
git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||
scanner commit . github ${{ github.repository }} \
|
||||
--markdown-file job_summary.md \
|
||||
${{ github.event_name == 'pull_request' && format('--since-commit {0}', github.event.pull_request.base.sha) || ''}}
|
||||
- name: Write result to summary
|
||||
run: cat ./job_summary.md >> $GITHUB_STEP_SUMMARY
|
||||
if: ${{ always() }}
|
|
@ -5,7 +5,7 @@ files: ^(anta|docs|scripts|tests|asynceapi)/
|
|||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: docs/.*.svg
|
||||
|
@ -15,7 +15,7 @@ repos:
|
|||
- id: check-merge-conflict
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.4
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- name: Check and insert license on Python files
|
||||
id: insert-license
|
||||
|
@ -43,7 +43,7 @@ repos:
|
|||
- '<!--| ~| -->'
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.2
|
||||
rev: v0.4.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: Run Ruff linter
|
||||
|
@ -51,11 +51,10 @@ repos:
|
|||
- id: ruff-format
|
||||
name: Run Ruff formatter
|
||||
|
||||
- repo: local # as per https://pylint.pycqa.org/en/latest/user_guide/installation/pre-commit-integration.html
|
||||
- repo: https://github.com/pycqa/pylint
|
||||
rev: "v3.2.3"
|
||||
hooks:
|
||||
- id: pylint
|
||||
entry: pylint
|
||||
language: python
|
||||
name: Check code style with pylint
|
||||
description: This hook runs pylint.
|
||||
types: [python]
|
||||
|
@ -63,9 +62,16 @@ repos:
|
|||
- -rn # Only display messages
|
||||
- -sn # Don't display the score
|
||||
- --rcfile=pyproject.toml # Link to config file
|
||||
additional_dependencies:
|
||||
- anta[cli]
|
||||
- types-PyYAML
|
||||
- types-requests
|
||||
- types-pyOpenSSL
|
||||
- pylint_pydantic
|
||||
- pytest
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
name: Checks for common misspellings in text files.
|
||||
|
|
18
.sonarcloud.properties
Normal file
18
.sonarcloud.properties
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Path to sources
|
||||
sonar.sources=anta/,asynceapi/
|
||||
#sonar.exclusions=
|
||||
#sonar.inclusions=
|
||||
|
||||
# Path to tests
|
||||
sonar.tests=tests/
|
||||
#sonar.test.exclusions=
|
||||
#sonar.test.inclusions=
|
||||
|
||||
# Source encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Python version (for python projects only)
|
||||
sonar.python.version=3.9,3.10,3.11,3.12
|
||||
|
||||
# Exclusions for copy-paste detection
|
||||
#sonar.cpd.exclusions=,
|
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
|
@ -3,18 +3,27 @@
|
|||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
"mypy-type-checker.importStrategy": "fromEnvironment",
|
||||
"mypy-type-checker.args": [
|
||||
"--config-file=pyproject.toml"
|
||||
],
|
||||
"pylint.severity": {
|
||||
"refactor": "Warning"
|
||||
},
|
||||
"pylint.args": [
|
||||
"--load-plugins", "pylint_pydantic",
|
||||
"--rcfile=pylintrc"
|
||||
"--load-plugins",
|
||||
"pylint_pydantic",
|
||||
"--rcfile=pyproject.toml"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"autoDocstring.docstringFormat": "numpy",
|
||||
"autoDocstring.includeName": false,
|
||||
"autoDocstring.includeExtendedSummary": true,
|
||||
"autoDocstring.startOnNewLine": true,
|
||||
"autoDocstring.guessTypes": true,
|
||||
"python.languageServer": "Pylance",
|
||||
"githubIssues.issueBranchTitle": "issues/${issueNumber}-${issueTitle}",
|
||||
"editor.formatOnPaste": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"mypy.configFile": "pyproject.toml",
|
||||
"workbench.remoteIndicator.showExtensionRecommendations": true,
|
||||
|
||||
}
|
|
@ -37,11 +37,11 @@ RUN adduser --system anta
|
|||
LABEL "org.opencontainers.image.title"="anta" \
|
||||
"org.opencontainers.artifact.description"="network-test-automation in a Python package and Python scripts to test Arista devices." \
|
||||
"org.opencontainers.image.description"="network-test-automation in a Python package and Python scripts to test Arista devices." \
|
||||
"org.opencontainers.image.source"="https://github.com/arista-netdevops-community/anta" \
|
||||
"org.opencontainers.image.source"="https://github.com/aristanetworks/anta" \
|
||||
"org.opencontainers.image.url"="https://www.anta.ninja" \
|
||||
"org.opencontainers.image.documentation"="https://www.anta.ninja" \
|
||||
"org.opencontainers.image.documentation"="https://anta.arista.com" \
|
||||
"org.opencontainers.image.licenses"="Apache-2.0" \
|
||||
"org.opencontainers.image.vendor"="The anta contributors." \
|
||||
"org.opencontainers.image.vendor"="Arista Networks" \
|
||||
"org.opencontainers.image.authors"="Khelil Sator, Angélique Phillipps, Colin MacGiollaEáin, Matthieu Tache, Onur Gashi, Paul Lavelle, Guillaume Mulocher, Thomas Grimonet" \
|
||||
"org.opencontainers.image.base.name"="python" \
|
||||
"org.opencontainers.image.revision"="dev" \
|
||||
|
|
|
@ -45,4 +45,4 @@ RICH_COLOR_THEME = {
|
|||
"unset": RICH_COLOR_PALETTE.UNSET,
|
||||
}
|
||||
|
||||
GITHUB_SUGGESTION = "Please reach out to the maintainer team or open an issue on Github: https://github.com/arista-netdevops-community/anta."
|
||||
GITHUB_SUGGESTION = "Please reach out to the maintainer team or open an issue on Github: https://github.com/aristanetworks/anta."
|
||||
|
|
|
@ -48,7 +48,7 @@ def debug_options(f: Callable[..., Any]) -> Callable[..., Any]:
|
|||
device: str,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
# TODO: @gmuloc - tags come from context https://github.com/arista-netdevops-community/anta/issues/584
|
||||
# TODO: @gmuloc - tags come from context https://github.com/aristanetworks/anta/issues/584
|
||||
# pylint: disable=unused-argument
|
||||
# ruff: noqa: ARG001
|
||||
if (d := inventory.get(device)) is None:
|
||||
|
|
|
@ -13,6 +13,7 @@ from pathlib import Path
|
|||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import click
|
||||
import requests
|
||||
from cvprac.cvp_client import CvpClient
|
||||
from cvprac.cvp_client_errors import CvpApiError
|
||||
from rich.pretty import pretty_repr
|
||||
|
@ -36,14 +37,27 @@ logger = logging.getLogger(__name__)
|
|||
@click.option("--username", "-u", help="CloudVision username", type=str, required=True)
|
||||
@click.option("--password", "-p", help="CloudVision password", type=str, required=True)
|
||||
@click.option("--container", "-c", help="CloudVision container where devices are configured", type=str)
|
||||
def from_cvp(ctx: click.Context, output: Path, host: str, username: str, password: str, container: str | None) -> None:
|
||||
@click.option(
|
||||
"--ignore-cert",
|
||||
help="Ignore verifying the SSL certificate when connecting to CloudVision",
|
||||
show_envvar=True,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
)
|
||||
def from_cvp(ctx: click.Context, output: Path, host: str, username: str, password: str, container: str | None, *, ignore_cert: bool) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
"""Build ANTA inventory from Cloudvision.
|
||||
"""Build ANTA inventory from CloudVision.
|
||||
|
||||
TODO - handle get_inventory and get_devices_in_container failure
|
||||
NOTE: Only username/password authentication is supported for on-premises CloudVision instances.
|
||||
Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.
|
||||
"""
|
||||
# TODO: - Handle get_cv_token, get_inventory and get_devices_in_container failures.
|
||||
logger.info("Getting authentication token for user '%s' from CloudVision instance '%s'", username, host)
|
||||
token = get_cv_token(cvp_ip=host, cvp_username=username, cvp_password=password)
|
||||
try:
|
||||
token = get_cv_token(cvp_ip=host, cvp_username=username, cvp_password=password, verify_cert=not ignore_cert)
|
||||
except requests.exceptions.SSLError as error:
|
||||
logger.error("Authentication to CloudVison failed: %s.", error)
|
||||
ctx.exit(ExitCode.USAGE_ERROR)
|
||||
|
||||
clnt = CvpClient()
|
||||
try:
|
||||
|
|
|
@ -77,16 +77,33 @@ def inventory_output_options(f: Callable[..., Any]) -> Callable[..., Any]:
|
|||
return wrapper
|
||||
|
||||
|
||||
def get_cv_token(cvp_ip: str, cvp_username: str, cvp_password: str) -> str:
|
||||
"""Generate AUTH token from CVP using password."""
|
||||
# TODO: need to handle requests error
|
||||
def get_cv_token(cvp_ip: str, cvp_username: str, cvp_password: str, *, verify_cert: bool) -> str:
|
||||
"""Generate the authentication token from CloudVision using username and password.
|
||||
|
||||
TODO: need to handle requests error
|
||||
|
||||
Args:
|
||||
----
|
||||
cvp_ip: IP address of CloudVision.
|
||||
cvp_username: Username to connect to CloudVision.
|
||||
cvp_password: Password to connect to CloudVision.
|
||||
verify_cert: Enable or disable certificate verification when connecting to CloudVision.
|
||||
|
||||
Returns
|
||||
-------
|
||||
token(str): The token to use in further API calls to CloudVision.
|
||||
|
||||
Raises
|
||||
------
|
||||
requests.ssl.SSLError: If the certificate verification fails
|
||||
|
||||
"""
|
||||
# use CVP REST API to generate a token
|
||||
url = f"https://{cvp_ip}/cvpservice/login/authenticate.do"
|
||||
payload = json.dumps({"userId": cvp_username, "password": cvp_password})
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload, verify=False, timeout=10)
|
||||
response = requests.request("POST", url, headers=headers, data=payload, verify=verify_cert, timeout=10)
|
||||
return response.json()["sessionId"]
|
||||
|
||||
|
||||
|
@ -94,7 +111,7 @@ def write_inventory_to_file(hosts: list[AntaInventoryHost], output: Path) -> Non
|
|||
"""Write a file inventory from pydantic models."""
|
||||
i = AntaInventoryInput(hosts=hosts)
|
||||
with output.open(mode="w", encoding="UTF-8") as out_fd:
|
||||
out_fd.write(yaml.dump({AntaInventory.INVENTORY_ROOT_KEY: i.model_dump(exclude_unset=True)}))
|
||||
out_fd.write(yaml.dump({AntaInventory.INVENTORY_ROOT_KEY: yaml.safe_load(i.yaml())}))
|
||||
logger.info("ANTA inventory file has been created: '%s'", output)
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, IPvAnyAddress, IPvAnyNetwork
|
||||
|
||||
from anta.custom_types import Hostname, Port
|
||||
|
@ -82,3 +84,16 @@ class AntaInventoryInput(BaseModel):
|
|||
networks: list[AntaInventoryNetwork] | None = None
|
||||
hosts: list[AntaInventoryHost] | None = None
|
||||
ranges: list[AntaInventoryRange] | None = None
|
||||
|
||||
def yaml(self) -> str:
|
||||
"""Return a YAML representation string of this model.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The YAML representation string of this model.
|
||||
"""
|
||||
# TODO: Pydantic and YAML serialization/deserialization is not supported natively.
|
||||
# This could be improved.
|
||||
# https://github.com/pydantic/pydantic/issues/1043
|
||||
# Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml
|
||||
return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
# mypy: disable-error-code=attr-defined
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
@ -118,6 +119,20 @@ def _get_interface_data(interface: str, vrf: str, command_output: dict[str, Any]
|
|||
return None
|
||||
|
||||
|
||||
def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: str, command_output: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Extract data related to an IS-IS interface for testing."""
|
||||
search_path = f"vrfs.{vrf}.isisInstances.{instance}.adjacencySegments"
|
||||
if get_value(dictionary=command_output, key=search_path, default=None) is None:
|
||||
return None
|
||||
|
||||
isis_instance = get_value(dictionary=command_output, key=search_path, default=None)
|
||||
|
||||
return next(
|
||||
(segment_data for segment_data in isis_instance if neighbor == segment_data["ipAddress"]),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
class VerifyISISNeighborState(AntaTest):
|
||||
"""Verifies all IS-IS neighbors are in UP state.
|
||||
|
||||
|
@ -211,14 +226,15 @@ class VerifyISISNeighborCount(AntaTest):
|
|||
isis_neighbor_count = _get_isis_neighbors_count(command_output)
|
||||
if len(isis_neighbor_count) == 0:
|
||||
self.result.is_skipped("No IS-IS neighbor detected")
|
||||
return
|
||||
for interface in self.inputs.interfaces:
|
||||
eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data["interface"] == interface.name and ifl_data["level"] == interface.level]
|
||||
if not eos_data:
|
||||
self.result.is_failure(f"No neighbor detected for interface {interface.name}")
|
||||
return
|
||||
continue
|
||||
if eos_data[0]["count"] != interface.count:
|
||||
self.result.is_failure(
|
||||
f"Interface {interface.name}:"
|
||||
f"Interface {interface.name}: "
|
||||
f"expected Level {interface.level}: count {interface.count}, "
|
||||
f"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}"
|
||||
)
|
||||
|
@ -284,7 +300,8 @@ class VerifyISISInterfaceMode(AntaTest):
|
|||
self.result.is_success()
|
||||
|
||||
if len(command_output["vrfs"]) == 0:
|
||||
self.result.is_failure("IS-IS is not configured on device")
|
||||
self.result.is_skipped("IS-IS is not configured on device")
|
||||
return
|
||||
|
||||
# Check for p2p interfaces
|
||||
for interface in self.inputs.interfaces:
|
||||
|
@ -306,3 +323,409 @@ class VerifyISISInterfaceMode(AntaTest):
|
|||
self.result.is_failure(f"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode")
|
||||
else:
|
||||
self.result.is_failure(f"Interface {interface.name} not found in VRF {interface.vrf}")
|
||||
|
||||
|
||||
class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):
|
||||
"""Verifies ISIS Segment Routing Adjacency Segments.
|
||||
|
||||
Verify that all expected Adjacency segments are correctly visible for each interface.
|
||||
|
||||
Expected Results
|
||||
----------------
|
||||
* Success: The test will pass if all listed interfaces have correct adjacencies.
|
||||
* Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.
|
||||
* Skipped: The test will be skipped if no ISIS SR Adjacency is found.
|
||||
|
||||
Examples
|
||||
--------
|
||||
```yaml
|
||||
anta.tests.routing:
|
||||
isis:
|
||||
- VerifyISISSegmentRoutingAdjacencySegments:
|
||||
instances:
|
||||
- name: CORE-ISIS
|
||||
vrf: default
|
||||
segments:
|
||||
- interface: Ethernet2
|
||||
address: 10.0.1.3
|
||||
sid_origin: dynamic
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
name = "VerifyISISSegmentRoutingAdjacencySegments"
|
||||
description = "Verify expected Adjacency segments are correctly visible for each interface."
|
||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing adjacency-segments", ofmt="json")]
|
||||
|
||||
class Input(AntaTest.Input):
|
||||
"""Input model for the VerifyISISSegmentRoutingAdjacencySegments test."""
|
||||
|
||||
instances: list[IsisInstance]
|
||||
|
||||
class IsisInstance(BaseModel):
|
||||
"""ISIS Instance model definition."""
|
||||
|
||||
name: str
|
||||
"""ISIS instance name."""
|
||||
vrf: str = "default"
|
||||
"""VRF name where ISIS instance is configured."""
|
||||
segments: list[Segment]
|
||||
"""List of Adjacency segments configured in this instance."""
|
||||
|
||||
class Segment(BaseModel):
|
||||
"""Segment model definition."""
|
||||
|
||||
interface: Interface
|
||||
"""Interface name to check."""
|
||||
level: Literal[1, 2] = 2
|
||||
"""ISIS level configured for interface. Default is 2."""
|
||||
sid_origin: Literal["dynamic"] = "dynamic"
|
||||
"""Adjacency type"""
|
||||
address: IPv4Address
|
||||
"""IP address of remote end of segment."""
|
||||
|
||||
@AntaTest.anta_test
|
||||
def test(self) -> None:
|
||||
"""Main test function for VerifyISISSegmentRoutingAdjacencySegments."""
|
||||
command_output = self.instance_commands[0].json_output
|
||||
self.result.is_success()
|
||||
|
||||
if len(command_output["vrfs"]) == 0:
|
||||
self.result.is_skipped("IS-IS is not configured on device")
|
||||
return
|
||||
|
||||
# initiate defaults
|
||||
failure_message = []
|
||||
skip_vrfs = []
|
||||
skip_instances = []
|
||||
|
||||
# Check if VRFs and instances are present in output.
|
||||
for instance in self.inputs.instances:
|
||||
vrf_data = get_value(
|
||||
dictionary=command_output,
|
||||
key=f"vrfs.{instance.vrf}",
|
||||
default=None,
|
||||
)
|
||||
if vrf_data is None:
|
||||
skip_vrfs.append(instance.vrf)
|
||||
failure_message.append(f"VRF {instance.vrf} is not configured to run segment routging.")
|
||||
|
||||
elif get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None:
|
||||
skip_instances.append(instance.name)
|
||||
failure_message.append(f"Instance {instance.name} is not found in vrf {instance.vrf}.")
|
||||
|
||||
# Check Adjacency segments
|
||||
for instance in self.inputs.instances:
|
||||
if instance.vrf not in skip_vrfs and instance.name not in skip_instances:
|
||||
for input_segment in instance.segments:
|
||||
eos_segment = _get_adjacency_segment_data_by_neighbor(
|
||||
neighbor=str(input_segment.address),
|
||||
instance=instance.name,
|
||||
vrf=instance.vrf,
|
||||
command_output=command_output,
|
||||
)
|
||||
if eos_segment is None:
|
||||
failure_message.append(f"Your segment has not been found: {input_segment}.")
|
||||
|
||||
elif (
|
||||
eos_segment["localIntf"] != input_segment.interface
|
||||
or eos_segment["level"] != input_segment.level
|
||||
or eos_segment["sidOrigin"] != input_segment.sid_origin
|
||||
):
|
||||
failure_message.append(f"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.")
|
||||
if failure_message:
|
||||
self.result.is_failure("\n".join(failure_message))
|
||||
|
||||
|
||||
class VerifyISISSegmentRoutingDataplane(AntaTest):
|
||||
"""
|
||||
Verify dataplane of a list of ISIS-SR instances.
|
||||
|
||||
Expected Results
|
||||
----------------
|
||||
* Success: The test will pass if all instances have correct dataplane configured
|
||||
* Failure: The test will fail if one of the instances has incorrect dataplane configured
|
||||
* Skipped: The test will be skipped if ISIS is not running
|
||||
|
||||
Examples
|
||||
--------
|
||||
```yaml
|
||||
anta.tests.routing:
|
||||
isis:
|
||||
- VerifyISISSegmentRoutingDataplane:
|
||||
instances:
|
||||
- name: CORE-ISIS
|
||||
vrf: default
|
||||
dataplane: MPLS
|
||||
```
|
||||
"""
|
||||
|
||||
name = "VerifyISISSegmentRoutingDataplane"
|
||||
description = "Verify dataplane of a list of ISIS-SR instances"
|
||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing", ofmt="json")]
|
||||
|
||||
class Input(AntaTest.Input):
|
||||
"""Input model for the VerifyISISSegmentRoutingDataplane test."""
|
||||
|
||||
instances: list[IsisInstance]
|
||||
|
||||
class IsisInstance(BaseModel):
|
||||
"""ISIS Instance model definition."""
|
||||
|
||||
name: str
|
||||
"""ISIS instance name."""
|
||||
vrf: str = "default"
|
||||
"""VRF name where ISIS instance is configured."""
|
||||
dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS"
|
||||
"""Configured dataplane for the instance."""
|
||||
|
||||
@AntaTest.anta_test
|
||||
def test(self) -> None:
|
||||
"""Main test function for VerifyISISSegmentRoutingDataplane."""
|
||||
command_output = self.instance_commands[0].json_output
|
||||
self.result.is_success()
|
||||
|
||||
if len(command_output["vrfs"]) == 0:
|
||||
self.result.is_skipped("IS-IS-SR is not running on device.")
|
||||
return
|
||||
|
||||
# initiate defaults
|
||||
failure_message = []
|
||||
skip_vrfs = []
|
||||
skip_instances = []
|
||||
|
||||
# Check if VRFs and instances are present in output.
|
||||
for instance in self.inputs.instances:
|
||||
vrf_data = get_value(
|
||||
dictionary=command_output,
|
||||
key=f"vrfs.{instance.vrf}",
|
||||
default=None,
|
||||
)
|
||||
if vrf_data is None:
|
||||
skip_vrfs.append(instance.vrf)
|
||||
failure_message.append(f"VRF {instance.vrf} is not configured to run segment routing.")
|
||||
|
||||
elif get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None:
|
||||
skip_instances.append(instance.name)
|
||||
failure_message.append(f"Instance {instance.name} is not found in vrf {instance.vrf}.")
|
||||
|
||||
# Check Adjacency segments
|
||||
for instance in self.inputs.instances:
|
||||
if instance.vrf not in skip_vrfs and instance.name not in skip_instances:
|
||||
eos_dataplane = get_value(dictionary=command_output, key=f"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane", default=None)
|
||||
if instance.dataplane.upper() != eos_dataplane:
|
||||
failure_message.append(f"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})")
|
||||
|
||||
if failure_message:
|
||||
self.result.is_failure("\n".join(failure_message))
|
||||
|
||||
|
||||
class VerifyISISSegmentRoutingTunnels(AntaTest):
|
||||
"""
|
||||
Verify ISIS-SR tunnels computed by device.
|
||||
|
||||
Expected Results
|
||||
----------------
|
||||
* Success: The test will pass if all listed tunnels are computed on device.
|
||||
* Failure: The test will fail if one of the listed tunnels is missing.
|
||||
* Skipped: The test will be skipped if ISIS-SR is not configured.
|
||||
|
||||
Examples
|
||||
--------
|
||||
```yaml
|
||||
anta.tests.routing:
|
||||
isis:
|
||||
- VerifyISISSegmentRoutingTunnels:
|
||||
entries:
|
||||
# Check only endpoint
|
||||
- endpoint: 1.0.0.122/32
|
||||
# Check endpoint and via TI-LFA
|
||||
- endpoint: 1.0.0.13/32
|
||||
vias:
|
||||
- type: tunnel
|
||||
tunnel_id: ti-lfa
|
||||
# Check endpoint and via IP routers
|
||||
- endpoint: 1.0.0.14/32
|
||||
vias:
|
||||
- type: ip
|
||||
nexthop: 1.1.1.1
|
||||
```
|
||||
"""
|
||||
|
||||
name = "VerifyISISSegmentRoutingTunnels"
|
||||
description = "Verify ISIS-SR tunnels computed by device"
|
||||
categories: ClassVar[list[str]] = ["isis", "segment-routing"]
|
||||
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis segment-routing tunnel", ofmt="json")]
|
||||
|
||||
class Input(AntaTest.Input):
|
||||
"""Input model for the VerifyISISSegmentRoutingTunnels test."""
|
||||
|
||||
entries: list[Entry]
|
||||
"""List of tunnels to check on device."""
|
||||
|
||||
class Entry(BaseModel):
|
||||
"""Definition of a tunnel entry."""
|
||||
|
||||
endpoint: IPv4Network
|
||||
"""Endpoint IP of the tunnel."""
|
||||
vias: list[Vias] | None = None
|
||||
"""Optional list of path to reach endpoint."""
|
||||
|
||||
class Vias(BaseModel):
|
||||
"""Definition of a tunnel path."""
|
||||
|
||||
nexthop: IPv4Address | None = None
|
||||
"""Nexthop of the tunnel. If None, then it is not tested. Default: None"""
|
||||
type: Literal["ip", "tunnel"] | None = None
|
||||
"""Type of the tunnel. If None, then it is not tested. Default: None"""
|
||||
interface: Interface | None = None
|
||||
"""Interface of the tunnel. If None, then it is not tested. Default: None"""
|
||||
tunnel_id: Literal["TI-LFA", "ti-lfa", "unset"] | None = None
|
||||
"""Computation method of the tunnel. If None, then it is not tested. Default: None"""
|
||||
|
||||
def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = "endpoint") -> dict[str, Any] | None:
|
||||
return next(
|
||||
(entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),
|
||||
None,
|
||||
)
|
||||
|
||||
@AntaTest.anta_test
|
||||
def test(self) -> None:
|
||||
"""Main test function for VerifyISISSegmentRoutingTunnels.
|
||||
|
||||
This method performs the main test logic for verifying ISIS Segment Routing tunnels.
|
||||
It checks the command output, initiates defaults, and performs various checks on the tunnels.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
command_output = self.instance_commands[0].json_output
|
||||
self.result.is_success()
|
||||
|
||||
# initiate defaults
|
||||
failure_message = []
|
||||
|
||||
if len(command_output["entries"]) == 0:
|
||||
self.result.is_skipped("IS-IS-SR is not running on device.")
|
||||
return
|
||||
|
||||
for input_entry in self.inputs.entries:
|
||||
eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output["entries"])
|
||||
if eos_entry is None:
|
||||
failure_message.append(f"Tunnel to {input_entry} is not found.")
|
||||
elif input_entry.vias is not None:
|
||||
failure_src = []
|
||||
for via_input in input_entry.vias:
|
||||
if not self._check_tunnel_type(via_input, eos_entry):
|
||||
failure_src.append("incorrect tunnel type")
|
||||
if not self._check_tunnel_nexthop(via_input, eos_entry):
|
||||
failure_src.append("incorrect nexthop")
|
||||
if not self._check_tunnel_interface(via_input, eos_entry):
|
||||
failure_src.append("incorrect interface")
|
||||
if not self._check_tunnel_id(via_input, eos_entry):
|
||||
failure_src.append("incorrect tunnel ID")
|
||||
|
||||
if failure_src:
|
||||
failure_message.append(f"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}")
|
||||
|
||||
if failure_message:
|
||||
self.result.is_failure("\n".join(failure_message))
|
||||
|
||||
def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.
|
||||
|
||||
Args:
|
||||
via_input (VerifyISISSegmentRoutingTunnels.Input.Entry.Vias): The input tunnel type to check.
|
||||
eos_entry (dict[str, Any]): The EOS entry containing the tunnel types.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.
|
||||
"""
|
||||
if via_input.type is not None:
|
||||
return any(
|
||||
via_input.type
|
||||
== get_value(
|
||||
dictionary=eos_via,
|
||||
key="type",
|
||||
default="undefined",
|
||||
)
|
||||
for eos_via in eos_entry["vias"]
|
||||
)
|
||||
return True
|
||||
|
||||
def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the tunnel nexthop matches the given input.
|
||||
|
||||
Args:
|
||||
via_input (VerifyISISSegmentRoutingTunnels.Input.Entry.Vias): The input via object.
|
||||
eos_entry (dict[str, Any]): The EOS entry dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if the tunnel nexthop matches, False otherwise.
|
||||
"""
|
||||
if via_input.nexthop is not None:
|
||||
return any(
|
||||
str(via_input.nexthop)
|
||||
== get_value(
|
||||
dictionary=eos_via,
|
||||
key="nexthop",
|
||||
default="undefined",
|
||||
)
|
||||
for eos_via in eos_entry["vias"]
|
||||
)
|
||||
return True
|
||||
|
||||
def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the tunnel interface exists in the given EOS entry.
|
||||
|
||||
Args:
|
||||
via_input (VerifyISISSegmentRoutingTunnels.Input.Entry.Vias): The input via object.
|
||||
eos_entry (dict[str, Any]): The EOS entry dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if the tunnel interface exists, False otherwise.
|
||||
"""
|
||||
if via_input.interface is not None:
|
||||
return any(
|
||||
via_input.interface
|
||||
== get_value(
|
||||
dictionary=eos_via,
|
||||
key="interface",
|
||||
default="undefined",
|
||||
)
|
||||
for eos_via in eos_entry["vias"]
|
||||
)
|
||||
return True
|
||||
|
||||
def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.
|
||||
|
||||
Args:
|
||||
via_input (VerifyISISSegmentRoutingTunnels.Input.Entry.Vias): The input vias to check.
|
||||
eos_entry (dict[str, Any]): The EOS entry to compare against.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.
|
||||
"""
|
||||
if via_input.tunnel_id is not None:
|
||||
return any(
|
||||
via_input.tunnel_id.upper()
|
||||
== get_value(
|
||||
dictionary=eos_via,
|
||||
key="tunnelId.type",
|
||||
default="undefined",
|
||||
).upper()
|
||||
for eos_via in eos_entry["vias"]
|
||||
)
|
||||
return True
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
# Arista Network Test Automation (ANTA) Framework
|
||||
|
||||
| **Code** | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Numpy](https://img.shields.io/badge/Docstring_format-numpy-blue)](https://numpydoc.readthedocs.io/en/latest/format.html) |
|
||||
| **Code** | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Numpy](https://img.shields.io/badge/Docstring_format-numpy-blue)](https://numpydoc.readthedocs.io/en/latest/format.html) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=aristanetworks_anta&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=aristanetworks_anta) |
|
||||
| :------------: | :-------|
|
||||
| **License** | [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/arista-netdevops-community/anta/blob/main/LICENSE) |
|
||||
| **GitHub** | [![CI](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) ![Coverage](https://raw.githubusercontent.com/arista-netdevops-community/anta/coverage-badge/latest-release-coverage.svg) ![Commit](https://img.shields.io/github/last-commit/arista-netdevops-community/anta) ![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/) [![Contributors](https://img.shields.io/github/contributors/arista-netdevops-community/anta)](https://github.com/arista-netdevops-community/anta/graphs/contributors) |
|
||||
| **License** | [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](https://github.com/aristanetworks/anta/blob/main/LICENSE) |
|
||||
| **GitHub** | [![CI](https://github.com/aristanetworks/anta/actions/workflows/code-testing.yml/badge.svg)](https://github.com/aristanetworks/anta/actions/workflows/code-testing.yml) ![Coverage](https://raw.githubusercontent.com/aristanetworks/anta/coverage-badge/latest-release-coverage.svg) ![Commit](https://img.shields.io/github/last-commit/aristanetworks/anta) ![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/aristanetworks/anta) [![Github release](https://img.shields.io/github/release/aristanetworks/anta.svg)](https://github.com/aristanetworks/anta/releases/) [![Contributors](https://img.shields.io/github/contributors/aristanetworks/anta)](https://github.com/aristanetworks/anta/graphs/contributors) |
|
||||
| **PyPi** | ![PyPi Version](https://img.shields.io/pypi/v/anta) ![Python Versions](https://img.shields.io/pypi/pyversions/anta) ![Python format](https://img.shields.io/pypi/format/anta) ![PyPI - Downloads](https://img.shields.io/pypi/dm/anta) |
|
||||
|
||||
ANTA is Python framework that automates tests for Arista devices.
|
||||
|
@ -22,7 +22,7 @@ ANTA is Python framework that automates tests for Arista devices.
|
|||
- As a [Python library](advanced_usages/as-python-lib.md) in your own application
|
||||
- The [ANTA CLI](cli/overview.md)
|
||||
|
||||
![anta nrfu](https://raw.githubusercontent.com/arista-netdevops-community/anta/main/docs/imgs/anta-nrfu.svg)
|
||||
![anta nrfu](https://raw.githubusercontent.com/aristanetworks/anta/main/docs/imgs/anta-nrfu.svg)
|
||||
|
||||
## Install ANTA library
|
||||
|
||||
|
@ -72,12 +72,12 @@ Commands:
|
|||
You can also still choose to install it with directly with `pip`:
|
||||
|
||||
```bash
|
||||
$ pip install anta[cli]
|
||||
pip install anta[cli]
|
||||
```
|
||||
|
||||
## 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.
|
||||
The documentation is published on [ANTA package website](https://anta.arista.com).
|
||||
|
||||
## Contribution guide
|
||||
|
||||
|
|
|
@ -334,10 +334,10 @@ For that, you need to create your own Python package as described in this [hitch
|
|||
|
||||
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:
|
||||
Let say the custom Python package is `anta_custom` and the test is defined in `anta_custom.dc_project` Python module, the test catalog would look like:
|
||||
|
||||
```yaml
|
||||
anta_titom73.dc_project:
|
||||
anta_custom.dc_project:
|
||||
- VerifyFeatureX:
|
||||
minimum: 1
|
||||
```
|
||||
|
|
|
@ -8,20 +8,31 @@
|
|||
|
||||
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.
|
||||
|
||||
!!! info
|
||||
The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS).
|
||||
|
||||
### Command overview
|
||||
|
||||
```bash
|
||||
anta get from-cvp --help
|
||||
Usage: anta get from-cvp [OPTIONS]
|
||||
|
||||
Build ANTA inventory from Cloudvision
|
||||
Build ANTA inventory from CloudVision.
|
||||
|
||||
NOTE: Only username/password authentication is supported for on-premises CloudVision instances.
|
||||
Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.
|
||||
|
||||
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
|
||||
-o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;
|
||||
required]
|
||||
--overwrite Do not prompt when overriding current inventory [env
|
||||
var: ANTA_GET_FROM_CVP_OVERWRITE]
|
||||
-host, --host TEXT CloudVision instance FQDN or IP [required]
|
||||
-u, --username TEXT CloudVision username [required]
|
||||
-p, --password TEXT CloudVision password [required]
|
||||
-c, --container TEXT CloudVision container where devices are configured
|
||||
--ignore-cert By default connection to CV will use HTTPS
|
||||
certificate, set this flag to disable it [env var:
|
||||
ANTA_GET_FROM_CVP_IGNORE_CERT]
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ Options:
|
|||
```bash
|
||||
anta nrfu --device DC1-LEAF1A text
|
||||
```
|
||||
[![anta nrfu text results](../imgs/anta-nrfu-text-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-text-output.png)
|
||||
![anta nrfu text results](../imgs/anta-nrfu-text-output.png){ loading=lazy width="1600" }
|
||||
|
||||
## Performing NRFU with table rendering
|
||||
|
||||
|
@ -92,31 +92,31 @@ The `--group-by` option show a summarized view of the test results per host or p
|
|||
```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)
|
||||
![anta nrfu table results](../imgs/anta-nrfu-table-output.png){ loading=lazy width="1600" }
|
||||
|
||||
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)
|
||||
![$1anta nrfu table group_by_host_output](../imgs/anta-nrfu-table-group-by-host-output.png){ loading=lazy width="1600" }
|
||||
|
||||
```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)
|
||||
![$1anta nrfu table group_by_test_output](../imgs/anta-nrfu-table-group-by-test-output.png){ loading=lazy width="1600" }
|
||||
|
||||
To get more specific information, it is possible to filter on a single device or a single test:
|
||||
|
||||
```bash
|
||||
anta nrfu --device spine1 table
|
||||
```
|
||||
[![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)
|
||||
![$1anta nrfu table filter_host_output](../imgs/anta-nrfu-table-filter-host-output.png){ loading=lazy width="1600" }
|
||||
|
||||
```bash
|
||||
anta nrfu --test VerifyZeroTouch table
|
||||
```
|
||||
[![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)
|
||||
![$1anta nrfu table filter_test_output](../imgs/anta-nrfu-table-filter-test-output.png){ loading=lazy width="1600" }
|
||||
|
||||
## Performing NRFU with JSON rendering
|
||||
|
||||
|
@ -143,7 +143,7 @@ The `--output` option allows you to save the JSON report as a file.
|
|||
```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)
|
||||
![$1anta nrfu json results](../imgs/anta-nrfu-json-output.png){ loading=lazy width="1600" }
|
||||
|
||||
## Performing NRFU with custom reports
|
||||
|
||||
|
@ -173,7 +173,7 @@ The `--output` option allows you to choose the path where the final report will
|
|||
```bash
|
||||
anta nrfu --tags LEAF tpl-report --template ./custom_template.j2
|
||||
```
|
||||
[![anta nrfu tpl_resultss](../imgs/anta-nrfu-tpl-report-output.png){ loading=lazy width="1600" }](../imgs/anta-nrfu-tpl-report-output.png)
|
||||
![$1anta nrfu tpl_results](../imgs/anta-nrfu-tpl-report-output.png){ loading=lazy width="1600" }
|
||||
|
||||
The template `./custom_template.j2` is a simple Jinja2 template:
|
||||
|
||||
|
@ -205,4 +205,4 @@ cat nrfu-tpl-report.txt
|
|||
|
||||
It is possible to run `anta nrfu --dry-run` to execute ANTA up to the point where it should communicate with the network to execute the tests. When using `--dry-run`, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory.
|
||||
|
||||
[![anta nrfu dry_run](../imgs/anta_nrfu___dry_run.svg){ loading=lazy width="1600" }](../imgs/anta_nrfu___dry_run.svg)
|
||||
![$1anta nrfu dry_run](../imgs/anta_nrfu___dry_run.svg){ loading=lazy width="1600" }
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
# 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.
|
||||
Contribution model is based on a fork-model. Don't push to aristanetworks/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.
|
||||
|
||||
|
@ -16,7 +16,7 @@ Run the following commands to create an ANTA development environment:
|
|||
|
||||
```bash
|
||||
# Clone repository
|
||||
$ git clone https://github.com/arista-netdevops-community/anta.git
|
||||
$ git clone https://github.com/aristanetworks/anta.git
|
||||
$ cd anta
|
||||
|
||||
# Install ANTA in editable mode and its development tools
|
||||
|
@ -28,7 +28,7 @@ $ pip install -e .[dev,cli]
|
|||
$ pip list -e
|
||||
Package Version Editable project location
|
||||
------- ------- -------------------------
|
||||
anta 0.15.0 /mnt/lab/projects/anta
|
||||
anta 1.0.0 /mnt/lab/projects/anta
|
||||
```
|
||||
|
||||
Then, [`tox`](https://tox.wiki/) is configured with few environments to run CI locally:
|
||||
|
@ -229,4 +229,4 @@ muffet -c 2 --color=always http://127.0.0.1:8000 -e fonts.gstatic.com -b 8192
|
|||
|
||||
## 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).
|
||||
GitHub actions is used to test git pushes and pull requests. The workflows are defined in this [directory](https://github.com/aristanetworks/anta/tree/main/.github/workflows). We can view the results [here](https://github.com/aristanetworks/anta/actions).
|
||||
|
|
|
@ -126,4 +126,4 @@ toc_depth: 2
|
|||
|
||||
# Still facing issues?
|
||||
|
||||
If you've tried the above solutions and continue to experience problems, please follow the [troubleshooting](troubleshooting.md) instructions and report the issue in our [GitHub repository](https://github.com/arista-netdevops-community/anta).
|
||||
If you've tried the above solutions and continue to experience problems, please follow the [troubleshooting](troubleshooting.md) instructions and report the issue in our [GitHub repository](https://github.com/aristanetworks/anta).
|
||||
|
|
|
@ -121,12 +121,6 @@ anta.tests.configuration:
|
|||
|
||||
## Test your network
|
||||
|
||||
### Basic usage in a python script
|
||||
|
||||
```python
|
||||
--8<-- "anta_runner.py"
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
ANTA comes with a generic CLI entrypoint to run tests in your network. It requires an inventory file as well as a test catalog.
|
||||
|
@ -264,3 +258,9 @@ $ anta nrfu \
|
|||
```
|
||||
|
||||
You can find more information under the __usage__ section of the website
|
||||
|
||||
### Basic usage in a Python script
|
||||
|
||||
```python
|
||||
--8<-- "anta_runner.py"
|
||||
```
|
||||
|
|
|
@ -15,3 +15,7 @@
|
|||
</script>
|
||||
{{app}}
|
||||
{% endblock %}
|
||||
|
||||
{% block announce %}
|
||||
ANTA code has moved to a new house in aristanetworks organization and so has the documentation. <strong>Please update your bookmark to use <a href="https://anta.arista.com">anta.arista.com<a/></strong>
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,7 +19,7 @@ Python 3.11.8
|
|||
|
||||
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.
|
||||
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/aristanetworks/anta/blob/main/pyproject.toml) file, under dependencies.
|
||||
|
||||
|
||||
### Install library from Pypi server
|
||||
|
@ -59,18 +59,18 @@ pip install anta[cli]
|
|||
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git#egg=anta[cli]
|
||||
pip install git+https://github.com/aristanetworks/anta.git
|
||||
pip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]
|
||||
|
||||
# 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-feature-branch>#egg=anta[cli]
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]
|
||||
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git@<cool-tag>
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git@<cool-tag>#egg=anta[cli]
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<cool-tag>
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]
|
||||
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git@<more-or-less-cool-hash>
|
||||
pip install git+https://github.com/arista-netdevops-community/anta.git@<more-or-less-cool-hash>#egg=anta[cli]
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>
|
||||
pip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]
|
||||
```
|
||||
|
||||
### Check installation
|
||||
|
@ -93,7 +93,7 @@ which anta
|
|||
```bash
|
||||
# Check ANTA version
|
||||
anta --version
|
||||
anta, version v0.15.0
|
||||
anta, version v1.0.0
|
||||
```
|
||||
|
||||
## EOS Requirements
|
||||
|
|
|
@ -56,7 +56,8 @@ if __name__ == "__main__":
|
|||
# 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")}
|
||||
console_scripts = entry_points(group="console_scripts")
|
||||
scripts = {script.name: script for script in console_scripts}
|
||||
|
||||
if script_name in scripts:
|
||||
# A VALID SCRIPT WAS passed
|
||||
|
|
|
@ -61,6 +61,12 @@
|
|||
--md-code-border-color: #aec6db4f;
|
||||
}
|
||||
|
||||
.md-banner {
|
||||
background-color: #f5c842;
|
||||
color: #000000;
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 76.25em) {
|
||||
.md-main__inner, .md-header__inner {
|
||||
max-width: 85%;
|
||||
|
@ -128,12 +134,8 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.md-typeset h4::before {
|
||||
content: ">> ";
|
||||
}
|
||||
|
||||
.md-typeset h4 {
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
margin: 1em 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: -.01em;
|
||||
|
|
|
@ -11,9 +11,9 @@ A couple of things to check when hitting an issue with ANTA:
|
|||
```mermaid
|
||||
flowchart LR
|
||||
A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}
|
||||
B -- Yes --> C{Does the FAQ solution<<br />works for me?}
|
||||
B -- Yes --> C{Does the FAQ solution<br />works for me?}
|
||||
C -- Yes --> V(((Victory)))
|
||||
B -->|No| E{Is my problem<br />mentioned in one<<br />of the open issues?}
|
||||
B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}
|
||||
C -->|No| E
|
||||
E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}
|
||||
F -- Yes --> U[Upgrade]
|
||||
|
@ -24,8 +24,8 @@ flowchart LR
|
|||
F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))
|
||||
|
||||
click B "../faq" "FAQ"
|
||||
click E "https://github.com/arista-netdevops-community/anta/issues"
|
||||
click H "https://github.com/arista-netdevops-community/anta/issues"
|
||||
click E "https://github.com/aristanetworks/anta/issues"
|
||||
click H "https://github.com/aristanetworks/anta/issues"
|
||||
style A stroke:#f00,stroke-width:2px
|
||||
```
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ anta.tests:
|
|||
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.
|
||||
[This test catalog example](https://github.com/aristanetworks/anta/blob/main/examples/tests.yaml) is maintained with all the tests defined in the `anta.tests` Python module.
|
||||
|
||||
### Test tags
|
||||
|
||||
|
@ -205,10 +205,10 @@ anta.tests.configuration:
|
|||
### 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:
|
||||
So for instance, if my custom tests are defined in the `custom.tests.system` Python module, the test catalog will be:
|
||||
|
||||
```yaml
|
||||
titom73.tests.system:
|
||||
custom.tests.system:
|
||||
- VerifyPlatform:
|
||||
type: ['cEOS-LAB']
|
||||
```
|
||||
|
@ -269,7 +269,7 @@ if __name__ == "__main__":
|
|||
# Apply filters to all tests for this device
|
||||
for test in c.tests:
|
||||
test.inputs.filters = AntaTest.Input.Filters(tags=[device])
|
||||
catalog.merge(c)
|
||||
catalog = catalog.merge(c)
|
||||
with open(Path('anta-catalog.yml'), "w") as f:
|
||||
f.write(catalog.dump().yaml())
|
||||
```
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
## Device Inventory
|
||||
|
||||
The file [inventory.yaml](inventory.yaml) is an example of [device inventory](https://www.anta.ninja/stable/usage-inventory-catalog/#create-an-inventory-file).
|
||||
The file [inventory.yaml](inventory.yaml) is an example of [device inventory](https://anta.arista.com/stable/usage-inventory-catalog/#create-an-inventory-file).
|
||||
|
||||
## Test Catalog
|
||||
|
||||
The file [tests.yaml](tests.yaml) is an example of a [test catalog](https://www.anta.ninja/stable/usage-inventory-catalog/#test-catalog).
|
||||
The file [tests.yaml](tests.yaml) is an example of a [test catalog](https://anta.arista.com/stable/usage-inventory-catalog/#test-catalog).
|
||||
This file should contain all the tests implemented in [anta.tests](../anta/tests) with arbitrary parameters.
|
||||
|
||||
## eos-commands.yaml file
|
||||
|
||||
The file [eos-commands.yaml](eos-commands.yaml) is an example of input given with the `--commands-list` option to the [anta exec snapshot](https://www.anta.ninja/stable/cli/exec/#collect-a-set-of-commands) command.
|
||||
The file [eos-commands.yaml](eos-commands.yaml) is an example of input given with the `--commands-list` option to the [anta exec snapshot](https://anta.arista.com/stable/cli/exec/#collect-a-set-of-commands) command.
|
||||
|
|
14
mkdocs.yml
14
mkdocs.yml
|
@ -6,7 +6,7 @@ copyright: Copyright © 2019 - 2024 Arista Networks
|
|||
|
||||
# Repository
|
||||
repo_name: ANTA on Github
|
||||
repo_url: https://github.com/arista-netdevops-community/anta
|
||||
repo_url: https://github.com/aristanetworks/anta
|
||||
|
||||
# Configuration
|
||||
use_directory_urls: true
|
||||
|
@ -57,9 +57,9 @@ theme:
|
|||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/arista-netdevops-community/anta
|
||||
link: https://github.com/aristanetworks/anta
|
||||
- icon: fontawesome/brands/docker
|
||||
link: https://github.com/arista-netdevops-community/anta/pkgs/container/anta
|
||||
link: https://github.com/aristanetworks/anta/pkgs/container/anta
|
||||
- icon: fontawesome/brands/python
|
||||
link: https://pypi.org/project/anta/
|
||||
version:
|
||||
|
@ -112,6 +112,14 @@ plugins:
|
|||
- git-revision-date-localized:
|
||||
type: date
|
||||
- mike:
|
||||
- glightbox:
|
||||
background: none
|
||||
shadow: true
|
||||
touchNavigation: true
|
||||
loop: false
|
||||
effect: fade
|
||||
slide_effect: slide
|
||||
width: 90vw
|
||||
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
|
|
|
@ -5,14 +5,16 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "anta"
|
||||
version = "v0.15.0"
|
||||
version = "v1.0.0"
|
||||
readme = "docs/README.md"
|
||||
authors = [{ name = "Khelil Sator", email = "ksator@arista.com" }]
|
||||
authors = [{ name = "Arista Networks ANTA maintainers", email = "anta-dev@arista.com" }]
|
||||
maintainers = [
|
||||
{ name = "Arista Networks ANTA maintainers", email = "anta-dev@arista.com" },
|
||||
{ name = "Khelil Sator", email = "ksator@arista.com" },
|
||||
{ name = "Matthieu Tâche", email = "mtache@arista.com" },
|
||||
{ name = "Thomas Grimonet", email = "tgrimonet@arista.com" },
|
||||
{ name = "Guillaume Mulocher", email = "gmulocher@arista.com" },
|
||||
{ name = "Carl Baillargeon", email = "carl.baillargeon@arista.com" },
|
||||
]
|
||||
description = "Arista Network Test Automation (ANTA) Framework"
|
||||
license = { file = "LICENSE" }
|
||||
|
@ -33,7 +35,7 @@ keywords = ["test", "anta", "Arista", "network", "automation", "networking", "de
|
|||
classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Intended Audience :: Information Technology",
|
||||
|
@ -57,7 +59,7 @@ cli = [
|
|||
]
|
||||
dev = [
|
||||
"bumpver>=2023.1129",
|
||||
"codespell~=2.2.6",
|
||||
"codespell>=2.2.6,<2.4.0",
|
||||
"mypy-extensions~=1.0",
|
||||
"mypy~=1.10",
|
||||
"pre-commit>=3.3.3",
|
||||
|
@ -89,12 +91,13 @@ doc = [
|
|||
"mkdocs-material>=8.3.9",
|
||||
"mkdocs>=1.3.1",
|
||||
"mkdocstrings[python]>=0.20.0",
|
||||
"mkdocs-glightbox>=0.4.0"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.anta.ninja"
|
||||
"Bug Tracker" = "https://github.com/arista-netdevops-community/anta/issues"
|
||||
Contributing = "https://www.anta.ninja/main/contribution/"
|
||||
Homepage = "https://anta.arista.com"
|
||||
"Bug Tracker" = "https://github.com/aristanetworks/anta/issues"
|
||||
Contributing = "https://anta.arista.com/main/contribution/"
|
||||
|
||||
[project.scripts]
|
||||
anta = "anta.cli:cli"
|
||||
|
@ -110,7 +113,7 @@ namespaces = false
|
|||
# Version
|
||||
################################
|
||||
[tool.bumpver]
|
||||
current_version = "0.15.0"
|
||||
current_version = "1.0.0"
|
||||
version_pattern = "MAJOR.MINOR.PATCH"
|
||||
commit_message = "bump: Version {old_version} -> {new_version}"
|
||||
commit = true
|
||||
|
@ -396,6 +399,10 @@ runtime-evaluated-base-classes = ["pydantic.BaseModel", "anta.models.AntaTest.In
|
|||
"C901", # TODO: test function is too complex, needs a refactor
|
||||
"PLR0911", # TODO: Too many return statements, same as above needs a refactor
|
||||
]
|
||||
"anta/tests/routing/isis.py" = [
|
||||
"C901", # TODO: test function is too complex, needs a refactor
|
||||
"PLR0912" # Too many branches (15/12) (too-many-branches), needs a refactor
|
||||
]
|
||||
"anta/decorators.py" = [
|
||||
"ANN401", # Ok to use Any type hint in our decorators
|
||||
]
|
||||
|
|
|
@ -44,7 +44,10 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
|||
It will parametrize test cases based on the `DATA` data structure defined in `tests.units.anta_tests` modules.
|
||||
See `tests/units/anta_tests/README.md` for more information on how to use it.
|
||||
Test IDs are generated using the `build_test_id` function above.
|
||||
|
||||
Checking that only the function "test" is parametrized with data to allow for writing tests for helper functions
|
||||
in each module.
|
||||
"""
|
||||
if "tests.units.anta_tests" in metafunc.module.__package__:
|
||||
if "tests.units.anta_tests" in metafunc.module.__package__ and metafunc.function.__name__ == "test":
|
||||
# This is a unit test for an AntaTest subclass
|
||||
metafunc.parametrize("data", metafunc.module.DATA, ids=build_test_id)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from cvprac.cvp_client_errors import CvpApiError
|
||||
|
||||
from anta.cli._main import anta
|
||||
|
@ -24,19 +25,25 @@ DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cvp_container", "cvp_connect_failure"),
|
||||
("cvp_container", "verify_cert", "cv_token_failure", "cvp_connect_failure"),
|
||||
[
|
||||
pytest.param(None, False, id="all devices"),
|
||||
pytest.param("custom_container", False, id="custom container"),
|
||||
pytest.param(None, True, id="cvp connect failure"),
|
||||
pytest.param(None, True, False, False, id="all devices - verify cert"),
|
||||
pytest.param(None, True, True, False, id="all devices - fail SSL check"),
|
||||
pytest.param(None, False, False, False, id="all devices - do not verify cert"),
|
||||
pytest.param("custom_container", False, False, False, id="custom container"),
|
||||
pytest.param(None, False, False, True, id="cvp connect failure"),
|
||||
],
|
||||
)
|
||||
def test_from_cvp(
|
||||
tmp_path: Path,
|
||||
click_runner: CliRunner,
|
||||
cvp_container: str | None,
|
||||
verify_cert: bool,
|
||||
cv_token_failure: bool,
|
||||
cvp_connect_failure: bool,
|
||||
) -> None:
|
||||
# pylint: disable=too-many-arguments
|
||||
# ruff: noqa: C901
|
||||
"""Test `anta get from-cvp`.
|
||||
|
||||
This test verifies that username and password are NOT mandatory to run this command
|
||||
|
@ -57,6 +64,12 @@ def test_from_cvp(
|
|||
|
||||
if cvp_container is not None:
|
||||
cli_args.extend(["--container", cvp_container])
|
||||
if not verify_cert:
|
||||
cli_args.extend(["--ignore-cert"])
|
||||
|
||||
def mock_get_cv_token(*_args: str, **_kwargs: str) -> None:
|
||||
if cv_token_failure:
|
||||
raise requests.exceptions.SSLError
|
||||
|
||||
def mock_cvp_connect(_self: CvpClient, *_args: str, **_kwargs: str) -> None:
|
||||
if cvp_connect_failure:
|
||||
|
@ -64,7 +77,7 @@ def test_from_cvp(
|
|||
|
||||
# always get a token
|
||||
with (
|
||||
patch("anta.cli.get.commands.get_cv_token", return_value="dummy_token"),
|
||||
patch("anta.cli.get.commands.get_cv_token", autospec=True, side_effect=mock_get_cv_token),
|
||||
patch(
|
||||
"cvprac.cvp_client.CvpClient.connect",
|
||||
autospec=True,
|
||||
|
@ -79,20 +92,27 @@ def test_from_cvp(
|
|||
):
|
||||
result = click_runner.invoke(anta, cli_args)
|
||||
|
||||
if not cvp_connect_failure:
|
||||
if not cvp_connect_failure and not cv_token_failure:
|
||||
assert output.exists()
|
||||
|
||||
if cv_token_failure:
|
||||
assert "Authentication to CloudVison failed" in result.output
|
||||
assert result.exit_code == ExitCode.USAGE_ERROR
|
||||
return
|
||||
|
||||
mocked_cvp_connect.assert_called_once()
|
||||
if not cvp_connect_failure:
|
||||
|
||||
if cvp_connect_failure:
|
||||
assert "Error connecting to CloudVision" in result.output
|
||||
assert result.exit_code == ExitCode.USAGE_ERROR
|
||||
return
|
||||
|
||||
assert "Connected to CloudVision" in result.output
|
||||
if cvp_container is not None:
|
||||
mocked_get_devices_in_container.assert_called_once_with(ANY, cvp_container)
|
||||
else:
|
||||
mocked_get_inventory.assert_called_once()
|
||||
assert result.exit_code == ExitCode.OK
|
||||
else:
|
||||
assert "Error connecting to CloudVision" in result.output
|
||||
assert result.exit_code == ExitCode.USAGE_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -19,7 +19,14 @@ from anta.inventory import AntaInventory
|
|||
DATA_DIR: Path = Path(__file__).parents[3].resolve() / "data"
|
||||
|
||||
|
||||
def test_get_cv_token() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"verify_cert",
|
||||
[
|
||||
pytest.param(True, id="Verify cert enabled"),
|
||||
pytest.param(False, id="Verify cert disabled"),
|
||||
],
|
||||
)
|
||||
def test_get_cv_token(verify_cert: bool) -> None:
|
||||
"""Test anta.get.utils.get_cv_token."""
|
||||
ip_addr = "42.42.42.42"
|
||||
username = "ant"
|
||||
|
@ -29,13 +36,13 @@ def test_get_cv_token() -> None:
|
|||
mocked_ret = MagicMock(autospec=requests.Response)
|
||||
mocked_ret.json.return_value = {"sessionId": "simple"}
|
||||
patched_request.return_value = mocked_ret
|
||||
res = get_cv_token(ip_addr, username, password)
|
||||
res = get_cv_token(ip_addr, username, password, verify_cert=verify_cert)
|
||||
patched_request.assert_called_once_with(
|
||||
"POST",
|
||||
"https://42.42.42.42/cvpservice/login/authenticate.do",
|
||||
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
||||
data='{"userId": "ant", "password": "formica"}',
|
||||
verify=False,
|
||||
verify=verify_cert,
|
||||
timeout=10,
|
||||
)
|
||||
assert res == "simple"
|
||||
|
|
Loading…
Add table
Reference in a new issue