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"]
HashingAlgorithm = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
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."""
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that group_name and version are required fields, you can update this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Thanks!!

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
```
"""
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved

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]:
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
"""Validate that 'authentication_type' or 'priv_type' field is provided in each SNMP user."""
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
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)
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
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'"
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved
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
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved

# 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