From 578012637f1360d837eab7c445cbf6333f0ff3e9 Mon Sep 17 00:00:00 2001 From: vitthalmagadum <122079046+vitthalmagadum@users.noreply.github.com> Date: Fri, 7 Feb 2025 01:48:41 +0530 Subject: [PATCH] feat(anta): Added the test case to verify SNMP groups (#886) * issue_853 Added TC for SNMP groups * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Issue_853: Refactored testcase with input model changes * Issue_853: UUpdated docstrings,test failure message and optimized code * Issue_853: Updated test failure message * Issue_853: Optimized code, added UT for view configuration check * Issue_853: Updated testcase docstrings and fixed Cognitive Complexity issue * Issue_853: fixed Cognitive Complexity issue * Issue_853: optimized code to fix the congnitive complexity issues * Issue_853: optimized code, updated test failure messages, added snmpauth validator * Issue_853: updated input model unit testcase with snmp auth version change * Issue_853: Added unit testcases for snmp group input model * Issue_853: Updated test failure msga nd custom_types.py file * Refactor: Better annotation * Issue_853: Updated customtypes, and thier unit test for snmpv3 prefix * Update anta/input_models/snmp.py * Updated snmp_v3_prefix --------- Co-authored-by: VitthalMagadum Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Geetanjali.mane Co-authored-by: gmuloc Co-authored-by: Carl Baillargeon --- anta/custom_types.py | 25 +- anta/input_models/snmp.py | 48 +++- anta/tests/snmp.py | 72 +++++- examples/tests.yaml | 14 ++ tests/units/anta_tests/test_snmp.py | 320 ++++++++++++++++++++++++++ tests/units/input_models/test_snmp.py | 27 +++ tests/units/test_custom_types.py | 8 + 7 files changed, 504 insertions(+), 10 deletions(-) diff --git a/anta/custom_types.py b/anta/custom_types.py index 67646f6e7..ccd0b5f6e 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -268,10 +268,6 @@ def validate_regex(value: str) -> str: ] BgpUpdateError = Literal["inUpdErrWithdraw", "inUpdErrIgnore", "inUpdErrDisableAfiSafi", "disabledAfiSafi", "lastUpdErrTime"] BfdProtocol = Literal["bgp", "isis", "lag", "ospf", "ospfv3", "pim", "route-input", "static-bfd", "static-route", "vrrp", "vxlan"] -SnmpPdu = Literal["inGetPdus", "inGetNextPdus", "inSetPdus", "outGetResponsePdus", "outTrapPdus"] -SnmpErrorCounter = Literal[ - "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" -] IPv4RouteType = Literal[ "connected", "static", @@ -301,8 +297,25 @@ def validate_regex(value: str) -> str: "Route Cache Route", "CBF Leaked Route", ] +DynamicVlanSource = Literal["dmf", "dot1x", "dynvtep", "evpn", "mlag", "mlagsync", "mvpn", "swfwd", "vccbfd"] +LogSeverityLevel = Literal["alerts", "critical", "debugging", "emergencies", "errors", "informational", "notifications", "warnings"] + + +######################################## +# SNMP +######################################## +def snmp_v3_prefix(auth_type: Literal["auth", "priv", "noauth"]) -> str: + """Prefix the SNMP authentication type with 'v3'.""" + if auth_type == "noauth": + return "v3NoAuth" + return f"v3{auth_type.title()}" + + 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"] -DynamicVlanSource = Literal["dmf", "dot1x", "dynvtep", "evpn", "mlag", "mlagsync", "mvpn", "swfwd", "vccbfd"] -LogSeverityLevel = Literal["alerts", "critical", "debugging", "emergencies", "errors", "informational", "notifications", "warnings"] +SnmpPdu = Literal["inGetPdus", "inGetNextPdus", "inSetPdus", "outGetResponsePdus", "outTrapPdus"] +SnmpErrorCounter = Literal[ + "inVersionErrs", "inBadCommunityNames", "inBadCommunityUses", "inParseErrs", "outTooBigErrs", "outNoSuchNameErrs", "outBadValueErrs", "outGeneralErrs" +] +SnmpVersionV3AuthType = Annotated[Literal["auth", "priv", "noauth"], AfterValidator(snmp_v3_prefix)] diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py index 0e032b834..9475d9cbe 100644 --- a/anta/input_models/snmp.py +++ b/anta/input_models/snmp.py @@ -6,11 +6,19 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Literal +from typing import TYPE_CHECKING, Literal -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, model_validator -from anta.custom_types import Hostname, Interface, Port, SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion +from anta.custom_types import Hostname, Interface, Port, SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion, SnmpVersionV3AuthType + +if TYPE_CHECKING: + import sys + + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self class SnmpHost(BaseModel): @@ -83,3 +91,37 @@ def __str__(self) -> str: - Source Interface: Ethernet1 VRF: default """ return f"Source Interface: {self.interface} VRF: {self.vrf}" + + +class SnmpGroup(BaseModel): + """Model for an SNMP group.""" + + group_name: str + """SNMP group name.""" + version: SnmpVersion + """SNMP protocol version.""" + read_view: str | None = None + """Optional field, View to restrict read access.""" + write_view: str | None = None + """Optional field, View to restrict write access.""" + notify_view: str | None = None + """Optional field, View to restrict notifications.""" + authentication: SnmpVersionV3AuthType | None = None + """SNMPv3 authentication settings. Required when version is v3. Can be provided in the `VerifySnmpGroup` test.""" + + @model_validator(mode="after") + def validate_inputs(self) -> Self: + """Validate the inputs provided to the SnmpGroup class.""" + if self.version == "v3" and self.authentication is None: + msg = f"{self!s}: `authentication` field is missing in the input" + raise ValueError(msg) + return self + + def __str__(self) -> str: + """Return a human-readable string representation of the SnmpGroup for reporting. + + Examples + -------- + - Group: Test_Group Version: v2c + """ + return f"Group: {self.group_name}, Version: {self.version}" diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 0108d8512..e0aecaf60 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -12,7 +12,7 @@ from pydantic import field_validator from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu -from anta.input_models.snmp import SnmpHost, SnmpSourceInterface, SnmpUser +from anta.input_models.snmp import SnmpGroup, SnmpHost, SnmpSourceInterface, SnmpUser from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -664,3 +664,73 @@ def test(self) -> None: self.result.is_failure(f"{interface_details} - Not configured") elif actual_interface != interface_details.interface: self.result.is_failure(f"{interface_details} - Incorrect source interface - Actual: {actual_interface}") + + +class VerifySnmpGroup(AntaTest): + """Verifies the SNMP group configurations for specified version(s). + + This test performs the following checks: + + 1. Verifies that the SNMP group is configured for the specified version. + 2. For SNMP version 3, verify that the security model matches the expected value. + 3. Ensures that SNMP group configurations, including read, write, and notify views, align with version-specific requirements. + + Expected Results + ---------------- + * Success: The test will pass if the provided SNMP group and all specified parameters are correctly configured. + * Failure: The test will fail if the provided SNMP group is not configured or if any specified parameter is not correctly configured. + + Examples + -------- + ```yaml + anta.tests.snmp: + - VerifySnmpGroup: + snmp_groups: + - group_name: Group1 + version: v1 + read_view: group_read_1 + write_view: group_write_1 + notify_view: group_notify_1 + - group_name: Group2 + version: v3 + read_view: group_read_2 + write_view: group_write_2 + notify_view: group_notify_2 + authentication: priv + ``` + """ + + categories: ClassVar[list[str]] = ["snmp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp group", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifySnmpGroup test.""" + + snmp_groups: list[SnmpGroup] + """List of SNMP groups.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySnmpGroup.""" + self.result.is_success() + for group in self.inputs.snmp_groups: + # Verify SNMP group details. + if not (group_details := get_value(self.instance_commands[0].json_output, f"groups.{group.group_name}.versions.{group.version}")): + self.result.is_failure(f"{group} - Not configured") + continue + + view_types = [view_type for view_type in ["read", "write", "notify"] if getattr(group, f"{view_type}_view")] + # Verify SNMP views, the read, write and notify settings aligning with version-specific requirements. + for view_type in view_types: + expected_view = getattr(group, f"{view_type}_view") + # Verify actual view is configured. + if group_details.get(f"{view_type}View") == "": + self.result.is_failure(f"{group} View: {view_type} - Not configured") + elif (act_view := group_details.get(f"{view_type}View")) != expected_view: + self.result.is_failure(f"{group} - Incorrect {view_type.title()} view - Expected: {expected_view}, Actual: {act_view}") + elif not group_details.get(f"{view_type}ViewConfig"): + self.result.is_failure(f"{group}, {view_type.title()} View: {expected_view} - Not configured") + + # For version v3, verify that the security model aligns with the expected value. + if group.version == "v3" and (actual_auth := group_details.get("secModel")) != group.authentication: + self.result.is_failure(f"{group} - Incorrect security model - Expected: {group.authentication}, Actual: {actual_auth}") diff --git a/examples/tests.yaml b/examples/tests.yaml index c605e362d..4908ee8b8 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -788,6 +788,20 @@ anta.tests.snmp: # Verifies the SNMP error counters. error_counters: - inVersionErrs + - VerifySnmpGroup: + # Verifies the SNMP group configurations for specified version(s). + snmp_groups: + - group_name: Group1 + version: v1 + read_view: group_read_1 + write_view: group_write_1 + notify_view: group_notify_1 + - group_name: Group2 + version: v3 + read_view: group_read_2 + write_view: group_write_2 + notify_view: group_notify_2 + authentication: priv - VerifySnmpHostLogging: # Verifies SNMP logging configurations. hosts: diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index b2eee6a05..2c844c717 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -10,6 +10,7 @@ from anta.tests.snmp import ( VerifySnmpContact, VerifySnmpErrorCounters, + VerifySnmpGroup, VerifySnmpHostLogging, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, @@ -792,4 +793,323 @@ ], }, }, + { + "name": "success", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read_1", + "readViewConfig": True, + "writeView": "group_write_1", + "writeViewConfig": True, + "notifyView": "group_notify_1", + "notifyViewConfig": True, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read_2", + "readViewConfig": True, + "writeView": "group_write_2", + "writeViewConfig": True, + "notifyView": "group_notify_2", + "notifyViewConfig": True, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3Auth", + "readView": "group_read_3", + "readViewConfig": True, + "writeView": "group_write_3", + "writeViewConfig": True, + "notifyView": "group_notify_3", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "write_view": "group_write_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "auth", + }, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-incorrect-view", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3NoAuth", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1", "notify_view": "group_notify_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "noauth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1 - Incorrect Read view - Expected: group_read_1, Actual: group_read", + "Group: Group1, Version: v1 - Incorrect Write view - Expected: group_write_1, Actual: group_write", + "Group: Group1, Version: v1 - Incorrect Notify view - Expected: group_notify_1, Actual: group_notify", + "Group: Group2, Version: v2c - Incorrect Read view - Expected: group_read_2, Actual: group_read", + "Group: Group2, Version: v2c - Incorrect Notify view - Expected: group_notify_2, Actual: group_notify", + "Group: Group3, Version: v3 - Incorrect Read view - Expected: group_read_3, Actual: group_read", + "Group: Group3, Version: v3 - Incorrect Write view - Expected: group_write_3, Actual: group_write", + "Group: Group3, Version: v3 - Incorrect Notify view - Expected: group_notify_3, Actual: group_notify", + ], + }, + }, + { + "name": "failure-view-config-not-found", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": { + "versions": { + "v1": { + "secModel": "v1", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + "Group2": { + "versions": { + "v2c": { + "secModel": "v2c", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + "Group3": { + "versions": { + "v3": { + "secModel": "v3Priv", + "readView": "group_read", + "readViewConfig": False, + "writeView": "group_write", + "writeViewConfig": False, + "notifyView": "group_notify", + "notifyViewConfig": False, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read", "write_view": "group_write", "notify_view": "group_notify"}, + { + "group_name": "Group3", + "version": "v3", + "write_view": "group_write", + "notify_view": "group_notify", + "authentication": "priv", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1, Read View: group_read - Not configured", + "Group: Group1, Version: v1, Write View: group_write - Not configured", + "Group: Group1, Version: v1, Notify View: group_notify - Not configured", + "Group: Group2, Version: v2c, Read View: group_read - Not configured", + "Group: Group2, Version: v2c, Write View: group_write - Not configured", + "Group: Group2, Version: v2c, Notify View: group_notify - Not configured", + "Group: Group3, Version: v3, Write View: group_write - Not configured", + "Group: Group3, Version: v3, Notify View: group_notify - Not configured", + ], + }, + }, + { + "name": "failure-group-version-not-configured", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group1": {"versions": {"v1": {}}}, + "Group2": {"versions": {"v2c": {}}}, + "Group3": {"versions": {"v3": {}}}, + } + } + ], + "inputs": { + "snmp_groups": [ + {"group_name": "Group1", "version": "v1", "read_view": "group_read_1", "write_view": "group_write_1"}, + {"group_name": "Group2", "version": "v2c", "read_view": "group_read_2", "write_view": "group_write_2", "notify_view": "group_notify_2"}, + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read_3", + "write_view": "group_write_3", + "notify_view": "group_notify_3", + "authentication": "auth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group1, Version: v1 - Not configured", + "Group: Group2, Version: v2c - Not configured", + "Group: Group3, Version: v3 - Not configured", + ], + }, + }, + { + "name": "failure-incorrect-v3-auth-model", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group3": { + "versions": { + "v3": { + "secModel": "v3Auth", + "readView": "group_read", + "readViewConfig": True, + "writeView": "group_write", + "writeViewConfig": True, + "notifyView": "group_notify", + "notifyViewConfig": True, + } + } + }, + } + } + ], + "inputs": { + "snmp_groups": [ + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read", + "write_view": "group_write", + "notify_view": "group_notify", + "authentication": "priv", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group3, Version: v3 - Incorrect security model - Expected: v3Priv, Actual: v3Auth", + ], + }, + }, + { + "name": "failure-view-not-configured", + "test": VerifySnmpGroup, + "eos_data": [ + { + "groups": { + "Group3": {"versions": {"v3": {"secModel": "v3NoAuth", "readView": "group_read", "readViewConfig": True, "writeView": "", "notifyView": ""}}}, + } + } + ], + "inputs": { + "snmp_groups": [ + { + "group_name": "Group3", + "version": "v3", + "read_view": "group_read", + "write_view": "group_write", + "authentication": "noauth", + }, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Group: Group3, Version: v3 View: write - Not configured", + ], + }, + }, ] diff --git a/tests/units/input_models/test_snmp.py b/tests/units/input_models/test_snmp.py index e48ea301c..8418955e1 100644 --- a/tests/units/input_models/test_snmp.py +++ b/tests/units/input_models/test_snmp.py @@ -11,9 +11,11 @@ import pytest from pydantic import ValidationError +from anta.input_models.snmp import SnmpGroup from anta.tests.snmp import VerifySnmpNotificationHost, VerifySnmpUser if TYPE_CHECKING: + from anta.custom_types import SnmpVersion, SnmpVersionV3AuthType from anta.input_models.snmp import SnmpHost, SnmpUser @@ -163,3 +165,28 @@ def test_invalid(self, notification_hosts: list[SnmpHost]) -> None: """Test VerifySnmpNotificationHost.Input invalid inputs.""" with pytest.raises(ValidationError): VerifySnmpNotificationHost.Input(notification_hosts=notification_hosts) + + +class TestSnmpGroupInput: + """Test anta.input_models.snmp.SnmpGroup.""" + + @pytest.mark.parametrize( + ("group_name", "version", "read_view", "write_view", "notify_view", "authentication"), + [ + pytest.param("group1", "v3", "", "write_1", None, "auth", id="snmp-auth"), + ], + ) + def test_valid(self, group_name: str, read_view: str, version: SnmpVersion, write_view: str, notify_view: str, authentication: SnmpVersionV3AuthType) -> None: + """Test SnmpGroup valid inputs.""" + SnmpGroup(group_name=group_name, version=version, read_view=read_view, write_view=write_view, notify_view=notify_view, authentication=authentication) + + @pytest.mark.parametrize( + ("group_name", "version", "read_view", "write_view", "notify_view", "authentication"), + [ + pytest.param("group1", "v3", "", "write_1", None, None, id="snmp-invalid-auth"), + ], + ) + def test_invalid(self, group_name: str, read_view: str, version: SnmpVersion, write_view: str, notify_view: str, authentication: SnmpVersionV3AuthType) -> None: + """Test SnmpGroup invalid inputs.""" + with pytest.raises(ValidationError): + SnmpGroup(group_name=group_name, version=version, read_view=read_view, write_view=write_view, notify_view=notify_view, authentication=authentication) diff --git a/tests/units/test_custom_types.py b/tests/units/test_custom_types.py index 71f13e088..c0f3f3a4f 100644 --- a/tests/units/test_custom_types.py +++ b/tests/units/test_custom_types.py @@ -25,6 +25,7 @@ bgp_multiprotocol_capabilities_abbreviations, interface_autocomplete, interface_case_sensitivity, + snmp_v3_prefix, validate_regex, ) @@ -259,3 +260,10 @@ def test_validate_regex_invalid(str_input: str, error: str) -> None: """Test validate_regex with invalid regex.""" with pytest.raises(ValueError, match=error): validate_regex(str_input) + + +def test_snmp_v3_prefix_valid_input() -> None: + """Test snmp_v3_prefix with valid authentication type.""" + assert snmp_v3_prefix("auth") == "v3Auth" + assert snmp_v3_prefix("noauth") == "v3NoAuth" + assert snmp_v3_prefix("priv") == "v3Priv"