Repository: aristanetworks/anta (python)

Issue:
Add the test case to verify the SNMP source interface for specified VRF
Sample output:

DLFW1#show snmp
Chassis: SN-DLFW1
**SNMP source interfaces:
   $SNMP_SRC_INT in VRF MGMTMGMT**

Pull request description:
feat(anta): Added the test case to verify the SNMP source interface for specified VRF
# Description

Verifies SNMP source-interface for a specified VRF.

    Expected Results
    ----------------
    * Success: The test will pass if the provided SNMP source-interface is configured in the specified VRF.
    * Failure: The test will fail if the provided SNMP source-interface is NOT configured in the specified VRF.

Fixes #854 

# Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [X] I have run pre-commit for code linting and typing (`pre-commit run`)
- [x] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes (`tox -e testenv`)


Patch:
diff --git a/anta/input_models/snmp.py b/anta/input_models/snmp.py
index bd7350e4..d408d931 100644
--- a/anta/input_models/snmp.py
+++ b/anta/input_models/snmp.py
@@ -9,7 +9,7 @@ from ipaddress import IPv4Address
 
 from pydantic import BaseModel, ConfigDict
 
-from anta.custom_types import Hostname, SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion
+from anta.custom_types import Hostname, Interface, SnmpEncryptionAlgorithm, SnmpHashingAlgorithm, SnmpVersion
 
 
 class SnmpHost(BaseModel):
@@ -54,3 +54,21 @@ class SnmpUser(BaseModel):
         - User: Test Group: Test_Group Version: v2c
         """
         return f"User: {self.username} Group: {self.group_name} Version: {self.version}"
+
+
+class SnmpSourceInterface(BaseModel):
+    """Model for a SNMP source-interface."""
+
+    interface: Interface
+    """Interface to use as the source IP address of SNMP messages."""
+    vrf: str = "default"
+    """VRF of the source interface."""
+
+    def __str__(self) -> str:
+        """Return a human-readable string representation of the SnmpSourceInterface for reporting.
+
+        Examples
+        --------
+        - Source Interface: Ethernet1 VRF: default
+        """
+        return f"Source Interface: {self.interface} VRF: {self.vrf}"


Test patch:
diff --git a/anta/tests/snmp.py b/anta/tests/snmp.py
index 910d5929..84c5470e 100644
--- a/anta/tests/snmp.py
+++ b/anta/tests/snmp.py
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, ClassVar, get_args
 from pydantic import field_validator
 
 from anta.custom_types import PositiveInteger, SnmpErrorCounter, SnmpPdu
-from anta.input_models.snmp import SnmpHost, SnmpUser
+from anta.input_models.snmp import SnmpHost, SnmpSourceInterface, SnmpUser
 from anta.models import AntaCommand, AntaTest
 from anta.tools import get_value
 
@@ -489,3 +489,57 @@ class VerifySnmpUser(AntaTest):
 
                 if user.priv_type and (act_encryption := get_value(user_details, "v3Params.privType", "Not Found")) != user.priv_type:
                     self.result.is_failure(f"{user} - Incorrect privacy type - Expected: {user.priv_type} Actual: {act_encryption}")
+
+
+class VerifySnmpSourceInterface(AntaTest):
+    """Verifies SNMP source interfaces.
+
+    This test performs the following checks:
+
+      1. Verifies that source interface(s) are configured for SNMP.
+      2. For each specified source interface:
+          - Interface is configured in the specified VRF.
+
+    Expected Results
+    ----------------
+    * Success: The test will pass if the provided SNMP source interface(s) are configured in their specified VRF.
+    * Failure: The test will fail if any of the provided SNMP source interface(s) are NOT configured in their specified VRF.
+
+    Examples
+    --------
+    ```yaml
+    anta.tests.snmp:
+      - VerifySnmpSourceInterface:
+          interfaces:
+            - interface: Ethernet1
+              vrf: default
+            - interface: Management0
+              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 VerifySnmpSourceInterface test."""
+
+        interfaces: list[SnmpSourceInterface]
+        """List of source interfaces."""
+
+    @AntaTest.anta_test
+    def test(self) -> None:
+        """Main test function for VerifySnmpSourceInterface."""
+        self.result.is_success()
+        command_output = self.instance_commands[0].json_output.get("srcIntf", {})
+
+        if not (interface_output := command_output.get("sourceInterfaces")):
+            self.result.is_failure("SNMP source interface(s) not configured")
+            return
+
+        for interface_details in self.inputs.interfaces:
+            # If the source interface is not configured, or if it does not match the expected value, the test fails.
+            if not (actual_interface := interface_output.get(interface_details.vrf)):
+                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}")
diff --git a/examples/tests.yaml b/examples/tests.yaml
index f47285a1..6c64f5d9 100644
--- a/examples/tests.yaml
+++ b/examples/tests.yaml
@@ -793,6 +793,13 @@ anta.tests.snmp:
       pdus:
         - outTrapPdus
         - inGetNextPdus
+  - VerifySnmpSourceInterface:
+      # Verifies SNMP source interfaces.
+      interfaces:
+        - interface: Ethernet1
+          vrf: default
+        - interface: Management0
+          vrf: MGMT
   - VerifySnmpStatus:
       # Verifies if the SNMP agent is enabled.
       vrf: default
diff --git a/tests/units/anta_tests/test_snmp.py b/tests/units/anta_tests/test_snmp.py
index ebbf9489..fc30ad6c 100644
--- a/tests/units/anta_tests/test_snmp.py
+++ b/tests/units/anta_tests/test_snmp.py
@@ -15,6 +15,7 @@ from anta.tests.snmp import (
     VerifySnmpIPv6Acl,
     VerifySnmpLocation,
     VerifySnmpPDUCounters,
+    VerifySnmpSourceInterface,
     VerifySnmpStatus,
     VerifySnmpUser,
 )
@@ -536,4 +537,47 @@ DATA: list[dict[str, Any]] = [
             ],
         },
     },
+    {
+        "name": "success",
+        "test": VerifySnmpSourceInterface,
+        "eos_data": [
+            {
+                "srcIntf": {"sourceInterfaces": {"default": "Ethernet1", "MGMT": "Management0"}},
+            }
+        ],
+        "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]},
+        "expected": {"result": "success"},
+    },
+    {
+        "name": "failure-not-configured",
+        "test": VerifySnmpSourceInterface,
+        "eos_data": [
+            {
+                "srcIntf": {},
+            }
+        ],
+        "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]},
+        "expected": {"result": "failure", "messages": ["SNMP source interface(s) not configured"]},
+    },
+    {
+        "name": "failure-incorrect-interfaces",
+        "test": VerifySnmpSourceInterface,
+        "eos_data": [
+            {
+                "srcIntf": {
+                    "sourceInterfaces": {
+                        "default": "Management0",
+                    }
+                },
+            }
+        ],
+        "inputs": {"interfaces": [{"interface": "Ethernet1", "vrf": "default"}, {"interface": "Management0", "vrf": "MGMT"}]},
+        "expected": {
+            "result": "failure",
+            "messages": [
+                "Source Interface: Ethernet1 VRF: default - Incorrect source interface - Actual: Management0",
+                "Source Interface: Management0 VRF: MGMT - Not configured",
+            ],
+        },
+    },
 ]