Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anta): Added the test case to Verify SNMP Notification Host #838

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions anta/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,4 @@ 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"]
SnmpVersion = Literal["v1", "v2c", "v3"]
135 changes: 132 additions & 3 deletions anta/tests/snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
# mypy: disable-error-code=attr-defined
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar
from ipaddress import IPv4Address
from typing import TYPE_CHECKING, ClassVar, Literal

from anta.custom_types import PositiveInteger
from pydantic import BaseModel, model_validator

from anta.custom_types import Port, PositiveInteger, 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
Expand Down Expand Up @@ -237,3 +240,129 @@ def test(self) -> None:
self.result.is_failure(f"Expected `{self.inputs.contact}` as the contact, but found `{contact}` instead.")
else:
self.result.is_success()


class VerifySNMPNotificationHost(AntaTest):
"""Verifies the SNMP notification host (SNMP manager) configurations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have more brief description about what configuration we are verifying

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated docstring.


- 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)
14 changes: 14 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,20 @@ anta.tests.snmp:
location: New York
- VerifySnmpContact:
contact: [email protected]
- VerifySNMPNotificationHost:
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

anta.tests.software:
- VerifyEOSVersion:
Expand Down
118 changes: 117 additions & 1 deletion tests/units/anta_tests/test_snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from typing import Any

from anta.tests.snmp import VerifySnmpContact, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, VerifySnmpLocation, VerifySnmpStatus
from anta.tests.snmp import VerifySnmpContact, VerifySnmpIPv4Acl, VerifySnmpIPv6Acl, VerifySnmpLocation, VerifySNMPNotificationHost, VerifySnmpStatus
from tests.units.anta_tests import test

DATA: list[dict[str, Any]] = [
Expand Down Expand Up @@ -152,4 +152,120 @@
"messages": ["SNMP contact is not configured."],
},
},
{
"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"
],
},
},
]
Loading