anta 1.10.0.dev5__tar.gz → 1.10.0.dev6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/PKG-INFO +1 -1
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/__init__.py +0 -2
- anta-1.10.0.dev6/anta/_advisory/findings/assessment.py +48 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/reporter/reporting.py +10 -19
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/results.py +31 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/status.py +6 -11
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_eos/platform.py +30 -8
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/utils.py +5 -5
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/reporter/md_reporter.py +7 -35
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/reporter/table_reporter.py +3 -11
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/result_manager/__init__.py +11 -15
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/result_manager/models.py +3 -21
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/aaa.py +4 -5
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/sa_117.py +6 -12
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/sa_140.py +7 -15
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/sa_142.py +3 -3
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/sa_146.py +3 -11
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/sa_147.py +14 -31
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/interfaces.py +3 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/PKG-INFO +1 -1
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/SOURCES.txt +1 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/pyproject.toml +4 -4
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/LICENSE +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/README.md +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/base.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/eos_versions.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/eos.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/management.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/platform.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/redirection.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/software.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/ssh.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/facts/terminattr.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/findings/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/findings/models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/findings/projection.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/optional_commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/remediation.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/reporter/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/reporter/csv_reporter.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/reporter/md_reporter.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_advisory/version.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_eos/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_eos/parsing.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_eos/version.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/_runner.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/catalog.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/_main.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/check/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/check/commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/console.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/debug/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/debug/commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/debug/utils.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/exec/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/exec/commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/exec/utils.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/get/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/get/commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/get/utils.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/nrfu/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/nrfu/commands.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/nrfu/utils.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/cli/psirt.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/constants.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/custom_types.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/decorators.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/device.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/avt.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/bfd.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/configuration.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/connectivity.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/cvx.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/evpn.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/flow_tracking.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/hardware.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/interfaces.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/logging.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/path_selection.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/routing/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/routing/bgp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/routing/generic.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/routing/isis.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/routing/ospf.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/security.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/services.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/snmp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/stun.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/system.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/input_models/vlan.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/inventory/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/inventory/exceptions.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/inventory/models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/logger.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/py.typed +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/reporter/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/reporter/csv_reporter.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/reporter/jinja_reporter.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/runner.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/settings.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/advisories/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/avt.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/bfd.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/configuration.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/connectivity.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/cvx.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/evpn.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/field_notices.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/flow_tracking.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/greent.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/hardware.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/lanz.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/logging.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/mlag.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/multicast.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/path_selection.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/profiles.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/ptp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/routing/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/routing/bgp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/routing/generic.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/routing/isis.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/routing/ospf.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/security.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/services.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/snmp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/software.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/stp.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/stun.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/system.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/vlan.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tests/vxlan.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta/tools.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/dependency_links.txt +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/entry_points.txt +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/requires.txt +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/anta.egg-info/top_level.txt +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/__init__.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/_auth.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/_constants.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/_errors.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/_models.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/_types.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/aio_portcheck.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/config_session.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/device.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/asynceapi/errors.py +0 -0
- {anta-1.10.0.dev5 → anta-1.10.0.dev6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anta
|
|
3
|
-
Version: 1.10.0.
|
|
3
|
+
Version: 1.10.0.dev6
|
|
4
4
|
Summary: Arista Network Test Automation (ANTA) Framework
|
|
5
5
|
Author-email: Arista Networks ANTA maintainers <anta-dev@arista.com>
|
|
6
6
|
Maintainer-email: Arista Networks ANTA maintainers <anta-dev@arista.com>, Khelil Sator <ksator@arista.com>, Matthieu Tâche <mtache@arista.com>, Thomas Grimonet <tgrimonet@arista.com>, Guillaume Mulocher <gmulocher@arista.com>, Carl Baillargeon <carl.baillargeon@arista.com>
|
|
@@ -33,7 +33,6 @@ class RICH_COLOR_PALETTE:
|
|
|
33
33
|
|
|
34
34
|
ERROR = "indian_red"
|
|
35
35
|
FAILURE = "bold red"
|
|
36
|
-
INCONCLUSIVE = "bold orange3"
|
|
37
36
|
SUCCESS = "green4"
|
|
38
37
|
SKIPPED = "bold orange4"
|
|
39
38
|
HEADER = "cyan"
|
|
@@ -44,7 +43,6 @@ class RICH_COLOR_PALETTE:
|
|
|
44
43
|
RICH_COLOR_THEME = {
|
|
45
44
|
"success": RICH_COLOR_PALETTE.SUCCESS,
|
|
46
45
|
"skipped": RICH_COLOR_PALETTE.SKIPPED,
|
|
47
|
-
"inconclusive": RICH_COLOR_PALETTE.INCONCLUSIVE,
|
|
48
46
|
"failure": RICH_COLOR_PALETTE.FAILURE,
|
|
49
47
|
"error": RICH_COLOR_PALETTE.ERROR,
|
|
50
48
|
"unset": RICH_COLOR_PALETTE.UNSET,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright (c) 2026 Arista Networks, Inc.
|
|
2
|
+
# Use of this source code is governed by the Apache License 2.0
|
|
3
|
+
# that can be found in the LICENSE file.
|
|
4
|
+
"""Shared assessment steps for structured advisory findings."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from anta._advisory.eos_versions import AffectedStatus, evaluate_version
|
|
11
|
+
from anta._advisory.facts.models import Fact, FactProblemKind, UnavailableFact
|
|
12
|
+
from anta._advisory.findings.models import EosReleaseAssessment, ErrorResult, NotAffectedResult, VersionRelation
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from anta._advisory.eos_versions import VersionRule
|
|
18
|
+
from anta._eos.version import EOSVersion
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assess_eos_scope(
|
|
22
|
+
vulnerability_id: str,
|
|
23
|
+
version: Fact[EOSVersion],
|
|
24
|
+
affected_versions: Sequence[VersionRule],
|
|
25
|
+
) -> EosReleaseAssessment | ErrorResult | NotAffectedResult:
|
|
26
|
+
"""Return an affected EOS assessment or a terminal error or not-affected result."""
|
|
27
|
+
release = assess_eos_version(version, affected_versions)
|
|
28
|
+
if isinstance(release, UnavailableFact):
|
|
29
|
+
return ErrorResult(vulnerability_id=vulnerability_id, problems=(release,))
|
|
30
|
+
if release.relation is VersionRelation.OUTSIDE_SCOPE:
|
|
31
|
+
return NotAffectedResult(vulnerability_id=vulnerability_id, decisive=(release,))
|
|
32
|
+
return release
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def assess_eos_version(
|
|
36
|
+
version: Fact[EOSVersion],
|
|
37
|
+
affected_versions: Sequence[VersionRule],
|
|
38
|
+
) -> EosReleaseAssessment | UnavailableFact[EOSVersion]:
|
|
39
|
+
"""Interpret an EOS version fact without making a terminal vulnerability decision."""
|
|
40
|
+
if isinstance(version, UnavailableFact):
|
|
41
|
+
return version
|
|
42
|
+
|
|
43
|
+
evaluation = evaluate_version(version.value, affected_versions)
|
|
44
|
+
if evaluation.affected_status is AffectedStatus.UNKNOWN:
|
|
45
|
+
return version.definition.unavailable(FactProblemKind.INVALID, version.source)
|
|
46
|
+
|
|
47
|
+
relation = VersionRelation.AFFECTED if evaluation.affected_status is AffectedStatus.AFFECTED else VersionRelation.OUTSIDE_SCOPE
|
|
48
|
+
return EosReleaseAssessment(version, relation)
|
|
@@ -10,7 +10,7 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
12
|
from anta._advisory.models import _ADVISORY_VULNERABILITY_SEVERITY_RANK, _AdvisoryVulnerabilitySeverity
|
|
13
|
-
from anta._advisory.results import _get_advisory_metadata
|
|
13
|
+
from anta._advisory.results import _get_advisory_metadata, _get_advisory_status
|
|
14
14
|
from anta.logger import anta_log_exception
|
|
15
15
|
from anta.result_manager.models import AntaTestStatus
|
|
16
16
|
|
|
@@ -26,27 +26,16 @@ if TYPE_CHECKING:
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
-
_ADVISORY_RESULT_RANK = {
|
|
30
|
-
AntaTestStatus.FAILURE: 0,
|
|
31
|
-
AntaTestStatus.INCONCLUSIVE: 1,
|
|
32
|
-
AntaTestStatus.SUCCESS: 2,
|
|
33
|
-
AntaTestStatus.ERROR: 3,
|
|
34
|
-
AntaTestStatus.SKIPPED: 4,
|
|
35
|
-
AntaTestStatus.UNSET: 5,
|
|
36
|
-
}
|
|
29
|
+
_ADVISORY_RESULT_RANK = {"affected": 0, "inconclusive": 1, "mitigated": 2, "not affected": 3, "error": 4, "skipped": 5, "unset": 6}
|
|
37
30
|
|
|
38
31
|
|
|
39
32
|
def _get_advisory_result(result: TestResult | AtomicTestResult) -> str:
|
|
40
|
-
"""Translate an ANTA status to
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# message text. SUCCESS remains accepted for results that used the earlier success-based encoding.
|
|
44
|
-
if result.result is AntaTestStatus.SUCCESS:
|
|
45
|
-
mitigated_opening = "The device is affected but mitigated because "
|
|
46
|
-
return "mitigated" if any(mitigated_opening in message for message in result.messages) else "not affected"
|
|
33
|
+
"""Translate retained advisory status or an ANTA lifecycle status to report wording."""
|
|
34
|
+
if (status := _get_advisory_status(result)) is not None:
|
|
35
|
+
return status.value.replace("_", " ")
|
|
47
36
|
return {
|
|
48
37
|
AntaTestStatus.UNSET: "unset",
|
|
49
|
-
AntaTestStatus.
|
|
38
|
+
AntaTestStatus.SUCCESS: "not affected",
|
|
50
39
|
AntaTestStatus.FAILURE: "affected",
|
|
51
40
|
AntaTestStatus.ERROR: "error",
|
|
52
41
|
AntaTestStatus.SKIPPED: "skipped",
|
|
@@ -169,7 +158,9 @@ def group_advisory_results(results: Sequence[TestResult]) -> tuple[AdvisoryResul
|
|
|
169
158
|
else:
|
|
170
159
|
group_results = []
|
|
171
160
|
groups[advisory.sa_number] = (advisory, group_results)
|
|
172
|
-
sorted_result = result.model_copy(
|
|
161
|
+
sorted_result = result.model_copy(
|
|
162
|
+
update={"atomic_results": sorted(result.atomic_results, key=lambda atomic: _ADVISORY_RESULT_RANK[_get_advisory_result(atomic)])}
|
|
163
|
+
)
|
|
173
164
|
group_results.append(sorted_result)
|
|
174
165
|
|
|
175
166
|
result_groups = (
|
|
@@ -178,7 +169,7 @@ def group_advisory_results(results: Sequence[TestResult]) -> tuple[AdvisoryResul
|
|
|
178
169
|
results=tuple(
|
|
179
170
|
sorted(
|
|
180
171
|
group_results,
|
|
181
|
-
key=lambda result: (_ADVISORY_RESULT_RANK[result
|
|
172
|
+
key=lambda result: (_ADVISORY_RESULT_RANK[_get_advisory_result(result)], result.name.casefold(), result.test.casefold()),
|
|
182
173
|
)
|
|
183
174
|
),
|
|
184
175
|
)
|
|
@@ -9,12 +9,22 @@ from pydantic import Field
|
|
|
9
9
|
|
|
10
10
|
from anta._advisory.models import _AdvisoryMetadata # noqa: TC001 # Pydantic resolves this annotation at runtime.
|
|
11
11
|
from anta._advisory.remediation import RemediationGuidance, RemediationPlan # noqa: TC001 # Pydantic resolves these annotations at runtime.
|
|
12
|
+
from anta._advisory.status import AdvisoryStatus
|
|
12
13
|
from anta.result_manager.models import AntaTestStatus, AtomicTestResult, TestResult
|
|
13
14
|
|
|
15
|
+
_ADVISORY_STATUS_PRIORITY = {
|
|
16
|
+
AdvisoryStatus.NOT_AFFECTED: 0,
|
|
17
|
+
AdvisoryStatus.MITIGATED: 1,
|
|
18
|
+
AdvisoryStatus.INCONCLUSIVE: 2,
|
|
19
|
+
AdvisoryStatus.AFFECTED: 3,
|
|
20
|
+
AdvisoryStatus.ERROR: 4,
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
|
|
15
24
|
class _AdvisoryAtomicTestResult(AtomicTestResult):
|
|
16
25
|
"""Atomic advisory result with optional vulnerability association and remediation."""
|
|
17
26
|
|
|
27
|
+
advisory_status: AdvisoryStatus | None = Field(default=None, exclude=True)
|
|
18
28
|
vulnerability_ids: tuple[str, ...] | None = Field(default=None, exclude=True)
|
|
19
29
|
remediation: RemediationPlan | None = Field(default=None, exclude=True)
|
|
20
30
|
remediation_guidance: frozenset[RemediationGuidance] = Field(default_factory=frozenset, exclude=True)
|
|
@@ -25,6 +35,18 @@ class _AdvisoryTestResult(TestResult):
|
|
|
25
35
|
|
|
26
36
|
advisory: _AdvisoryMetadata = Field(exclude=True)
|
|
27
37
|
|
|
38
|
+
@property
|
|
39
|
+
def advisory_status(self) -> AdvisoryStatus | None:
|
|
40
|
+
"""Return the highest-priority semantic status from this advisory result."""
|
|
41
|
+
if self.result is AntaTestStatus.ERROR:
|
|
42
|
+
return AdvisoryStatus.ERROR
|
|
43
|
+
statuses = (atomic.advisory_status for atomic in self.atomic_results if isinstance(atomic, _AdvisoryAtomicTestResult))
|
|
44
|
+
return max(
|
|
45
|
+
(status for status in statuses if status is not None),
|
|
46
|
+
key=_ADVISORY_STATUS_PRIORITY.__getitem__,
|
|
47
|
+
default=None,
|
|
48
|
+
)
|
|
49
|
+
|
|
28
50
|
def add(
|
|
29
51
|
self,
|
|
30
52
|
description: str,
|
|
@@ -68,6 +90,15 @@ def _get_advisory_metadata(result: TestResult) -> _AdvisoryMetadata | None:
|
|
|
68
90
|
return result.advisory if isinstance(result, _AdvisoryTestResult) else None
|
|
69
91
|
|
|
70
92
|
|
|
93
|
+
def _get_advisory_status(result: TestResult | AtomicTestResult) -> AdvisoryStatus | None:
|
|
94
|
+
"""Return the semantic status from an advisory result, otherwise None."""
|
|
95
|
+
if isinstance(result, _AdvisoryTestResult):
|
|
96
|
+
return result.advisory_status
|
|
97
|
+
if isinstance(result, _AdvisoryAtomicTestResult):
|
|
98
|
+
return result.advisory_status
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
71
102
|
def _get_atomic_vulnerability_ids(result: AtomicTestResult) -> tuple[str, ...] | None:
|
|
72
103
|
"""Return explicitly associated vulnerability IDs from an advisory atomic result."""
|
|
73
104
|
return result.vulnerability_ids if isinstance(result, _AdvisoryAtomicTestResult) else None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2026 Arista Networks, Inc.
|
|
2
2
|
# Use of this source code is governed by the Apache License 2.0
|
|
3
3
|
# that can be found in the LICENSE file.
|
|
4
|
-
"""Semantic advisory statuses and their
|
|
4
|
+
"""Semantic advisory statuses and their ANTA projection."""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
@@ -34,12 +34,7 @@ def project_advisory_status(
|
|
|
34
34
|
message: str,
|
|
35
35
|
remediation: RemediationPlan | None,
|
|
36
36
|
) -> None:
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
MITIGATED remains distinct throughout assessment and is projected to INCONCLUSIVE only
|
|
40
|
-
because ANTA does not yet expose a native mitigated status. When it does, this function
|
|
41
|
-
is the single compatibility boundary that must change.
|
|
42
|
-
"""
|
|
37
|
+
"""Retain an advisory status, attach remediation, and project it onto ANTA."""
|
|
43
38
|
requires_remediation = status in {AdvisoryStatus.AFFECTED, AdvisoryStatus.MITIGATED, AdvisoryStatus.INCONCLUSIVE}
|
|
44
39
|
if requires_remediation != (remediation is not None):
|
|
45
40
|
requirement = "requires" if requires_remediation else "must not include"
|
|
@@ -53,12 +48,12 @@ def project_advisory_status(
|
|
|
53
48
|
guidance.add(RemediationGuidance.UNRESOLVED_CONDITIONS)
|
|
54
49
|
result.remediation_guidance = frozenset(guidance)
|
|
55
50
|
|
|
56
|
-
|
|
51
|
+
result.advisory_status = status
|
|
52
|
+
|
|
53
|
+
if status in {AdvisoryStatus.NOT_AFFECTED, AdvisoryStatus.MITIGATED}:
|
|
57
54
|
result.is_success(message)
|
|
58
|
-
elif status
|
|
55
|
+
elif status in {AdvisoryStatus.AFFECTED, AdvisoryStatus.INCONCLUSIVE}:
|
|
59
56
|
result.is_failure(message)
|
|
60
|
-
elif status in {AdvisoryStatus.MITIGATED, AdvisoryStatus.INCONCLUSIVE}:
|
|
61
|
-
result.is_inconclusive(message)
|
|
62
57
|
elif status is AdvisoryStatus.ERROR:
|
|
63
58
|
result.is_error(message)
|
|
64
59
|
else:
|
|
@@ -64,13 +64,18 @@ class PlatformFamily(str, Enum):
|
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
66
|
SERIES_720_D = "720D Series"
|
|
67
|
+
SERIES_720_DF = "720DF Series"
|
|
68
|
+
SERIES_720_DP = "720DP Series"
|
|
69
|
+
SERIES_720_DT = "720DT Series"
|
|
67
70
|
SERIES_720_XP = "720XP Series"
|
|
68
71
|
SERIES_720_XDM = "720XDM Series"
|
|
69
72
|
SERIES_722_XPM = "722XPM Series"
|
|
70
|
-
|
|
73
|
+
SERIES_750 = "750 Series"
|
|
74
|
+
SERIES_750_X = "750X Series"
|
|
71
75
|
SERIES_710 = "710 Series"
|
|
72
76
|
SERIES_7010 = "7010 Series"
|
|
73
77
|
SERIES_7010_X = "7010X Series"
|
|
78
|
+
SERIES_7010_TX = "7010TX Series"
|
|
74
79
|
SERIES_7020_R = "7020R Series"
|
|
75
80
|
SERIES_7020_R4 = "7020R4 Series"
|
|
76
81
|
SERIES_7130 = "7130 Series"
|
|
@@ -90,6 +95,7 @@ class PlatformFamily(str, Enum):
|
|
|
90
95
|
SERIES_7260_X = "7260X Series"
|
|
91
96
|
SERIES_7260_X3 = "7260X3 Series"
|
|
92
97
|
SERIES_7280_E = "7280E Series"
|
|
98
|
+
SERIES_7280_SE = "7280SE Series"
|
|
93
99
|
SERIES_7280_R = "7280R Series"
|
|
94
100
|
SERIES_7280_R2 = "7280R2 Series"
|
|
95
101
|
SERIES_7280_R3 = "7280R3 Series"
|
|
@@ -114,7 +120,7 @@ class PlatformFamily(str, Enum):
|
|
|
114
120
|
CLOUDEOS = "CloudEOS"
|
|
115
121
|
CEOS_LAB = "cEOS-lab"
|
|
116
122
|
VEOS_LAB = "vEOS-lab"
|
|
117
|
-
|
|
123
|
+
CVX = "CloudVision eXchange"
|
|
118
124
|
|
|
119
125
|
|
|
120
126
|
@dataclass(frozen=True, slots=True)
|
|
@@ -213,14 +219,16 @@ def _module_rule(role: PlatformComponentRole, *patterns: str) -> _ModuleFamilyRu
|
|
|
213
219
|
|
|
214
220
|
|
|
215
221
|
SYSTEM_PLATFORM_RULES: tuple[_SystemPlatformRule, ...] = (
|
|
216
|
-
_system_rule(PlatformType.FIXED, r"^CCS-
|
|
222
|
+
_system_rule(PlatformType.FIXED, r"^CCS-720DF-.*$", families=(PlatformFamily.SERIES_720_D, PlatformFamily.SERIES_720_DF)),
|
|
223
|
+
_system_rule(PlatformType.FIXED, r"^CCS-720DP-.*$", families=(PlatformFamily.SERIES_720_D, PlatformFamily.SERIES_720_DP)),
|
|
224
|
+
_system_rule(PlatformType.FIXED, r"^CCS-720DT-.*$", families=(PlatformFamily.SERIES_720_D, PlatformFamily.SERIES_720_DT)),
|
|
217
225
|
_system_rule(PlatformType.FIXED, r"^CCS-720XP-.*$", families=(PlatformFamily.SERIES_720_XP,)),
|
|
218
226
|
_system_rule(PlatformType.FIXED, r"^CCS-720XDM-.*$", families=(PlatformFamily.SERIES_720_XDM,)),
|
|
219
227
|
_system_rule(PlatformType.FIXED, r"^CCS-72[02]XPM-.*$", families=(PlatformFamily.SERIES_722_XPM,)),
|
|
220
|
-
_system_rule(PlatformType.CHASSIS, r"^CCS-75[58]-CH.*$", families=(PlatformFamily.
|
|
228
|
+
_system_rule(PlatformType.CHASSIS, r"^CCS-75[58]-CH.*$", families=(PlatformFamily.SERIES_750, PlatformFamily.SERIES_750_X)),
|
|
221
229
|
_system_rule(PlatformType.FIXED, r"^CCS-710[A-Z0-9]*-.*$", families=(PlatformFamily.SERIES_710,)),
|
|
222
230
|
_system_rule(PlatformType.FIXED, r"^DCS-7010T-.*$", families=(PlatformFamily.SERIES_7010,)),
|
|
223
|
-
_system_rule(PlatformType.FIXED, r"^DCS-7010TX-.*$", families=(PlatformFamily.SERIES_7010_X,)),
|
|
231
|
+
_system_rule(PlatformType.FIXED, r"^DCS-7010TX-.*$", families=(PlatformFamily.SERIES_7010_X, PlatformFamily.SERIES_7010_TX)),
|
|
224
232
|
_system_rule(PlatformType.FIXED, r"^DCS-7020[ST]R[A-Z]*-.*$", families=(PlatformFamily.SERIES_7020_R,)),
|
|
225
233
|
_system_rule(PlatformType.FIXED, r"^DCS-7020H?R4M?-.*$", families=(PlatformFamily.SERIES_7020_R4,)),
|
|
226
234
|
_system_rule(PlatformType.FIXED, r"^DCS-7050[A-Z]*X(?!\d).*$", families=(PlatformFamily.SERIES_7050_X,)),
|
|
@@ -239,7 +247,7 @@ SYSTEM_PLATFORM_RULES: tuple[_SystemPlatformRule, ...] = (
|
|
|
239
247
|
_system_rule(PlatformType.FIXED, r"^DCS-7250[A-WY-Z]*X.*$", families=(PlatformFamily.SERIES_7250_X,)),
|
|
240
248
|
_system_rule(PlatformType.FIXED, r"^DCS-7260[A-Z]*X(?!\d).*$", families=(PlatformFamily.SERIES_7260_X,)),
|
|
241
249
|
_system_rule(PlatformType.FIXED, r"^DCS-7260[A-Z]*X3.*$", families=(PlatformFamily.SERIES_7260_X3,)),
|
|
242
|
-
_system_rule(PlatformType.FIXED, r"^DCS-7280SE-.*$", families=(PlatformFamily.SERIES_7280_E,)),
|
|
250
|
+
_system_rule(PlatformType.FIXED, r"^DCS-7280SE-.*$", families=(PlatformFamily.SERIES_7280_E, PlatformFamily.SERIES_7280_SE)),
|
|
243
251
|
_system_rule(PlatformType.FIXED, r"^DCS-7280[CQST]R(?!\d).*$", families=(PlatformFamily.SERIES_7280_R,)),
|
|
244
252
|
_system_rule(PlatformType.FIXED, r"^DCS-7280[CS]R2.*$", families=(PlatformFamily.SERIES_7280_R2,)),
|
|
245
253
|
_system_rule(PlatformType.FIXED, r"^DCS-7280[CDPST]R3.*$", families=(PlatformFamily.SERIES_7280_R3,)),
|
|
@@ -258,19 +266,33 @@ SYSTEM_PLATFORM_RULES: tuple[_SystemPlatformRule, ...] = (
|
|
|
258
266
|
_system_rule(PlatformType.VIRTUAL, r"^CLOUDEOS$", families=(PlatformFamily.CLOUDEOS,)),
|
|
259
267
|
_system_rule(PlatformType.VIRTUAL, r"^(?:CEOS-LAB|CEOSLAB)(?:-.+)?$", families=(PlatformFamily.CEOS_LAB,)),
|
|
260
268
|
_system_rule(PlatformType.VIRTUAL, r"^(?:VEOS-LAB|VEOSLAB)(?:-.+)?$", families=(PlatformFamily.VEOS_LAB,)),
|
|
261
|
-
|
|
269
|
+
# TODO(platform-cvx-cloudeos): Newer CloudEOS releases also report "vEOS".
|
|
270
|
+
# Find an additional device property that distinguishes them before relying
|
|
271
|
+
# on the model string alone for CVX versus CloudEOS classification.
|
|
272
|
+
_system_rule(PlatformType.VIRTUAL, r"^VEOS$", families=(PlatformFamily.CVX,)),
|
|
262
273
|
)
|
|
263
274
|
|
|
264
275
|
|
|
265
276
|
MODULE_PLATFORM_FAMILY_RULES: dict[PlatformFamily, tuple[_ModuleFamilyRule, ...]] = {
|
|
277
|
+
PlatformFamily.SERIES_750: (
|
|
278
|
+
_module_rule(PlatformComponentRole.SUPERVISOR, r"^CCS-750-SUP(?:25|100)(?:-CLK)?$"),
|
|
279
|
+
_module_rule(PlatformComponentRole.SWITCH_CARD, r"^CCS-75[58]-X3-SC$"),
|
|
280
|
+
_module_rule(PlatformComponentRole.LINE_CARD, r"^CCS-750X(?:M|PM)?-.*-LC$"),
|
|
281
|
+
),
|
|
282
|
+
PlatformFamily.SERIES_750_X: (
|
|
283
|
+
_module_rule(PlatformComponentRole.SUPERVISOR, r"^CCS-750-SUP(?:25|100)(?:-CLK)?$"),
|
|
284
|
+
_module_rule(PlatformComponentRole.SWITCH_CARD, r"^CCS-75[58]-X3-SC$"),
|
|
285
|
+
_module_rule(PlatformComponentRole.LINE_CARD, r"^CCS-750X(?:M|PM)?-.*-LC$"),
|
|
286
|
+
),
|
|
266
287
|
PlatformFamily.SERIES_7289_R3A: (_module_rule(PlatformComponentRole.SWITCH_CARD, r"^(?:DCS-)?7289R3A[A-Z]*-SC$"),),
|
|
267
288
|
PlatformFamily.SERIES_7300_X: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7300X(?!\d)-.*-LC$"),),
|
|
268
289
|
PlatformFamily.SERIES_7300_X3: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7300X3-.*-LC$"),),
|
|
290
|
+
PlatformFamily.SERIES_7320_X: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7320X-.*-LC$"),),
|
|
269
291
|
PlatformFamily.SERIES_7358_X4: (_module_rule(PlatformComponentRole.SWITCH_CARD, r"^(?:DCS-)?7358X4-SC$"),),
|
|
270
292
|
PlatformFamily.SERIES_7368_X4: (_module_rule(PlatformComponentRole.SWITCH_CARD, r"^(?:DCS-)?7368X4-SC$"),),
|
|
271
293
|
PlatformFamily.SERIES_7388_X5: (_module_rule(PlatformComponentRole.SWITCH_CARD, r"^(?:DCS-)?7388X5-SC$"),),
|
|
272
294
|
PlatformFamily.SERIES_7500_E: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7500E-.*-LC$"),),
|
|
273
|
-
PlatformFamily.SERIES_7500_R: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7500R(?!\d)
|
|
295
|
+
PlatformFamily.SERIES_7500_R: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7500R(?!\d)[A-Z]*-.*-LC$"),),
|
|
274
296
|
PlatformFamily.SERIES_7500_R2: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7500R2[A-Z]*-.*-LC$"),),
|
|
275
297
|
PlatformFamily.SERIES_7500_R3: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7500R3[A-Z]*-.*-LC$"),),
|
|
276
298
|
PlatformFamily.SERIES_7800_R3: (_module_rule(PlatformComponentRole.LINE_CARD, r"^(?:DCS-)?7800R3[A-Z]*-.*-LC$"),),
|
|
@@ -63,14 +63,14 @@ def parse_tags(_ctx: click.Context, _param: Option, value: str | None) -> set[st
|
|
|
63
63
|
def exit_with_code(ctx: click.Context) -> None:
|
|
64
64
|
"""Exit the Click application with an exit code.
|
|
65
65
|
|
|
66
|
-
This function determines the global test status to be either `unset`, `skipped`, `success`, `
|
|
66
|
+
This function determines the global test status to be either `unset`, `skipped`, `success`, `failure` or `error`
|
|
67
67
|
from the `ResultManager` instance.
|
|
68
68
|
If flag `ignore_error` is set, the `error` status will be ignored in all the tests.
|
|
69
69
|
If flag `ignore_status` is set, the exit code will always be 0.
|
|
70
70
|
Exit the application with the following exit code:
|
|
71
71
|
* 0 if `ignore_status` is `True` or global test status is `unset`, `skipped` or `success`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* 3 if status is `error`
|
|
73
|
+
* 4 if status is `failure`.
|
|
74
74
|
|
|
75
75
|
Parameters
|
|
76
76
|
----------
|
|
@@ -86,7 +86,7 @@ def exit_with_code(ctx: click.Context) -> None:
|
|
|
86
86
|
|
|
87
87
|
if status in {"unset", "skipped", "success"}:
|
|
88
88
|
ctx.exit(ExitCode.OK)
|
|
89
|
-
if status
|
|
89
|
+
if status == "failure":
|
|
90
90
|
ctx.exit(ExitCode.TESTS_FAILED)
|
|
91
91
|
if status == "error":
|
|
92
92
|
ctx.exit(ExitCode.TESTS_ERROR)
|
|
@@ -133,7 +133,7 @@ def result_options(f: Callable[..., R]) -> Callable[..., R]:
|
|
|
133
133
|
default=None,
|
|
134
134
|
type=click.Choice(_HIDE_STATUS, case_sensitive=False),
|
|
135
135
|
multiple=True,
|
|
136
|
-
help="Hide results by type: success /
|
|
136
|
+
help="Hide results by type: success / failure / error / skipped.",
|
|
137
137
|
required=False,
|
|
138
138
|
)(f)
|
|
139
139
|
f = click.option(
|
|
@@ -43,7 +43,6 @@ STATUS_MAP = {
|
|
|
43
43
|
AntaTestStatus.FAILURE: "❌ Failure",
|
|
44
44
|
AntaTestStatus.ERROR: "❗ Error",
|
|
45
45
|
AntaTestStatus.SKIPPED: "⏭️ Skipped",
|
|
46
|
-
AntaTestStatus.INCONCLUSIVE: "❓ Inconclusive",
|
|
47
46
|
AntaTestStatus.UNSET: "Unset",
|
|
48
47
|
}
|
|
49
48
|
"""Mapping of `AntaTestStatus` to their string representation with icons and non-breaking spaces for Markdown."""
|
|
@@ -436,7 +435,6 @@ class SummaryTotals(MDReportBase):
|
|
|
436
435
|
TOTAL_TESTS,
|
|
437
436
|
STATUS_MAP[AntaTestStatus.SUCCESS],
|
|
438
437
|
STATUS_MAP[AntaTestStatus.SKIPPED],
|
|
439
|
-
STATUS_MAP[AntaTestStatus.INCONCLUSIVE],
|
|
440
438
|
STATUS_MAP[AntaTestStatus.FAILURE],
|
|
441
439
|
STATUS_MAP[AntaTestStatus.ERROR],
|
|
442
440
|
]
|
|
@@ -449,7 +447,6 @@ class SummaryTotals(MDReportBase):
|
|
|
449
447
|
f"| {self.results.get_total_results()} "
|
|
450
448
|
f"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} "
|
|
451
449
|
f"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} "
|
|
452
|
-
f"| {self.results.get_total_results({AntaTestStatus.INCONCLUSIVE})} "
|
|
453
450
|
f"| {self.results.get_total_results({AntaTestStatus.FAILURE})} "
|
|
454
451
|
f"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\n"
|
|
455
452
|
)
|
|
@@ -470,7 +467,6 @@ class SummaryTotalsDeviceUnderTest(MDReportBase):
|
|
|
470
467
|
TOTAL_TESTS,
|
|
471
468
|
STATUS_MAP[AntaTestStatus.SUCCESS],
|
|
472
469
|
STATUS_MAP[AntaTestStatus.SKIPPED],
|
|
473
|
-
STATUS_MAP[AntaTestStatus.INCONCLUSIVE],
|
|
474
470
|
STATUS_MAP[AntaTestStatus.FAILURE],
|
|
475
471
|
STATUS_MAP[AntaTestStatus.ERROR],
|
|
476
472
|
CATEGORIES_SKIPPED,
|
|
@@ -482,19 +478,11 @@ class SummaryTotalsDeviceUnderTest(MDReportBase):
|
|
|
482
478
|
def generate_rows(self) -> Generator[str, None, None]:
|
|
483
479
|
"""Generate the rows of the summary totals device under test table."""
|
|
484
480
|
for device, stat in self.results.device_stats.items():
|
|
485
|
-
total_tests =
|
|
486
|
-
stat.tests_success_count
|
|
487
|
-
+ stat.tests_skipped_count
|
|
488
|
-
+ stat.tests_inconclusive_count
|
|
489
|
-
+ stat.tests_failure_count
|
|
490
|
-
+ stat.tests_error_count
|
|
491
|
-
+ stat.tests_unset_count
|
|
492
|
-
)
|
|
481
|
+
total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count + stat.tests_unset_count
|
|
493
482
|
categories_skipped = ", ".join(convert_categories(list(stat.categories_skipped), sort=True))
|
|
494
483
|
categories_failed = ", ".join(convert_categories(list(stat.categories_failed), sort=True))
|
|
495
484
|
yield (
|
|
496
|
-
f"| **{device}** | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.
|
|
497
|
-
f"| {stat.tests_failure_count} | {stat.tests_error_count} "
|
|
485
|
+
f"| **{device}** | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} "
|
|
498
486
|
f"| {categories_skipped or '-'} | {categories_failed or '-'} |\n"
|
|
499
487
|
)
|
|
500
488
|
|
|
@@ -514,7 +502,6 @@ class SummaryTotalsPerCategory(MDReportBase):
|
|
|
514
502
|
TOTAL_TESTS,
|
|
515
503
|
STATUS_MAP[AntaTestStatus.SUCCESS],
|
|
516
504
|
STATUS_MAP[AntaTestStatus.SKIPPED],
|
|
517
|
-
STATUS_MAP[AntaTestStatus.INCONCLUSIVE],
|
|
518
505
|
STATUS_MAP[AntaTestStatus.FAILURE],
|
|
519
506
|
STATUS_MAP[AntaTestStatus.ERROR],
|
|
520
507
|
]
|
|
@@ -525,17 +512,10 @@ class SummaryTotalsPerCategory(MDReportBase):
|
|
|
525
512
|
"""Generate the rows of the summary totals per category table."""
|
|
526
513
|
for category, stat in self.results.category_stats.items():
|
|
527
514
|
converted_category = convert_single_category_cached(category)
|
|
528
|
-
total_tests =
|
|
529
|
-
stat.tests_success_count
|
|
530
|
-
+ stat.tests_skipped_count
|
|
531
|
-
+ stat.tests_inconclusive_count
|
|
532
|
-
+ stat.tests_failure_count
|
|
533
|
-
+ stat.tests_error_count
|
|
534
|
-
+ stat.tests_unset_count
|
|
535
|
-
)
|
|
515
|
+
total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count + stat.tests_unset_count
|
|
536
516
|
yield (
|
|
537
|
-
f"| **{converted_category}** | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.
|
|
538
|
-
f"| {stat.
|
|
517
|
+
f"| **{converted_category}** | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} "
|
|
518
|
+
f"| {stat.tests_error_count} |\n"
|
|
539
519
|
)
|
|
540
520
|
|
|
541
521
|
def generate_section(self) -> None:
|
|
@@ -619,16 +599,8 @@ class TestResults(MDReportBase):
|
|
|
619
599
|
# Format the messages
|
|
620
600
|
if is_expanded:
|
|
621
601
|
total = len(result.atomic_results)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if failed > 0 and inconclusive > 0:
|
|
625
|
-
messages_str = f"{failed}/{total} checks failed; {inconclusive}/{total} checks inconclusive"
|
|
626
|
-
elif failed > 0:
|
|
627
|
-
messages_str = f"{failed}/{total} checks failed"
|
|
628
|
-
elif inconclusive > 0:
|
|
629
|
-
messages_str = f"{inconclusive}/{total} checks inconclusive"
|
|
630
|
-
else:
|
|
631
|
-
messages_str = f"All {total} checks passed"
|
|
602
|
+
failed = len([res for res in result.atomic_results if res.result != AntaTestStatus.SUCCESS])
|
|
603
|
+
messages_str = f"{failed}/{total} checks failed" if failed > 0 else f"All {total} checks passed"
|
|
632
604
|
else:
|
|
633
605
|
messages_str = self.safe_markdown("<br>".join(result.messages)) or "-"
|
|
634
606
|
|
|
@@ -43,7 +43,7 @@ class ReportTable:
|
|
|
43
43
|
device: str = "Summary per device"
|
|
44
44
|
|
|
45
45
|
@dataclass
|
|
46
|
-
class Columns:
|
|
46
|
+
class Columns:
|
|
47
47
|
"""Column names for the table report."""
|
|
48
48
|
|
|
49
49
|
device: str = "Device"
|
|
@@ -58,8 +58,6 @@ class ReportTable:
|
|
|
58
58
|
number_of_errors: str = "# of errors"
|
|
59
59
|
failed_devices: str = "List of devices with failed or errored tests"
|
|
60
60
|
failed_tests: str = "List of failed or errored tests"
|
|
61
|
-
# TODO(ANTA 2.0): Group this with the other number_of_* fields when positional compatibility can be broken.
|
|
62
|
-
number_of_inconclusive: str = "# of inconclusive"
|
|
63
61
|
|
|
64
62
|
def __init__(self) -> None:
|
|
65
63
|
"""Initialize a ReportTable instance."""
|
|
@@ -231,7 +229,6 @@ class ReportTable:
|
|
|
231
229
|
- `columns.test`
|
|
232
230
|
- `columns.number_of_success`
|
|
233
231
|
- `columns.number_of_skipped`
|
|
234
|
-
- `columns.number_of_inconclusive`
|
|
235
232
|
- `columns.number_of_failure`
|
|
236
233
|
- `columns.number_of_errors`
|
|
237
234
|
- `columns.failed_devices`
|
|
@@ -251,7 +248,6 @@ class ReportTable:
|
|
|
251
248
|
self.columns.test,
|
|
252
249
|
self.columns.number_of_success,
|
|
253
250
|
self.columns.number_of_skipped,
|
|
254
|
-
self.columns.number_of_inconclusive,
|
|
255
251
|
self.columns.number_of_failure,
|
|
256
252
|
self.columns.number_of_errors,
|
|
257
253
|
self.columns.failed_devices,
|
|
@@ -264,7 +260,6 @@ class ReportTable:
|
|
|
264
260
|
test,
|
|
265
261
|
str(stats.devices_success_count),
|
|
266
262
|
str(stats.devices_skipped_count),
|
|
267
|
-
str(stats.devices_inconclusive_count),
|
|
268
263
|
str(stats.devices_failure_count),
|
|
269
264
|
str(stats.devices_error_count),
|
|
270
265
|
", ".join(stats.devices_failure),
|
|
@@ -286,7 +281,6 @@ class ReportTable:
|
|
|
286
281
|
- `columns.device`
|
|
287
282
|
- `columns.number_of_success`
|
|
288
283
|
- `columns.number_of_skipped`
|
|
289
|
-
- `columns.number_of_inconclusive`
|
|
290
284
|
- `columns.number_of_failure`
|
|
291
285
|
- `columns.number_of_errors`
|
|
292
286
|
- `columns.failed_tests`
|
|
@@ -306,7 +300,6 @@ class ReportTable:
|
|
|
306
300
|
self.columns.device,
|
|
307
301
|
self.columns.number_of_success,
|
|
308
302
|
self.columns.number_of_skipped,
|
|
309
|
-
self.columns.number_of_inconclusive,
|
|
310
303
|
self.columns.number_of_failure,
|
|
311
304
|
self.columns.number_of_errors,
|
|
312
305
|
self.columns.failed_tests,
|
|
@@ -318,7 +311,6 @@ class ReportTable:
|
|
|
318
311
|
device,
|
|
319
312
|
str(stats.tests_success_count),
|
|
320
313
|
str(stats.tests_skipped_count),
|
|
321
|
-
str(stats.tests_inconclusive_count),
|
|
322
314
|
str(stats.tests_failure_count),
|
|
323
315
|
str(stats.tests_error_count),
|
|
324
316
|
", ".join(stats.tests_failure),
|
|
@@ -360,7 +352,7 @@ class ReportTable:
|
|
|
360
352
|
"""Create a table report with result aggregated per test.
|
|
361
353
|
|
|
362
354
|
Create table with full output:
|
|
363
|
-
Test Name | # of success | # of skipped | # of
|
|
355
|
+
Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes
|
|
364
356
|
|
|
365
357
|
Warnings
|
|
366
358
|
--------
|
|
@@ -392,7 +384,7 @@ class ReportTable:
|
|
|
392
384
|
) -> Table:
|
|
393
385
|
"""Create a table report with result aggregated per device.
|
|
394
386
|
|
|
395
|
-
Create table with full output: Device | # of success | # of skipped | # of
|
|
387
|
+
Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases
|
|
396
388
|
|
|
397
389
|
Warnings
|
|
398
390
|
--------
|
|
@@ -33,21 +33,17 @@ class ResultManager:
|
|
|
33
33
|
|
|
34
34
|
When adding a test with a status that is not `error`, the overall status is
|
|
35
35
|
updated according to the following precedence:
|
|
36
|
-
`failure` > `
|
|
37
|
-
|
|
38
|
-
| Current Status | Added test Status
|
|
39
|
-
| -------------- |
|
|
40
|
-
| unset | Any
|
|
41
|
-
| skipped | unset, skipped
|
|
42
|
-
| skipped | success
|
|
43
|
-
| skipped |
|
|
44
|
-
|
|
|
45
|
-
| success |
|
|
46
|
-
|
|
|
47
|
-
| success | failure | failure |
|
|
48
|
-
| inconclusive | unset, skipped, success, inconclusive | inconclusive |
|
|
49
|
-
| inconclusive | failure | failure |
|
|
50
|
-
| failure | Any status except error | failure |
|
|
36
|
+
`failure` > `success` > `skipped`/`unset`.
|
|
37
|
+
|
|
38
|
+
| Current Status | Added test Status | Updated Status |
|
|
39
|
+
| -------------- | -------------------------------- | -------------- |
|
|
40
|
+
| unset | Any | Any |
|
|
41
|
+
| skipped | unset, skipped | skipped |
|
|
42
|
+
| skipped | success | success |
|
|
43
|
+
| skipped | failure | failure |
|
|
44
|
+
| success | unset, skipped, success | success |
|
|
45
|
+
| success | failure | failure |
|
|
46
|
+
| failure | Any status except error | failure |
|
|
51
47
|
|
|
52
48
|
If the status of the added test is error, the status is untouched and the
|
|
53
49
|
`error_status` attribute is set to True.
|