Skip to content
4 changes: 3 additions & 1 deletion anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ def validate_regex(value: str) -> str:
SnmpErrorCounter = Literal[
"inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs"
]

IPv4RouteType = Literal[
"connected",
"static",
Expand Down Expand Up @@ -238,3 +237,6 @@ def validate_regex(value: str) -> str:
"Route Cache Route",
"CBF Leaked Route",
]
SnmpVersion = Literal["v1", "v2c", "v3"]
SnmpHashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
SnmpEncryptionAlgorithm = Literal["AES-128", "AES-192", "AES-256", "DES"]
35 changes: 35 additions & 0 deletions anta/input_models/snmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) 2023-2025 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""Module containing input models for SNMP tests."""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict

from anta.custom_types import SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion


class SnmpUser(BaseModel):
"""Model for a SNMP User."""

model_config = ConfigDict(extra="forbid")
username: str
"""SNMP user name."""
group_name: str
"""SNMP group for the user."""
version: SnmpVersion
"""SNMP protocol version."""
auth_type: SnmpHashingAlgorithm | None = None
"""User authentication algorithm. Can be provided in the `VerifySnmpUser` test."""
priv_type: SnmpEncryptionAlgorithm | None = None
"""User privacy algorithm. Can be provided in the `VerifySnmpUser` test."""

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

Examples
--------
- User: Test Group: Test_Group Version: v2c
"""
return f"User: {self.username} Group: {self.group_name} Version: {self.version}"
84 changes: 83 additions & 1 deletion anta/tests/snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
# mypy: disable-error-code=attr-defined
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar, get_args
from typing import TYPE_CHECKING, ClassVar, TypeVar, get_args

from pydantic import field_validator

from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu
from anta.input_models.snmp import SnmpUser
from anta.models import AntaCommand, AntaTest
from anta.tools import get_value

if TYPE_CHECKING:
from anta.models import AntaTemplate

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


class VerifySnmpStatus(AntaTest):
"""Verifies whether the SNMP agent is enabled in a specified VRF.
Expand Down Expand Up @@ -339,3 +345,79 @@ def test(self) -> None:
self.result.is_success()
else:
self.result.is_failure(f"The following SNMP error counters are not found or have non-zero error counters:\n{error_counters_not_ok}")


class VerifySnmpUser(AntaTest):
"""Verifies the SNMP user configurations.

This test performs the following checks for each specified user:

1. User exists in SNMP configuration.
2. Group assignment is correct.
3. For SNMPv3 users only:
- Authentication type matches (if specified)
- Privacy type matches (if specified)

Expected Results
----------------
* Success: If all of the following conditions are met:
- All users exist with correct group assignments.
- SNMPv3 authentication and privacy types match specified values.
* Failure: If any of the following occur:
- User not found in SNMP configuration.
- Incorrect group assignment.
- For SNMPv3: Mismatched authentication or privacy types.

Examples
--------
```yaml
anta.tests.snmp:
- VerifySnmpUser:
snmp_users:
- username: test
group_name: test_group
version: v3
auth_type: MD5
priv_type: AES-128
```
"""

categories: ClassVar[list[str]] = ["snmp"]
commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp user", revision=1)]

class Input(AntaTest.Input):
"""Input model for the VerifySnmpUser test."""

snmp_users: list[SnmpUser]
"""List of SNMP users."""

@field_validator("snmp_users")
@classmethod
def validate_snmp_users(cls, snmp_users: list[T]) -> list[T]:
"""Validate that 'auth_type' or 'priv_type' field is provided in each SNMPv3 user."""
for user in snmp_users:
if user.version == "v3" and not (user.auth_type or user.priv_type):
msg = f"{user} 'auth_type' or 'priv_type' field is required with 'version: v3'"
raise ValueError(msg)
return snmp_users

@AntaTest.anta_test
def test(self) -> None:
"""Main test function for VerifySnmpUser."""
self.result.is_success()

for user in self.inputs.snmp_users:
# Verify SNMP user details.
if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{user.version}.users.{user.username}")):
self.result.is_failure(f"{user} - Not found")
continue

if user.group_name != (act_group := user_details.get("groupName", "Not Found")):
self.result.is_failure(f"{user} - Incorrect user group - Actual: {act_group}")

if user.version == "v3":
if user.auth_type and (act_auth_type := get_value(user_details, "v3Params.authType", "Not Found")) != user.auth_type:
self.result.is_failure(f"{user} - Incorrect authentication type - Expected: {user.auth_type} Actual: {act_auth_type}")

if user.priv_type and (act_encryption := get_value(user_details, "v3Params.privType", "Not Found")) != user.priv_type:
self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {user.priv_type} Actual: {act_encryption}")
16 changes: 16 additions & 0 deletions docs/api/tests.snmp.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ anta_title: ANTA catalog for SNMP tests
~ that can be found in the LICENSE file.
-->

# Tests

::: anta.tests.snmp

options:
show_root_heading: false
show_root_toc_entry: false
Expand All @@ -18,3 +21,16 @@ anta_title: ANTA catalog for SNMP tests
filters:
- "!test"
- "!render"

# Input models

::: anta.input_models.snmp

options:
show_root_heading: false
show_root_toc_entry: false
show_bases: false
merge_init_into_class: false
anta_hide_test_module_description: true
show_labels: true
filters: ["!^__str__"]
8 changes: 8 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,14 @@ anta.tests.snmp:
- VerifySnmpStatus:
# Verifies if the SNMP agent is enabled.
vrf: default
- VerifySnmpUser:
# Verifies the SNMP user configurations.
snmp_users:
- username: test
group_name: test_group
version: v3
auth_type: MD5
priv_type: AES-128
anta.tests.software:
- VerifyEOSExtensions:
# Verifies that all EOS extensions installed on the device are enabled for boot persistence.
Expand Down
165 changes: 165 additions & 0 deletions tests/units/anta_tests/test_snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
VerifySnmpLocation,
VerifySnmpPDUCounters,
VerifySnmpStatus,
VerifySnmpUser,
)
from tests.units.anta_tests import test

Expand Down Expand Up @@ -319,4 +320,168 @@
],
},
},
{
"name": "success",
"test": VerifySnmpUser,
"eos_data": [
{
"usersByVersion": {
"v1": {
"users": {
"Test1": {
"groupName": "TestGroup1",
},
}
},
"v2c": {
"users": {
"Test2": {
"groupName": "TestGroup2",
},
}
},
"v3": {
"users": {
"Test3": {
"groupName": "TestGroup3",
"v3Params": {"authType": "SHA-384", "privType": "AES-128"},
},
"Test4": {"groupName": "TestGroup3", "v3Params": {"authType": "SHA-512", "privType": "AES-192"}},
}
},
}
}
],
"inputs": {
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "version": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "version": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"},
]
},
"expected": {"result": "success"},
},
{
"name": "failure-not-configured",
"test": VerifySnmpUser,
"eos_data": [
{
"usersByVersion": {
"v3": {
"users": {
"Test3": {
"groupName": "TestGroup3",
"v3Params": {"authType": "SHA-384", "privType": "AES-128"},
},
}
},
}
}
],
"inputs": {
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "version": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "version": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"},
]
},
"expected": {
"result": "failure",
"messages": [
"User: Test1 Group: TestGroup1 Version: v1 - Not found",
"User: Test2 Group: TestGroup2 Version: v2c - Not found",
"User: Test4 Group: TestGroup3 Version: v3 - Not found",
],
},
},
{
"name": "failure-incorrect-group",
"test": VerifySnmpUser,
"eos_data": [
{
"usersByVersion": {
"v1": {
"users": {
"Test1": {
"groupName": "TestGroup2",
},
}
},
"v2c": {
"users": {
"Test2": {
"groupName": "TestGroup1",
},
}
},
"v3": {},
}
}
],
"inputs": {
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "version": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "version": "v2c"},
]
},
"expected": {
"result": "failure",
"messages": [
"User: Test1 Group: TestGroup1 Version: v1 - Incorrect user group - Actual: TestGroup2",
"User: Test2 Group: TestGroup2 Version: v2c - Incorrect user group - Actual: TestGroup1",
],
},
},
{
"name": "failure-incorrect-auth-encryption",
"test": VerifySnmpUser,
"eos_data": [
{
"usersByVersion": {
"v1": {
"users": {
"Test1": {
"groupName": "TestGroup1",
},
}
},
"v2c": {
"users": {
"Test2": {
"groupName": "TestGroup2",
},
}
},
"v3": {
"users": {
"Test3": {
"groupName": "TestGroup3",
"v3Params": {"authType": "SHA-512", "privType": "AES-192"},
},
"Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}},
}
},
}
}
],
"inputs": {
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "version": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "version": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "version": "v3", "auth_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup4", "version": "v3", "auth_type": "SHA-512", "priv_type": "AES-192"},
]
},
"expected": {
"result": "failure",
"messages": [
"User: Test3 Group: TestGroup3 Version: v3 - Incorrect authentication type - Expected: SHA-384 Actual: SHA-512",
"User: Test3 Group: TestGroup3 Version: v3 - Incorrect privacy type - Expected: AES-128 Actual: AES-192",
"User: Test4 Group: TestGroup4 Version: v3 - Incorrect authentication type - Expected: SHA-512 Actual: SHA-384",
"User: Test4 Group: TestGroup4 Version: v3 - Incorrect privacy type - Expected: AES-192 Actual: AES-128",
],
},
},
]
Loading
Loading