diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py index 680d617c0..bd7350e45 100644 --- a/anta/input_models/snmp.py +++ b/anta/input_models/snmp.py @@ -5,9 +5,30 @@ from __future__ import annotations +from ipaddress import IPv4Address + from pydantic import BaseModel, ConfigDict -from anta.custom_types import SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion +from anta.custom_types import Hostname, SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion + + +class SnmpHost(BaseModel): + """Model for a SNMP host.""" + + model_config = ConfigDict(extra="forbid") + hostname: IPv4Address | Hostname + """IPv4 address or hostname of the SNMP notification host.""" + vrf: str = "default" + """Optional VRF for SNMP hosts. If not provided, it defaults to `default`.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the SnmpHost for reporting. + + Examples + -------- + - Host: 192.168.1.100 VRF: default + """ + return f"Host: {self.hostname} VRF: {self.vrf}" class SnmpUser(BaseModel): diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py index ef7410430..910d5929c 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 SnmpUser +from anta.input_models.snmp import SnmpHost, SnmpUser from anta.models import AntaCommand, AntaTest from anta.tools import get_value @@ -344,6 +344,77 @@ def test(self) -> None: 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 VerifySnmpHostLogging(AntaTest): + """Verifies SNMP logging configurations. + + This test performs the following checks: + + 1. SNMP logging is enabled globally. + 2. For each specified SNMP host: + - Host exists in configuration. + - Host's VRF assignment matches expected value. + + Expected Results + ---------------- + * Success: The test will pass if all of the following conditions are met: + - SNMP logging is enabled on the device. + - All specified hosts are configured with correct VRF assignments. + * Failure: The test will fail if any of the following conditions is met: + - SNMP logging is disabled on the device. + - SNMP host not found in configuration. + - Host's VRF assignment doesn't match expected value. + + Examples + -------- + ```yaml + anta.tests.snmp: + - VerifySnmpHostLogging: + hosts: + - hostname: 192.168.1.100 + vrf: default + - hostname: 192.168.1.103 + vrf: MGMT + ``` + """ + + categories: ClassVar[list[str]] = ["snmp"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show snmp", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifySnmpHostLogging test.""" + + hosts: list[SnmpHost] + """List of SNMP hosts.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifySnmpHostLogging.""" + self.result.is_success() + + command_output = self.instance_commands[0].json_output.get("logging", {}) + # If SNMP logging is disabled, test fails. + if not command_output.get("loggingEnabled"): + self.result.is_failure("SNMP logging is disabled") + return + + host_details = command_output.get("hosts", {}) + + for host in self.inputs.hosts: + hostname = str(host.hostname) + vrf = host.vrf + actual_snmp_host = host_details.get(hostname, {}) + + # If SNMP host is not configured on the device, test fails. + if not actual_snmp_host: + self.result.is_failure(f"{host} - Not configured") + continue + + # If VRF is not matches the expected value, test fails. + actual_vrf = "default" if (vrf_name := actual_snmp_host.get("vrf")) == "" else vrf_name + if actual_vrf != vrf: + self.result.is_failure(f"{host} - Incorrect VRF - Actual: {actual_vrf}") + + class VerifySnmpUser(AntaTest): """Verifies the SNMP user configurations. diff --git a/examples/tests.yaml b/examples/tests.yaml index db1d179d6..387d3bb0a 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -727,6 +727,13 @@ anta.tests.snmp: # Verifies the SNMP error counters. error_counters: - inVersionErrs + - VerifySnmpHostLogging: + # Verifies SNMP logging configurations. + hosts: + - hostname: 192.168.1.100 + vrf: default + - hostname: 192.168.1.103 + vrf: MGMT - VerifySnmpIPv4Acl: # Verifies if the SNMP agent has IPv4 ACL(s) configured. number: 3 diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py index d2eb6b87f..ebbf94897 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, + VerifySnmpHostLogging, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, VerifySnmpLocation, @@ -320,6 +321,57 @@ ], }, }, + { + "name": "success", + "test": VerifySnmpHostLogging, + "eos_data": [ + { + "logging": { + "loggingEnabled": True, + "hosts": { + "192.168.1.100": {"port": 162, "vrf": ""}, + "192.168.1.101": {"port": 162, "vrf": "MGMT"}, + "snmp-server-01": {"port": 162, "vrf": "default"}, + }, + } + } + ], + "inputs": { + "hosts": [ + {"hostname": "192.168.1.100", "vrf": "default"}, + {"hostname": "192.168.1.101", "vrf": "MGMT"}, + {"hostname": "snmp-server-01", "vrf": "default"}, + ] + }, + "expected": {"result": "success"}, + }, + { + "name": "failure-logging-disabled", + "test": VerifySnmpHostLogging, + "eos_data": [{"logging": {"loggingEnabled": False}}], + "inputs": {"hosts": [{"hostname": "192.168.1.100", "vrf": "default"}, {"hostname": "192.168.1.101", "vrf": "MGMT"}]}, + "expected": {"result": "failure", "messages": ["SNMP logging is disabled"]}, + }, + { + "name": "failure-mismatch-vrf", + "test": VerifySnmpHostLogging, + "eos_data": [{"logging": {"loggingEnabled": True, "hosts": {"192.168.1.100": {"port": 162, "vrf": "MGMT"}, "192.168.1.101": {"port": 162, "vrf": "Test"}}}}], + "inputs": {"hosts": [{"hostname": "192.168.1.100", "vrf": "default"}, {"hostname": "192.168.1.101", "vrf": "MGMT"}]}, + "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"], + }, + }, + { + "name": "failure-host-not-configured", + "test": VerifySnmpHostLogging, + "eos_data": [{"logging": {"loggingEnabled": True, "hosts": {"192.168.1.100": {"port": 162, "vrf": "MGMT"}, "192.168.1.103": {"port": 162, "vrf": "Test"}}}}], + "inputs": {"hosts": [{"hostname": "192.168.1.101", "vrf": "default"}, {"hostname": "192.168.1.102", "vrf": "MGMT"}]}, + "expected": { + "result": "failure", + "messages": ["Host: 192.168.1.101 VRF: default - Not configured", "Host: 192.168.1.102 VRF: MGMT - Not configured"], + }, + }, { "name": "success", "test": VerifySnmpUser,