Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anta): Added the test case to verify SNMP user #877

Merged
merged 11 commits into from
Jan 14, 2025
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