Skip to content

Commit 6ceb261

Browse files
Merge branch 'main' into issue_853
2 parents 7886dbd + 3f76153 commit 6ceb261

File tree

13 files changed

+219
-64
lines changed

13 files changed

+219
-64
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ repos:
4646
- '<!--| ~| -->'
4747

4848
- repo: https://github.com/astral-sh/ruff-pre-commit
49-
rev: v0.9.1
49+
rev: v0.9.2
5050
hooks:
5151
- id: ruff
5252
name: Run Ruff linter

anta/custom_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,4 @@ def validate_regex(value: str) -> str:
263263
SnmpHashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
264264
SnmpEncryptionAlgorithm = Literal["AES-128", "AES-192", "AES-256", "DES"]
265265
DynamicVlanSource = Literal["dmf", "dot1x", "dynvtep", "evpn", "mlag", "mlagsync", "mvpn", "swfwd", "vccbfd"]
266+
LogSeverityLevel = Literal["alerts", "critical", "debugging", "emergencies", "errors", "informational", "notifications", "warnings"]

anta/input_models/routing/bgp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ class BgpRoute(BaseModel):
224224
"""The IPv4 network address."""
225225
vrf: str = "default"
226226
"""Optional VRF for the BGP peer. Defaults to `default`."""
227-
paths: list[BgpRoutePath] | None = None
228-
"""A list of paths for the BGP route. Required field in the `VerifyBGPRouteOrigin` test."""
227+
paths: list[BgpRoutePath]
228+
"""A list of paths for the BGP route."""
229229

230230
def __str__(self) -> str:
231231
"""Return a human-readable string representation of the BgpRoute for reporting.

anta/input_models/routing/generic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ class IPv4Routes(BaseModel):
1717

1818
model_config = ConfigDict(extra="forbid")
1919
prefix: IPv4Network
20-
"""The IPV4 network to validate the route type."""
20+
"""IPv4 prefix in CIDR notation."""
2121
vrf: str = "default"
2222
"""VRF context. Defaults to `default` VRF."""
23-
route_type: IPv4RouteType
24-
"""List of IPV4 Route type to validate the valid rout type."""
23+
route_type: IPv4RouteType | None = None
24+
"""Expected route type. Required field in the `VerifyIPv4RouteType` test."""
2525

2626
def __str__(self) -> str:
2727
"""Return a human-readable string representation of the IPv4RouteType for reporting."""

anta/reporter/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
class ReportTable:
2929
"""TableReport Generate a Table based on TestResult."""
3030

31-
@dataclass()
31+
@dataclass
3232
class Headers: # pylint: disable=too-many-instance-attributes
3333
"""Headers for the table report."""
3434

anta/tests/interfaces.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
import re
1111
from ipaddress import IPv4Interface
12-
from typing import Any, ClassVar
12+
from typing import Any, ClassVar, TypeVar
1313

14-
from pydantic import BaseModel, Field
14+
from pydantic import BaseModel, Field, field_validator
1515
from pydantic_extra_types.mac_address import MacAddress
1616

1717
from anta import GITHUB_SUGGESTION
@@ -23,6 +23,9 @@
2323

2424
BPS_GBPS_CONVERSIONS = 1000000000
2525

26+
# Using a TypeVar for the InterfaceState model since mypy thinks it's a ClassVar and not a valid type when used in field validators
27+
T = TypeVar("T", bound=InterfaceState)
28+
2629

2730
class VerifyInterfaceUtilization(AntaTest):
2831
"""Verifies that the utilization of interfaces is below a certain threshold.
@@ -226,6 +229,16 @@ class Input(AntaTest.Input):
226229
"""List of interfaces with their expected state."""
227230
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
228231

232+
@field_validator("interfaces")
233+
@classmethod
234+
def validate_interfaces(cls, interfaces: list[T]) -> list[T]:
235+
"""Validate that 'status' field is provided in each interface."""
236+
for interface in interfaces:
237+
if interface.status is None:
238+
msg = f"{interface} 'status' field missing in the input"
239+
raise ValueError(msg)
240+
return interfaces
241+
229242
@AntaTest.anta_test
230243
def test(self) -> None:
231244
"""Main test function for VerifyInterfacesStatus."""
@@ -891,6 +904,16 @@ class Input(AntaTest.Input):
891904
"""List of interfaces with their expected state."""
892905
InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState
893906

907+
@field_validator("interfaces")
908+
@classmethod
909+
def validate_interfaces(cls, interfaces: list[T]) -> list[T]:
910+
"""Validate that 'portchannel' field is provided in each interface."""
911+
for interface in interfaces:
912+
if interface.portchannel is None:
913+
msg = f"{interface} 'portchannel' field missing in the input"
914+
raise ValueError(msg)
915+
return interfaces
916+
894917
@AntaTest.anta_test
895918
def test(self) -> None:
896919
"""Main test function for VerifyLACPInterfacesStatus."""

anta/tests/logging.py

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414
from ipaddress import IPv4Address
1515
from typing import TYPE_CHECKING, ClassVar
1616

17-
from anta.models import AntaCommand, AntaTest
17+
from anta.custom_types import LogSeverityLevel
18+
from anta.models import AntaCommand, AntaTemplate, AntaTest
1819

1920
if TYPE_CHECKING:
2021
import logging
2122

22-
from anta.models import AntaTemplate
23-
2423

2524
def _get_logging_states(logger: logging.Logger, command_output: str) -> str:
2625
"""Parse `show logging` output and gets operational logging states used in the tests in this module.
@@ -201,35 +200,43 @@ class VerifyLoggingLogsGeneration(AntaTest):
201200
202201
This test performs the following checks:
203202
204-
1. Sends a test log message at the **informational** level
205-
2. Retrieves the most recent logs (last 30 seconds)
206-
3. Verifies that the test message was successfully logged
207-
208-
!!! warning
209-
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
203+
1. Sends a test log message at the specified severity log level.
204+
2. Retrieves the most recent logs (last 30 seconds).
205+
3. Verifies that the test message was successfully logged.
210206
211207
Expected Results
212208
----------------
213209
* Success: If logs are being generated and the test message is found in recent logs.
214210
* Failure: If any of the following occur:
215-
- The test message is not found in recent logs
216-
- The logging system is not capturing new messages
217-
- No logs are being generated
211+
- The test message is not found in recent logs.
212+
- The logging system is not capturing new messages.
213+
- No logs are being generated.
218214
219215
Examples
220216
--------
221217
```yaml
222218
anta.tests.logging:
223219
- VerifyLoggingLogsGeneration:
220+
severity_level: informational
224221
```
225222
"""
226223

227224
categories: ClassVar[list[str]] = ["logging"]
228225
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
229-
AntaCommand(command="send log level informational message ANTA VerifyLoggingLogsGeneration validation", ofmt="text"),
230-
AntaCommand(command="show logging informational last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
226+
AntaTemplate(template="send log level {severity_level} message ANTA VerifyLoggingLogsGeneration validation", ofmt="text"),
227+
AntaTemplate(template="show logging {severity_level} last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
231228
]
232229

230+
class Input(AntaTest.Input):
231+
"""Input model for the VerifyLoggingLogsGeneration test."""
232+
233+
severity_level: LogSeverityLevel = "informational"
234+
"""Log severity level. Defaults to informational."""
235+
236+
def render(self, template: AntaTemplate) -> list[AntaCommand]:
237+
"""Render the template for log severity level in the input."""
238+
return [template.render(severity_level=self.inputs.severity_level)]
239+
233240
@AntaTest.anta_test
234241
def test(self) -> None:
235242
"""Main test function for VerifyLoggingLogsGeneration."""
@@ -248,37 +255,45 @@ class VerifyLoggingHostname(AntaTest):
248255
249256
This test performs the following checks:
250257
251-
1. Retrieves the device's configured FQDN
252-
2. Sends a test log message at the **informational** level
253-
3. Retrieves the most recent logs (last 30 seconds)
254-
4. Verifies that the test message includes the complete FQDN of the device
255-
256-
!!! warning
257-
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
258+
1. Retrieves the device's configured FQDN.
259+
2. Sends a test log message at the specified severity log level.
260+
3. Retrieves the most recent logs (last 30 seconds).
261+
4. Verifies that the test message includes the complete FQDN of the device.
258262
259263
Expected Results
260264
----------------
261265
* Success: If logs are generated with the device's complete FQDN.
262266
* Failure: If any of the following occur:
263-
- The test message is not found in recent logs
264-
- The log message does not include the device's FQDN
265-
- The FQDN in the log message doesn't match the configured FQDN
267+
- The test message is not found in recent logs.
268+
- The log message does not include the device's FQDN.
269+
- The FQDN in the log message doesn't match the configured FQDN.
266270
267271
Examples
268272
--------
269273
```yaml
270274
anta.tests.logging:
271275
- VerifyLoggingHostname:
276+
severity_level: informational
272277
```
273278
"""
274279

275280
categories: ClassVar[list[str]] = ["logging"]
276281
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
277282
AntaCommand(command="show hostname", revision=1),
278-
AntaCommand(command="send log level informational message ANTA VerifyLoggingHostname validation", ofmt="text"),
279-
AntaCommand(command="show logging informational last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
283+
AntaTemplate(template="send log level {severity_level} message ANTA VerifyLoggingHostname validation", ofmt="text"),
284+
AntaTemplate(template="show logging {severity_level} last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
280285
]
281286

287+
class Input(AntaTest.Input):
288+
"""Input model for the VerifyLoggingHostname test."""
289+
290+
severity_level: LogSeverityLevel = "informational"
291+
"""Log severity level. Defaults to informational."""
292+
293+
def render(self, template: AntaTemplate) -> list[AntaCommand]:
294+
"""Render the template for log severity level in the input."""
295+
return [template.render(severity_level=self.inputs.severity_level)]
296+
282297
@AntaTest.anta_test
283298
def test(self) -> None:
284299
"""Main test function for VerifyLoggingHostname."""
@@ -303,37 +318,45 @@ class VerifyLoggingTimestamp(AntaTest):
303318
304319
This test performs the following checks:
305320
306-
1. Sends a test log message at the **informational** level
307-
2. Retrieves the most recent logs (last 30 seconds)
308-
3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format
309-
- Example format: `2024-01-25T15:30:45.123456+00:00`
310-
- Includes microsecond precision
311-
- Contains timezone offset
312-
313-
!!! warning
314-
EOS logging buffer should be set to severity level `informational` or higher for this test to work.
321+
1. Sends a test log message at the specified severity log level.
322+
2. Retrieves the most recent logs (last 30 seconds).
323+
3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format.
324+
- Example format: `2024-01-25T15:30:45.123456+00:00`.
325+
- Includes microsecond precision.
326+
- Contains timezone offset.
315327
316328
Expected Results
317329
----------------
318330
* Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.
319331
* Failure: If any of the following occur:
320-
- The test message is not found in recent logs
321-
- The timestamp format does not match the expected RFC3339 format
332+
- The test message is not found in recent logs.
333+
- The timestamp format does not match the expected RFC3339 format.
322334
323335
Examples
324336
--------
325337
```yaml
326338
anta.tests.logging:
327339
- VerifyLoggingTimestamp:
340+
severity_level: informational
328341
```
329342
"""
330343

331344
categories: ClassVar[list[str]] = ["logging"]
332345
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [
333-
AntaCommand(command="send log level informational message ANTA VerifyLoggingTimestamp validation", ofmt="text"),
334-
AntaCommand(command="show logging informational last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
346+
AntaTemplate(template="send log level {severity_level} message ANTA VerifyLoggingTimestamp validation", ofmt="text"),
347+
AntaTemplate(template="show logging {severity_level} last 30 seconds | grep ANTA", ofmt="text", use_cache=False),
335348
]
336349

350+
class Input(AntaTest.Input):
351+
"""Input model for the VerifyLoggingTimestamp test."""
352+
353+
severity_level: LogSeverityLevel = "informational"
354+
"""Log severity level. Defaults to informational."""
355+
356+
def render(self, template: AntaTemplate) -> list[AntaCommand]:
357+
"""Render the template for log severity level in the input."""
358+
return [template.render(severity_level=self.inputs.severity_level)]
359+
337360
@AntaTest.anta_test
338361
def test(self) -> None:
339362
"""Main test function for VerifyLoggingTimestamp."""

anta/tests/routing/generic.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ipaddress import IPv4Address, IPv4Interface
1212
from typing import TYPE_CHECKING, ClassVar, Literal
1313

14-
from pydantic import model_validator
14+
from pydantic import field_validator, model_validator
1515

1616
from anta.custom_types import PositiveInteger
1717
from anta.input_models.routing.generic import IPv4Routes
@@ -189,9 +189,10 @@ class VerifyIPv4RouteType(AntaTest):
189189
"""Verifies the route-type of the IPv4 prefixes.
190190
191191
This test performs the following checks for each IPv4 route:
192-
1. Verifies that the specified VRF is configured.
193-
2. Verifies that the specified IPv4 route is exists in the configuration.
194-
3. Verifies that the the specified IPv4 route is of the expected type.
192+
193+
1. Verifies that the specified VRF is configured.
194+
2. Verifies that the specified IPv4 route is exists in the configuration.
195+
3. Verifies that the the specified IPv4 route is of the expected type.
195196
196197
Expected Results
197198
----------------
@@ -230,6 +231,17 @@ class Input(AntaTest.Input):
230231
"""Input model for the VerifyIPv4RouteType test."""
231232

232233
routes_entries: list[IPv4Routes]
234+
"""List of IPv4 route(s)."""
235+
236+
@field_validator("routes_entries")
237+
@classmethod
238+
def validate_routes_entries(cls, routes_entries: list[IPv4Routes]) -> list[IPv4Routes]:
239+
"""Validate that 'route_type' field is provided in each BGP route entry."""
240+
for entry in routes_entries:
241+
if entry.route_type is None:
242+
msg = f"{entry} 'route_type' field missing in the input"
243+
raise ValueError(msg)
244+
return routes_entries
233245

234246
@AntaTest.anta_test
235247
def test(self) -> None:

docs/templates/python/material/class.html.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
{{ super() }}
6060

6161
{% for dec in class.decorators %}
62-
{% if dec.value.function.name == "deprecated_test_class" %}
62+
{% if dec.value.function is defined and dec.value.function.name == "deprecated_test_class" %}
6363
<img alt="Static Badge" src="https://img.shields.io/badge/DEPRECATED-yellow?style=flat&logoSize=auto">
6464
{% for arg in dec.value.arguments | selectattr("name", "equalto", "removal_in_version") | list %}
6565
<img alt="Static Badge" src="https://img.shields.io/badge/REMOVAL-{{ arg.value[1:-1] }}-grey?style=flat&logoSize=auto&labelColor=red">

examples/tests.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ anta.tests.logging:
291291
# Verifies there are no syslog messages with a severity of ERRORS or higher.
292292
- VerifyLoggingHostname:
293293
# Verifies if logs are generated with the device FQDN.
294+
severity_level: informational
294295
- VerifyLoggingHosts:
295296
# Verifies logging hosts (syslog servers) for a specified VRF.
296297
hosts:
@@ -299,6 +300,7 @@ anta.tests.logging:
299300
vrf: default
300301
- VerifyLoggingLogsGeneration:
301302
# Verifies if logs are generated.
303+
severity_level: informational
302304
- VerifyLoggingPersistent:
303305
# Verifies if logging persistent is enabled and logs are saved in flash.
304306
- VerifyLoggingSourceIntf:
@@ -307,6 +309,7 @@ anta.tests.logging:
307309
vrf: default
308310
- VerifyLoggingTimestamp:
309311
# Verifies if logs are generated with the appropriate timestamp.
312+
severity_level: informational
310313
- VerifySyslogLogging:
311314
# Verifies if syslog logging is enabled.
312315
anta.tests.mlag:

0 commit comments

Comments
 (0)