Skip to content

Commit

Permalink
Updated input model refactoring changes
Browse files Browse the repository at this point in the history
  • Loading branch information
vitthalmagadum committed Jan 8, 2025
1 parent 4f4c60e commit 581acc2
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 89 deletions.
10 changes: 3 additions & 7 deletions anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,6 @@ def validate_regex(value: str) -> str:
SnmpErrorCounter = Literal[
"inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs"
]
<<<<<<< HEAD
SnmpVersion = Literal["v1", "v2c", "v3"]
HashingAlgorithms = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
EncryptionAlgorithms = Literal["AES-128", "AES-192", "AES-256", "DES"]
=======

IPv4RouteType = Literal[
"connected",
"static",
Expand Down Expand Up @@ -243,4 +237,6 @@ def validate_regex(value: str) -> str:
"Route Cache Route",
"CBF Leaked Route",
]
>>>>>>> main
SnmpVersion = Literal["v1", "v2c", "v3"]
HashingAlgorithms = Literal["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]
EncryptionAlgorithms = 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 EncryptionAlgorithms, HashingAlgorithms, 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."""
security_model: SnmpVersion | None = None
"""SNMP protocol version. Required field in the `VerifySnmpUser` test."""
authentication_type: HashingAlgorithms | None = None
"""User authentication settings. Can be provided in the `VerifySnmpUser` test."""
encryption: EncryptionAlgorithms | None = None
"""User privacy settings. 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} Version: {self.security_model}"
99 changes: 41 additions & 58 deletions anta/tests/snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +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 BaseModel, model_validator
from pydantic import field_validator

from anta.custom_types import EncryptionAlgorithms, HashingAlgorithms, PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion
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_failed_logs, get_value
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 @@ -346,98 +350,77 @@ def test(self) -> None:
class VerifySnmpUser(AntaTest):
"""Verifies the SNMP user configurations for specified version(s).
- Verifies that the valid user name and group name.
- Ensures that the SNMP v3 security model, the user authentication and privacy settings aligning with version-specific requirements.
This test performs the following checks for each specified address family:
1. Verifies that the valid user name and group name.
2. Ensures that the SNMP v3 security model, the user authentication and privacy settings aligning with version-specific requirements.
Expected Results
----------------
* Success: The test will pass if the provided SNMP user and all specified parameters are correctly configured.
* Failure: The test will fail if the provided SNMP user is not configured or specified parameters are not correctly configured.
* Success: If all of the following conditions are met:
- All specified users are found in the SNMP configuration with valid user group.
- The SNMP v3 security model, the user authentication and privacy settings matches 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 not correct.
- For SNMP v3 security model, the user authentication and privacy settings does not matches the required settings.
Examples
--------
```yaml
anta.tests.snmp:
- VerifySnmpUser:
users:
snmp_users:
- username: test
group_name: test_group
security_model: v3
authentication_type: MD5
priv_type: AES-128
encryption: AES-128
```
"""

name = "VerifySnmpUser"
description = "Verifies the SNMP user configurations for specified version(s)."
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."""

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

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

username: str
"""SNMP user name."""
group_name: str
"""SNMP group for the user."""
security_model: SnmpVersion
"""SNMP protocol version.."""
authentication_type: HashingAlgorithms | None = None
"""User authentication settings."""
priv_type: EncryptionAlgorithms | None = None
"""User privacy settings."""

@model_validator(mode="after")
def validate_inputs(self: BaseModel) -> BaseModel:
"""Validate the inputs provided to the SnmpUser class."""
if self.security_model in ["v1", "v2c"] and (self.authentication_type or self.priv_type) is not None:
msg = "SNMP versions 1 and 2c, do not support encryption or advanced authentication."
@field_validator("snmp_users")
@classmethod
def validate_snmp_user(cls, snmp_users: list[T]) -> list[T]:
"""Validate that 'authentication_type' or 'encryption' field is provided in each SNMP user."""
for user in snmp_users:
if user.security_model == "v3" and not (user.authentication_type or user.encryption):
msg = f"{user}; At least one of 'authentication_type' or 'encryption' must be provided."
raise ValueError(msg)
return self
return snmp_users

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

for user in self.inputs.users:
for user in self.inputs.snmp_users:
username = user.username
group_name = user.group_name
security_model = user.security_model
authentication_type = user.authentication_type
priv_type = user.priv_type
encryption = user.encryption

# Verify SNMP host details.
# Verify SNMP user details.
if not (user_details := get_value(self.instance_commands[0].json_output, f"usersByVersion.{security_model}.users.{username}")):
failures += f"SNMP user '{username}' is not configured with security model '{security_model}'.\n"
self.result.is_failure(f"{user} - Not found")
continue

# Update expected host details.
expected_user_details = {"user group": group_name}

# Update actual host details.
actual_user_details = {"user group": user_details.get("groupName", "Not Found")}

if authentication_type:
expected_user_details["authentication type"] = authentication_type
actual_user_details["authentication type"] = user_details.get("v3Params", {}).get("authType", "Not Found")

if priv_type:
expected_user_details["privacy type"] = priv_type
actual_user_details["privacy type"] = user_details.get("v3Params", {}).get("privType", "Not Found")
if group_name != (act_group := user_details.get("groupName", "Not Found")):
self.result.is_failure(f"{user} - Incorrect user group - Expected: {group_name} Actual: {act_group}")

# Collecting failures logs if any.
failure_logs = get_failed_logs(expected_user_details, actual_user_details)
if failure_logs:
failures += f"For SNMP user {username}:{failure_logs}\n"
if security_model == "v3":
if authentication_type and (act_auth_type := user_details.get("v3Params", {}).get("authType", "Not Found")) != authentication_type:
self.result.is_failure(f"{user} - Incorrect authentication type - Expected: {authentication_type} Actual: {act_auth_type}")

# Check if there are any failures.
if failures:
self.result.is_failure(failures)
if encryption and (act_encryption := user_details.get("v3Params", {}).get("privType", "Not Found")) != encryption:
self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {encryption} Actual: {act_encryption}")
8 changes: 8 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,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
security_model: v3
authentication_type: MD5
encryption: AES-128
anta.tests.software:
- VerifyEOSExtensions:
# Verifies that all EOS extensions installed on the device are enabled for boot persistence.
Expand Down
80 changes: 56 additions & 24 deletions tests/units/anta_tests/test_snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,11 @@
}
],
"inputs": {
"users": [
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"},
]
},
"expected": {"result": "success"},
Expand All @@ -380,24 +380,24 @@
}
],
"inputs": {
"users": [
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"},
]
},
"expected": {
"result": "failure",
"messages": [
"SNMP user 'Test1' is not configured with security model 'v1'.\n"
"SNMP user 'Test2' is not configured with security model 'v2c'.\n"
"SNMP user 'Test4' is not configured with security model 'v3'."
"User: Test1 Version: v1 - Not found",
"User: Test2 Version: v2c - Not found",
"User: Test4 Version: v3 - Not found",
],
},
},
{
"name": "failure-incorrect-configure",
"name": "failure-incorrect-group",
"test": VerifySnmpUser,
"eos_data": [
{
Expand All @@ -416,10 +416,48 @@
},
}
},
"v3": {},
}
}
],
"inputs": {
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"},
]
},
"expected": {
"result": "failure",
"messages": [
"User: Test1 Version: v1 - Incorrect user group - Expected: TestGroup1 Actual: TestGroup2",
"User: Test2 Version: v2c - Incorrect user group - Expected: TestGroup2 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": "TestGroup4",
"groupName": "TestGroup3",
"v3Params": {"authType": "SHA-512", "privType": "AES-192"},
},
"Test4": {"groupName": "TestGroup4", "v3Params": {"authType": "SHA-384", "privType": "AES-128"}},
Expand All @@ -429,26 +467,20 @@
}
],
"inputs": {
"users": [
"snmp_users": [
{"username": "Test1", "group_name": "TestGroup1", "security_model": "v1"},
{"username": "Test2", "group_name": "TestGroup2", "security_model": "v2c"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "priv_type": "AES-128"},
{"username": "Test4", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-512", "priv_type": "AES-192"},
{"username": "Test3", "group_name": "TestGroup3", "security_model": "v3", "authentication_type": "SHA-384", "encryption": "AES-128"},
{"username": "Test4", "group_name": "TestGroup4", "security_model": "v3", "authentication_type": "SHA-512", "encryption": "AES-192"},
]
},
"expected": {
"result": "failure",
"messages": [
"For SNMP user Test1:\nExpected `TestGroup1` as the user group, but found `TestGroup2` instead.\n"
"For SNMP user Test2:\nExpected `TestGroup2` as the user group, but found `TestGroup1` instead.\n"
"For SNMP user Test3:\n"
"Expected `TestGroup3` as the user group, but found `TestGroup4` instead.\n"
"Expected `SHA-384` as the authentication type, but found `SHA-512` instead.\n"
"Expected `AES-128` as the privacy type, but found `AES-192` instead.\n"
"For SNMP user Test4:\n"
"Expected `TestGroup3` as the user group, but found `TestGroup4` instead.\n"
"Expected `SHA-512` as the authentication type, but found `SHA-384` instead.\n"
"Expected `AES-192` as the privacy type, but found `AES-128` instead."
"User: Test3 Version: v3 - Incorrect authentication type - Expected: SHA-384 Actual: SHA-512",
"User: Test3 Version: v3 - Incorrect privacy type - Expected: AES-128 Actual: AES-192",
"User: Test4 Version: v3 - Incorrect authentication type - Expected: SHA-512 Actual: SHA-384",
"User: Test4 Version: v3 - Incorrect privacy type - Expected: AES-192 Actual: AES-128",
],
},
},
Expand Down

0 comments on commit 581acc2

Please sign in to comment.