From b8a3419a676e83ac91bd77a66cb1819c404e2bcd Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Thu, 26 Sep 2024 07:35:54 -0400 Subject: [PATCH 1/7] iisue_822 Added TC for SNMP Notification host --- anta/custom_types.py | 1 + anta/tests/snmp.py | 135 +++++++++++++++++++++++++++- tests/units/anta_tests/test_snmp.py | 117 ++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 3 deletions(-) diff --git a/anta/custom_types.py b/anta/custom_types.py index bd6a7b8d2..68e9fa82e 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -238,3 +238,4 @@ def validate_regex(value: str) -> str: "Route Cache Route", "CBF Leaked Route", ] +SnmpVersion = Literal["v1", "v2c", "v3"] diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 36b97ba2f..a89b9f77f 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -7,11 +7,14 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, get_args +from ipaddress import IPv4Address +from typing import TYPE_CHECKING, ClassVar, Literal, get_args -from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu +from pydantic import BaseModel, model_validator + +from anta.custom_types import Port, PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion from anta.models import AntaCommand, AntaTest -from anta.tools import get_value +from anta.tools import get_failed_logs, get_item, get_value if TYPE_CHECKING: from anta.models import AntaTemplate @@ -339,3 +342,129 @@ 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 VerifySNMPNotificationHost(AntaTest): + """Verifies the SNMP notification host (SNMP manager) configurations. + + - Verifies that the valid notification type and VRF name. + - Ensures that UDP port provided matches the expected value. + - Ensures that the community_string is properly set for SNMP v1/v2 and for SNMP v3, the user field is included, aligning with version-specific requirements. + + Expected Results + ---------------- + * Success: The test will pass if the provided SNMP notification host and all specified parameters are correctly configured. + * Failure: The test will fail if the provided SNMP notification host is not configured or specified parameters are not correctly configured. + + Examples + -------- + ```yaml + anta.tests.snmp: + - VerifySNMPNotificationHost: + notification_hosts: + - hostname: 192.168.1.100 + vrf: default + notification_type: trap + version: v1 + udp_port: 162 + community_string: public + user: public + ``` + """ + + name = "VerifySNMPNotificationHost" + description = "Verifies the SNMP notification host (SNMP manager) configurations." + categories: ClassVar[list[str]] = ["snmp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp notification host", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifySNMPNotificationHost test.""" + + notification_hosts: list[SNMPHost] + """List of SNMP hosts.""" + + class SNMPHost(BaseModel): + """Model for a SNMP Host.""" + + hostname: IPv4Address + """IPv4 address of the SNMP notification host.""" + vrf: str = "default" + """Optional VRF for SNMP Hosts. If not provided, it defaults to `default`.""" + notification_type: Literal["trap", "inform"] + """Type of SNMP notification (trap or inform).""" + version: SnmpVersion + """SNMP protocol version.""" + udp_port: Port | int = 162 + """UDP port for SNMP. If not provided then defaults to 162.""" + community_string: str | None = None + """Optional SNMP community string for authentication.""" + user: str | None = None + """Optional SNMP user for authentication.""" + + @model_validator(mode="after") + def validate_inputs(self: BaseModel) -> BaseModel: + """Validate the inputs provided to the SNMPHost class. + + If SNMP version is either v1 or v2c, community string must be provided. + + If SNMP version is v3, user must be provided. + """ + if self.version in ["v1", "v2c"] and self.community_string is None: + msg = "Community string must be provided when SNMP Protocol version is either v1 or v2c." + raise ValueError(msg) + if self.version == "v3" and self.user is None: + msg = "User must be provided when SNMP Protocol version is v3." + raise ValueError(msg) + return self + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySNMPNotificationHost.""" + self.result.is_success() + failures: str = "" + + # Verify SNMP host details. + if not (snmp_hosts := get_value(self.instance_commands[0].json_output, "hosts")): + self.result.is_failure("No SNMP host is configured.") + return + + for host in self.inputs.notification_hosts: + hostname = str(host.hostname) + vrf = host.vrf + version = host.version + notification_type = host.notification_type + udp_port = host.udp_port + community_string = host.community_string + user = host.user + + # Verify SNMP host details. + if not (host_details := get_item(snmp_hosts, "hostname", hostname)): + failures += f"SNMP host '{hostname}' is not configured.\n" + continue + + # Update expected host details. + expected_host_details = {"vrf": vrf, "notification type": notification_type, "udp port": udp_port} + + # Update actual host details. + actual_host_details = {"notification type": host_details.get("notificationType", "Not Found"), "udp port": host_details.get("port", "Not Found")} + + # Verify SNMP protocol version. + if version in ["v1", "v2c"]: + expected_host_details["community_string"] = community_string + actual_host_details["community_string"] = host_details.get("v1v2cParams", {}).get("communityString", "Not Found") + + if version == "v3": + expected_host_details["user"] = user + actual_host_details["user"] = host_details.get("v3Params", {}).get("user", "Not Found") + + # Verify the VRF for SNMP Hosts. If vrf is default then command output consists empty string. + actual_host_details["vrf"] = "default" if (vrf_name := host_details.get("vrf", "Not Found")) == "" else vrf_name + + # Collecting failures logs if any. + failure_logs = get_failed_logs(expected_host_details, actual_host_details) + if failure_logs: + failures += f"For SNMP host {hostname}:{failure_logs}\n" + + # Check if there are any failures. + if failures: + self.result.is_failure(failures) diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index 195ef298e..349067133 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -13,6 +13,7 @@ VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, VerifySnmpLocation, + VerifySNMPNotificationHost, VerifySnmpPDUCounters, VerifySnmpStatus, ) @@ -319,4 +320,120 @@ ], }, }, + { + "name": "success", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v3", + "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, + }, + { + "hostname": "192.168.1.101", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v2c", + "v1v2cParams": {"communityString": "public"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-not-configured", + "test": VerifySNMPNotificationHost, + "eos_data": [{"hosts": []}], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": {"result": "failure", "messages": ["No SNMP host is configured."]}, + }, + { + "name": "failure-details-not-found", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v3", + "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": {"result": "failure", "messages": ["SNMP host '192.168.1.101' is not configured.\n"]}, + }, + { + "name": "failure-incorrect-config", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 163, + "vrf": "", + "notificationType": "inform", + "protocolVersion": "v3", + "v3Params": {"user": "public1", "securityLevel": "authNoPriv"}, + }, + { + "hostname": "192.168.1.101", + "port": 163, + "vrf": "MGMT", + "notificationType": "inform", + "protocolVersion": "v2c", + "v1v2cParams": {"communityString": "public1"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "For SNMP host 192.168.1.100:\n" + "Expected `trap` as the notification type, but found `inform` instead.\n" + "Expected `162` as the udp port, but found `163` instead.\n" + "Expected `public` as the user, but found `public1` instead.\n" + "For SNMP host 192.168.1.101:\n" + "Expected `default` as the vrf, but found `MGMT` instead.\n" + "Expected `trap` as the notification type, but found `inform` instead.\n" + "Expected `162` as the udp port, but found `163` instead.\n" + "Expected `public` as the community_string, but found `public1` instead.\n" + ], + }, + }, ] From dcdcf9cc3afdf75093fd60c5d0638166c22656f4 Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Mon, 30 Sep 2024 07:26:23 -0400 Subject: [PATCH 2/7] issue_822 Handling review comments: updated failure msgs and docstring --- anta/tests/snmp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index a89b9f77f..483934263 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -241,10 +241,14 @@ class VerifySnmpPDUCounters(AntaTest): By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. + - Verifies that the valid notification type and VRF name. + - Ensures that UDP port provided matches the expected value. + - Ensures that the community_string is properly set for SNMP v1/v2 and for SNMP v3, the user field is included, aligning with version-specific requirements. + Expected Results ---------------- - * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. - * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. + * Success: The test will pass if the provided SNMP notification host and all specified parameters are correctly configured. + * Failure: The test will fail if the provided SNMP notification host is not configured or specified parameters are not correctly configured. Examples -------- From c793bcb8c3a50acb60490457dc6a7e67df637c7c Mon Sep 17 00:00:00 2001 From: VitthalMagadum Date: Mon, 14 Oct 2024 06:55:58 -0400 Subject: [PATCH 3/7] issue_822 Updated after resolved conflicts --- anta/tests/snmp.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 483934263..a89b9f77f 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -241,14 +241,10 @@ class VerifySnmpPDUCounters(AntaTest): By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. - - Verifies that the valid notification type and VRF name. - - Ensures that UDP port provided matches the expected value. - - Ensures that the community_string is properly set for SNMP v1/v2 and for SNMP v3, the user field is included, aligning with version-specific requirements. - Expected Results ---------------- - * Success: The test will pass if the provided SNMP notification host and all specified parameters are correctly configured. - * Failure: The test will fail if the provided SNMP notification host is not configured or specified parameters are not correctly configured. + * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. + * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples -------- From ce6aa8c233344e0e6e87bda671052ce1d5d11160 Mon Sep 17 00:00:00 2001 From: "Geetanjali.mane" Date: Thu, 9 Jan 2025 08:00:27 +0000 Subject: [PATCH 4/7] Issue_822: Refactored testcase with latest inputs changes --- anta/input_models/snmp.py | 42 ++++++++ anta/tests/snmp.py | 127 ++++++++++-------------- docs/api/tests.snmp.md | 15 +++ examples/tests.yaml | 10 ++ tests/units/anta_tests/test_snmp.py | 131 ++++++++++++++++++++---- tests/units/input_models/test_snmp.py | 138 ++++++++++++++++++++++++++ 6 files changed, 372 insertions(+), 91 deletions(-) create mode 100644 anta/input_models/snmp.py create mode 100644 tests/units/input_models/test_snmp.py diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py new file mode 100644 index 000000000..e2671425d --- /dev/null +++ b/anta/input_models/snmp.py @@ -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 ipaddress import IPv4Address +from typing import Literal + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import Hostname, Port, SnmpVersion + + +class SNMPHost(BaseModel): + """Model for a SNMP Host.""" + + model_config = ConfigDict(extra="forbid") + hostname: IPv4Address | Hostname + """IPv4 address of the SNMP notification host.""" + vrf: str = "default" + """Optional VRF for SNMP Hosts. If not provided, it defaults to `default`.""" + notification_type: Literal["trap", "inform"] = "trap" + """Type of SNMP notification (trap or inform), it defaults to trap.""" + version: SnmpVersion | None = None + """SNMP protocol version.Required field in the `VerifySNMPNotificationHost` test.""" + udp_port: Port | int = 162 + """UDP port for SNMP. If not provided then defaults to 162.""" + community_string: str | None = None + """Optional SNMP community string for authentication,required only for version is v2 or vc2. Can be provided in the `VerifySNMPNotificationHost` test.""" + user: str | None = None + """Optional SNMP user for authentication, required only for the version v3. Can be provided in the `VerifySNMPNotificationHost` test.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the Host for reporting. + + Examples + -------- + - Host: 192.168.1.100 VRF: default + """ + return f"Host: {self.hostname} VRF: {self.vrf}" diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index a89b9f77f..704c92fec 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -7,14 +7,14 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from ipaddress import IPv4Address -from typing import TYPE_CHECKING, ClassVar, Literal, get_args +from typing import TYPE_CHECKING, ClassVar, get_args -from pydantic import BaseModel, model_validator +from pydantic import field_validator -from anta.custom_types import Port, PositiveInteger, SnmpErrorCounter, SnmpPdu, SnmpVersion +from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu +from anta.input_models.snmp import SNMPHost from anta.models import AntaCommand, AntaTest -from anta.tools import get_failed_logs, get_item, get_value +from anta.tools import get_value if TYPE_CHECKING: from anta.models import AntaTemplate @@ -347,14 +347,27 @@ def test(self) -> None: class VerifySNMPNotificationHost(AntaTest): """Verifies the SNMP notification host (SNMP manager) configurations. - - Verifies that the valid notification type and VRF name. - - Ensures that UDP port provided matches the expected value. - - Ensures that the community_string is properly set for SNMP v1/v2 and for SNMP v3, the user field is included, aligning with version-specific requirements. + This test performs the following checks for each specified host: + + 1. Verifies that the SNMP host and hostname is configured on the device. + 2. Verifies that the notification type matches the expected value. + 3. Ensures that UDP port provided matches the expected value. + 4. Ensures that the a valid community string is properly set for SNMP version v1/vc2 and it should match the expected value. + 5. Ensures that the a valid the user field properly set for SNMP version v3 and it should match the expected value. Expected Results ---------------- - * Success: The test will pass if the provided SNMP notification host and all specified parameters are correctly configured. - * Failure: The test will fail if the provided SNMP notification host is not configured or specified parameters are not correctly configured. + * Success: The test will pass if all of the following conditions are met: + - The SNMP host and hostname is configured on the device. + - The notification type and UDP port matches the expected value. + - The valid community string is properly set for SNMP version v1/vc2 and it should matches the expected value. + - The valid user field is included for SNMP v3 and it should matches the expected value. + + * Failure: The test will fail if any of the following conditions is met: + - The SNMP host or hostname is not configured on the device. + - The notification type or UDP port do not matches the expected value. + - The community string is for SNMP version v1/vc2 is not matches the expected value. + - The user field for SNMP version v3 is not matches the expected value. Examples -------- @@ -372,8 +385,6 @@ class VerifySNMPNotificationHost(AntaTest): ``` """ - name = "VerifySNMPNotificationHost" - description = "Verifies the SNMP notification host (SNMP manager) configurations." categories: ClassVar[list[str]] = ["snmp"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp notification host", revision=1)] @@ -383,88 +394,58 @@ class Input(AntaTest.Input): notification_hosts: list[SNMPHost] """List of SNMP hosts.""" - class SNMPHost(BaseModel): - """Model for a SNMP Host.""" - - hostname: IPv4Address - """IPv4 address of the SNMP notification host.""" - vrf: str = "default" - """Optional VRF for SNMP Hosts. If not provided, it defaults to `default`.""" - notification_type: Literal["trap", "inform"] - """Type of SNMP notification (trap or inform).""" - version: SnmpVersion - """SNMP protocol version.""" - udp_port: Port | int = 162 - """UDP port for SNMP. If not provided then defaults to 162.""" - community_string: str | None = None - """Optional SNMP community string for authentication.""" - user: str | None = None - """Optional SNMP user for authentication.""" - - @model_validator(mode="after") - def validate_inputs(self: BaseModel) -> BaseModel: - """Validate the inputs provided to the SNMPHost class. - - If SNMP version is either v1 or v2c, community string must be provided. - - If SNMP version is v3, user must be provided. - """ - if self.version in ["v1", "v2c"] and self.community_string is None: - msg = "Community string must be provided when SNMP Protocol version is either v1 or v2c." + @field_validator("notification_hosts") + @classmethod + def validate_notification_hosts(cls, notification_hosts: list[SNMPHost]) -> list[SNMPHost]: + """Validate that all required fields are provided in each SNMP Notification Host.""" + for host in notification_hosts: + if host.version is None: + msg = f"{host}; 'version' field missing in the input" + raise ValueError(msg) + if host.version in ["v1", "v2c"] and host.community_string is None: + msg = f"{host} Version: {host.version}; 'community_string' field missing in the input" raise ValueError(msg) - if self.version == "v3" and self.user is None: - msg = "User must be provided when SNMP Protocol version is v3." + if host.version == "v3" and host.user is None: + msg = f"{host} Version: {host.version}; 'user' field missing in the input" raise ValueError(msg) - return self + return notification_hosts @AntaTest.anta_test def test(self) -> None: """Main test function for VerifySNMPNotificationHost.""" self.result.is_success() - failures: str = "" - # Verify SNMP host details. + # If SNMP host is not configured, test fails. if not (snmp_hosts := get_value(self.instance_commands[0].json_output, "hosts")): self.result.is_failure("No SNMP host is configured.") return for host in self.inputs.notification_hosts: hostname = str(host.hostname) - vrf = host.vrf - version = host.version notification_type = host.notification_type + version = host.version udp_port = host.udp_port community_string = host.community_string user = host.user - # Verify SNMP host details. - if not (host_details := get_item(snmp_hosts, "hostname", hostname)): - failures += f"SNMP host '{hostname}' is not configured.\n" + host_details = next((host for host in snmp_hosts if (host.get("hostname") == hostname and host.get("protocolVersion") == version)), None) + # If expected SNMP hostname is not configured with the specified protocol version, test fails. + if not host_details: + self.result.is_failure(f"{host} Version: {version} - Not configured") continue - # Update expected host details. - expected_host_details = {"vrf": vrf, "notification type": notification_type, "udp port": udp_port} - - # Update actual host details. - actual_host_details = {"notification type": host_details.get("notificationType", "Not Found"), "udp port": host_details.get("port", "Not Found")} - - # Verify SNMP protocol version. - if version in ["v1", "v2c"]: - expected_host_details["community_string"] = community_string - actual_host_details["community_string"] = host_details.get("v1v2cParams", {}).get("communityString", "Not Found") - - if version == "v3": - expected_host_details["user"] = user - actual_host_details["user"] = host_details.get("v3Params", {}).get("user", "Not Found") + # If actual notification type do not matches the expected value, test fails. + if notification_type != (actual_notification_type := get_value(host_details, "notificationType", "Not Found")): + self.result.is_failure(f"{host} - Incorrect notification type - Expected: {notification_type} Actual: {actual_notification_type}") - # Verify the VRF for SNMP Hosts. If vrf is default then command output consists empty string. - actual_host_details["vrf"] = "default" if (vrf_name := host_details.get("vrf", "Not Found")) == "" else vrf_name + # If actual udp port do not matches the expected value, test fails. + if udp_port != (actual_udp_port := get_value(host_details, "port", "Not Found")): + self.result.is_failure(f"{host} - Incorrect UDP port - Expected: {udp_port} Actual: {actual_udp_port}") - # Collecting failures logs if any. - failure_logs = get_failed_logs(expected_host_details, actual_host_details) - if failure_logs: - failures += f"For SNMP host {hostname}:{failure_logs}\n" + # If SNMP protocol version is v1 or v2c and actual community string do not matches the expected value, test fails. + if version in ["v1", "v2c"] and community_string != (actual_community_string := get_value(host_details, "v1v2cParams.communityString", "Not Found")): + self.result.is_failure(f"{host} Version: {version} - Incorrect community string - Expected: {community_string} Actual: {actual_community_string}") - # Check if there are any failures. - if failures: - self.result.is_failure(failures) + # If SNMP protocol version is v3 and actual user do not matches the expected value, test fails. + if version == "v3" and user != (actual_user := get_value(host_details, "v3Params.user", "Not Found")): + self.result.is_failure(f"{host} Version: {version} - Incorrect user - Expected: {user} Actual: {actual_user}") diff --git a/docs/api/tests.snmp.md b/docs/api/tests.snmp.md index 85c147e2a..8c01e2bd7 100644 --- a/docs/api/tests.snmp.md +++ b/docs/api/tests.snmp.md @@ -7,6 +7,8 @@ anta_title: ANTA catalog for SNMP tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.snmp options: show_root_heading: false @@ -18,3 +20,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__"] diff --git a/examples/tests.yaml b/examples/tests.yaml index a4bc1fabf..62fef881f 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -717,6 +717,16 @@ anta.tests.services: # Verifies the hostname of a device. hostname: s1-spine1 anta.tests.snmp: + - VerifySNMPNotificationHost: + # Verifies the SNMP notification host (SNMP manager) configurations. + notification_hosts: + - hostname: 192.168.1.100 + vrf: default + notification_type: trap + version: v1 + udp_port: 162 + community_string: public + user: public - VerifySnmpContact: # Verifies the SNMP contact of a device. contact: Jon@example.com diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index 349067133..da60fc13a 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -366,7 +366,7 @@ "expected": {"result": "failure", "messages": ["No SNMP host is configured."]}, }, { - "name": "failure-details-not-found", + "name": "failure-details-host-not-found", "test": VerifySNMPNotificationHost, "eos_data": [ { @@ -388,29 +388,68 @@ {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, - "expected": {"result": "failure", "messages": ["SNMP host '192.168.1.101' is not configured.\n"]}, + "expected": {"result": "failure", "messages": ["Host: 192.168.1.101 VRF: default - Not configured"]}, }, { - "name": "failure-incorrect-config", + "name": "failure-incorrect-notification-type", "test": VerifySNMPNotificationHost, "eos_data": [ { "hosts": [ { "hostname": "192.168.1.100", - "port": 163, + "port": 162, "vrf": "", - "notificationType": "inform", + "notificationType": "trap", "protocolVersion": "v3", - "v3Params": {"user": "public1", "securityLevel": "authNoPriv"}, + "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, }, { "hostname": "192.168.1.101", - "port": 163, - "vrf": "MGMT", + "port": 162, + "vrf": "", "notificationType": "inform", "protocolVersion": "v2c", - "v1v2cParams": {"communityString": "public1"}, + "v1v2cParams": {"communityString": "public"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "inform", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Host: 192.168.1.100 VRF: default - Incorrect notification type - Expected: inform Actual: trap", + "Host: 192.168.1.101 VRF: default - Incorrect notification type - Expected: trap Actual: inform", + ], + }, + }, + { + "name": "failure-incorrect-udp-port", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 163, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v3", + "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, + }, + { + "hostname": "192.168.1.101", + "port": 164, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v2c", + "v1v2cParams": {"communityString": "public"}, }, ] } @@ -424,16 +463,72 @@ "expected": { "result": "failure", "messages": [ - "For SNMP host 192.168.1.100:\n" - "Expected `trap` as the notification type, but found `inform` instead.\n" - "Expected `162` as the udp port, but found `163` instead.\n" - "Expected `public` as the user, but found `public1` instead.\n" - "For SNMP host 192.168.1.101:\n" - "Expected `default` as the vrf, but found `MGMT` instead.\n" - "Expected `trap` as the notification type, but found `inform` instead.\n" - "Expected `162` as the udp port, but found `163` instead.\n" - "Expected `public` as the community_string, but found `public1` instead.\n" + "Host: 192.168.1.100 VRF: default - Incorrect UDP port - Expected: 162 Actual: 163", + "Host: 192.168.1.101 VRF: default - Incorrect UDP port - Expected: 162 Actual: 164", + ], + }, + }, + { + "name": "failure-incorrect-community-string-version-v1-vc2", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v1", + "v1v2cParams": {"communityString": "private"}, + }, + { + "hostname": "192.168.1.101", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "vc2", + "v1v2cParams": {"communityString": "private"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v1", "udp_port": 162, "community_string": "public"}, + {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Host: 192.168.1.100 VRF: default Version: v1 - Incorrect community string - Expected: public Actual: private", + "Host: 192.168.1.101 VRF: default Version: v2c - Incorrect community string - Expected: public Actual: private", ], }, }, + { + "name": "failure-incorrect-user-for-version-v3", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 162, + "vrf": "", + "notificationType": "trap", + "protocolVersion": "v3", + "v3Params": {"user": "private", "securityLevel": "authNoPriv"}, + } + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + ] + }, + "expected": {"result": "failure", "messages": ["Host: 192.168.1.100 VRF: default Version: v3 - Incorrect user - Expected: public Actual: private"]}, + }, ] diff --git a/tests/units/input_models/test_snmp.py b/tests/units/input_models/test_snmp.py new file mode 100644 index 000000000..0a1fbea98 --- /dev/null +++ b/tests/units/input_models/test_snmp.py @@ -0,0 +1,138 @@ +# 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. +"""Tests for anta.input_models.snmp.py.""" + +# pylint: disable=C0302 +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from pydantic import ValidationError + +from anta.tests.snmp import VerifySNMPNotificationHost + +if TYPE_CHECKING: + from anta.input_models.snmp import SNMPHost + + +class TestSNMPHost: + """Test anta.input_models.snmp.SNMPHost.""" + + @pytest.mark.parametrize( + ("notification_hosts"), + [ + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v1", + "udp_port": 162, + "community_string": "public", + "user": None, + } + ], + id="valid-v1", + ), + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v2c", + "udp_port": 162, + "community_string": "public", + "user": None, + } + ], + id="valid-v2c", + ), + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v3", + "udp_port": 162, + "community_string": None, + "user": "public", + } + ], + id="valid-v3", + ), + ], + ) + def test_valid(self, notification_hosts: list[SNMPHost]) -> None: + """Test VerifySNMPNotificationHost.Input valid inputs.""" + VerifySNMPNotificationHost.Input(notification_hosts=notification_hosts) + + @pytest.mark.parametrize( + ("notification_hosts"), + [ + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": None, + "udp_port": 162, + "community_string": None, + "user": None, + } + ], + id="invalid-version", + ), + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v1", + "udp_port": 162, + "community_string": None, + "user": None, + } + ], + id="invalid-community-string-version-v1", + ), + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v2c", + "udp_port": 162, + "community_string": None, + "user": None, + } + ], + id="invalid-community-string-version-v2c", + ), + pytest.param( + [ + { + "hostname": "192.168.1.100", + "vrf": "test", + "notification_type": "trap", + "version": "v3", + "udp_port": 162, + "community_string": None, + "user": None, + } + ], + id="invalid-user-version-v3", + ), + ], + ) + def test_invalid(self, notification_hosts: list[SNMPHost]) -> None: + """Test VerifySNMPNotificationHost.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifySNMPNotificationHost.Input(notification_hosts=notification_hosts) From 3a4dc19745f5de7d0ac9017fabaf3b10e7fb44c3 Mon Sep 17 00:00:00 2001 From: "Geetanjali.mane" Date: Thu, 9 Jan 2025 09:04:40 +0000 Subject: [PATCH 5/7] Issue_822: Updated unitestcase,docstrings and added check for incorrect VRF. --- anta/input_models/snmp.py | 4 +-- anta/tests/snmp.py | 13 +++++--- tests/units/anta_tests/test_snmp.py | 46 +++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py index e2671425d..bf2cbeb36 100644 --- a/anta/input_models/snmp.py +++ b/anta/input_models/snmp.py @@ -28,9 +28,9 @@ class SNMPHost(BaseModel): udp_port: Port | int = 162 """UDP port for SNMP. If not provided then defaults to 162.""" community_string: str | None = None - """Optional SNMP community string for authentication,required only for version is v2 or vc2. Can be provided in the `VerifySNMPNotificationHost` test.""" + """Optional SNMP community string for authentication,required for SNMP version is v1 or v2c. Can be provided in the `VerifySNMPNotificationHost` test.""" user: str | None = None - """Optional SNMP user for authentication, required only for the version v3. Can be provided in the `VerifySNMPNotificationHost` test.""" + """Optional SNMP user for authentication, required for SNMP version v3. Can be provided in the `VerifySNMPNotificationHost` test.""" def __str__(self) -> str: """Return a human-readable string representation of the Host for reporting. diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 704c92fec..0ef02be52 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -352,21 +352,21 @@ class VerifySNMPNotificationHost(AntaTest): 1. Verifies that the SNMP host and hostname is configured on the device. 2. Verifies that the notification type matches the expected value. 3. Ensures that UDP port provided matches the expected value. - 4. Ensures that the a valid community string is properly set for SNMP version v1/vc2 and it should match the expected value. - 5. Ensures that the a valid the user field properly set for SNMP version v3 and it should match the expected value. + 4. Ensures that the a valid community string is properly set for SNMP version v1/v2c and it should matches the expected value. + 5. Ensures that the a valid user field is properly set for SNMP version v3 and it should matches the expected value. Expected Results ---------------- * Success: The test will pass if all of the following conditions are met: - The SNMP host and hostname is configured on the device. - The notification type and UDP port matches the expected value. - - The valid community string is properly set for SNMP version v1/vc2 and it should matches the expected value. + - The valid community string is properly set for SNMP version v1/v2c and it should matches the expected value. - The valid user field is included for SNMP v3 and it should matches the expected value. * Failure: The test will fail if any of the following conditions is met: - The SNMP host or hostname is not configured on the device. - The notification type or UDP port do not matches the expected value. - - The community string is for SNMP version v1/vc2 is not matches the expected value. + - The community string is for SNMP version v1/v2c is not matches the expected value. - The user field for SNMP version v3 is not matches the expected value. Examples @@ -421,6 +421,7 @@ def test(self) -> None: return for host in self.inputs.notification_hosts: + vrf = host.vrf hostname = str(host.hostname) notification_type = host.notification_type version = host.version @@ -433,6 +434,10 @@ def test(self) -> None: if not host_details: self.result.is_failure(f"{host} Version: {version} - Not configured") continue + actual_vrf = "default" if (vrf_name := host_details.get("vrf")) == "" else vrf_name + + if actual_vrf != vrf: + self.result.is_failure(f"{host} - Incorrect VRF - Actual: {actual_vrf}") # If actual notification type do not matches the expected value, test fails. if notification_type != (actual_notification_type := get_value(host_details, "notificationType", "Not Found")): diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index da60fc13a..686f39438 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -337,7 +337,7 @@ { "hostname": "192.168.1.101", "port": 162, - "vrf": "", + "vrf": "MGMT", "notificationType": "trap", "protocolVersion": "v2c", "v1v2cParams": {"communityString": "public"}, @@ -348,7 +348,7 @@ "inputs": { "notification_hosts": [ {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, - {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + {"hostname": "192.168.1.101", "vrf": "MGMT", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, "expected": {"result": "success"}, @@ -388,7 +388,7 @@ {"hostname": "192.168.1.101", "vrf": "default", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, ] }, - "expected": {"result": "failure", "messages": ["Host: 192.168.1.101 VRF: default - Not configured"]}, + "expected": {"result": "failure", "messages": ["Host: 192.168.1.101 VRF: default Version: v2c - Not configured"]}, }, { "name": "failure-incorrect-notification-type", @@ -469,7 +469,7 @@ }, }, { - "name": "failure-incorrect-community-string-version-v1-vc2", + "name": "failure-incorrect-community-string-version-v1-v2c", "test": VerifySNMPNotificationHost, "eos_data": [ { @@ -487,7 +487,7 @@ "port": 162, "vrf": "", "notificationType": "trap", - "protocolVersion": "vc2", + "protocolVersion": "v2c", "v1v2cParams": {"communityString": "private"}, }, ] @@ -531,4 +531,40 @@ }, "expected": {"result": "failure", "messages": ["Host: 192.168.1.100 VRF: default Version: v3 - Incorrect user - Expected: public Actual: private"]}, }, + { + "name": "failure-incorrect-vrf", + "test": VerifySNMPNotificationHost, + "eos_data": [ + { + "hosts": [ + { + "hostname": "192.168.1.100", + "port": 162, + "vrf": "MGMT", + "notificationType": "trap", + "protocolVersion": "v3", + "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, + }, + { + "hostname": "192.168.1.101", + "port": 162, + "vrf": "test", + "notificationType": "trap", + "protocolVersion": "v2c", + "v1v2cParams": {"communityString": "public"}, + }, + ] + } + ], + "inputs": { + "notification_hosts": [ + {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, + {"hostname": "192.168.1.101", "vrf": "MGMT", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, + ] + }, + "expected": { + "result": "failure", + "messages": ["Host: 192.168.1.100 VRF: default - Incorrect VRF - Actual: MGMT", "Host: 192.168.1.101 VRF: MGMT - Incorrect VRF - Actual: test"], + }, + }, ] From 1692ea871529171b466bfabac806926cc761bed5 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 9 Jan 2025 05:06:03 -0500 Subject: [PATCH 6/7] fixing the sonar issues --- anta/tests/snmp.py | 16 ++++++------- tests/units/anta_tests/test_snmp.py | 36 ----------------------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 0ef02be52..05587a4ea 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -421,7 +421,7 @@ def test(self) -> None: return for host in self.inputs.notification_hosts: - vrf = host.vrf + vrf = "" if host.vrf == "default" else host.vrf hostname = str(host.hostname) notification_type = host.notification_type version = host.version @@ -429,15 +429,13 @@ def test(self) -> None: community_string = host.community_string user = host.user - host_details = next((host for host in snmp_hosts if (host.get("hostname") == hostname and host.get("protocolVersion") == version)), None) + host_details = next( + (host for host in snmp_hosts if (host.get("hostname") == hostname and host.get("protocolVersion") == version and host.get("vrf") == vrf)), None + ) # If expected SNMP hostname is not configured with the specified protocol version, test fails. if not host_details: self.result.is_failure(f"{host} Version: {version} - Not configured") continue - actual_vrf = "default" if (vrf_name := host_details.get("vrf")) == "" else vrf_name - - if actual_vrf != vrf: - self.result.is_failure(f"{host} - Incorrect VRF - Actual: {actual_vrf}") # If actual notification type do not matches the expected value, test fails. if notification_type != (actual_notification_type := get_value(host_details, "notificationType", "Not Found")): @@ -449,8 +447,10 @@ def test(self) -> None: # If SNMP protocol version is v1 or v2c and actual community string do not matches the expected value, test fails. if version in ["v1", "v2c"] and community_string != (actual_community_string := get_value(host_details, "v1v2cParams.communityString", "Not Found")): - self.result.is_failure(f"{host} Version: {version} - Incorrect community string - Expected: {community_string} Actual: {actual_community_string}") + self.result.is_failure( + f"{host} Version: {version} - Incorrect community string - Expected: {community_string} Actual: {actual_community_string}" + ) # If SNMP protocol version is v3 and actual user do not matches the expected value, test fails. - if version == "v3" and user != (actual_user := get_value(host_details, "v3Params.user", "Not Found")): + elif version == "v3" and user != (actual_user := get_value(host_details, "v3Params.user", "Not Found")): self.result.is_failure(f"{host} Version: {version} - Incorrect user - Expected: {user} Actual: {actual_user}") diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index 686f39438..d609f9389 100644 --- a/tests/units/anta_tests/test_snmp.py +++ b/tests/units/anta_tests/test_snmp.py @@ -531,40 +531,4 @@ }, "expected": {"result": "failure", "messages": ["Host: 192.168.1.100 VRF: default Version: v3 - Incorrect user - Expected: public Actual: private"]}, }, - { - "name": "failure-incorrect-vrf", - "test": VerifySNMPNotificationHost, - "eos_data": [ - { - "hosts": [ - { - "hostname": "192.168.1.100", - "port": 162, - "vrf": "MGMT", - "notificationType": "trap", - "protocolVersion": "v3", - "v3Params": {"user": "public", "securityLevel": "authNoPriv"}, - }, - { - "hostname": "192.168.1.101", - "port": 162, - "vrf": "test", - "notificationType": "trap", - "protocolVersion": "v2c", - "v1v2cParams": {"communityString": "public"}, - }, - ] - } - ], - "inputs": { - "notification_hosts": [ - {"hostname": "192.168.1.100", "vrf": "default", "notification_type": "trap", "version": "v3", "udp_port": 162, "user": "public"}, - {"hostname": "192.168.1.101", "vrf": "MGMT", "notification_type": "trap", "version": "v2c", "udp_port": 162, "community_string": "public"}, - ] - }, - "expected": { - "result": "failure", - "messages": ["Host: 192.168.1.100 VRF: default - Incorrect VRF - Actual: MGMT", "Host: 192.168.1.101 VRF: MGMT - Incorrect VRF - Actual: test"], - }, - }, ] From 6346d0ecd064292a1e528447a5f4253a2c309e8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:18:21 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- anta/tests/snmp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index 05587a4ea..a0821248e 100644 --- a/anta/tests/snmp.py +++ b/anta/tests/snmp.py @@ -447,9 +447,7 @@ def test(self) -> None: # If SNMP protocol version is v1 or v2c and actual community string do not matches the expected value, test fails. if version in ["v1", "v2c"] and community_string != (actual_community_string := get_value(host_details, "v1v2cParams.communityString", "Not Found")): - self.result.is_failure( - f"{host} Version: {version} - Incorrect community string - Expected: {community_string} Actual: {actual_community_string}" - ) + self.result.is_failure(f"{host} Version: {version} - Incorrect community string - Expected: {community_string} Actual: {actual_community_string}") # If SNMP protocol version is v3 and actual user do not matches the expected value, test fails. elif version == "v3" and user != (actual_user := get_value(host_details, "v3Params.user", "Not Found")):