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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
HashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
SnmpEncryptionAlgorithm = Literal["AES-128", "AES-192", "AES-256", "DES"]
42 changes: 42 additions & 0 deletions anta/input_models/snmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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 HashingAlgorithm, SnmpEncryptionAlgorithm, SnmpVersion


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

model_config = ConfigDict(extra="forbid")
username: str
"""SNMP user name."""
group_name: str | None = None
"""SNMP group for the user. Required field in the `VerifySnmpUser` test."""
version: SnmpVersion | None = None
"""SNMP protocol version. Required field in the `VerifySnmpUser` test."""
auth_type: HashingAlgorithm | 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
- User: Test Group: Test_Group
- User: Test Group: Test_Group Version: v2c
"""
base_string = f"User: {self.username}"
if self.group_name:
base_string += f" Group: {self.group_name}"
if self.version:
base_string += f" Version: {self.version}"
return base_string
90 changes: 89 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,85 @@ 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 for specified version(s).

This test performs the following checks for each specified user:

1. Verifies that the user name and group name are valid.
2. Ensures that the SNMP v3 security model, user authentication, and privacy settings align with version-specific requirements.

Expected Results
----------------
* Success: If all of the following conditions are met:
- All specified users are found in the SNMP configuration with valid user groups.
- The SNMP v3 security model, user authentication and privacy settings match the required settings.
* Failure: If any of the following occur:
- A specified user is not found in the SNMP configuration.
- A user's group is incorrect.
- For SNMP v3 security model, the user authentication and privacy settings do not matches the required settings.

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_user(cls, snmp_users: list[T]) -> list[T]:
"""Validate that 'authentication_type' or 'priv_type' field is provided in each SNMP user."""
for user in snmp_users:
if user.group_name is None or user.version is None:
msg = f"{user}; 'group_name' or 'version' field missing in the input"
raise ValueError(msg)
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:
username = user.username
group_name = user.group_name
version = user.version
auth_type = user.auth_type
priv_type = user.priv_type

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

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

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

if priv_type and (act_encryption := get_value(user_details, "v3Params.privType", "Not Found")) != priv_type:
self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {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 @@ -744,6 +744,14 @@ anta.tests.snmp:
- VerifySnmpStatus:
# Verifies if the SNMP agent is enabled.
vrf: default
- VerifySnmpUser:
# Verifies the SNMP user configurations for specified version(s).
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