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.tests): Added testcase to verify DNS ip name server #537

Merged
merged 4 commits into from
Feb 23, 2024
Merged
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
59 changes: 58 additions & 1 deletion anta/tests/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
"""
from __future__ import annotations

from typing import List
from ipaddress import IPv4Address, IPv6Address
from typing import List, Union

from pydantic import BaseModel, Field

from anta.models import AntaCommand, AntaTemplate, AntaTest
from anta.tools.get_dict_superset import get_dict_superset
from anta.tools.get_item import get_item

# Mypy does not understand AntaTest.Input typing
# mypy: disable-error-code=attr-defined
Expand Down Expand Up @@ -78,3 +83,55 @@ def test(self) -> None:
failed_domains.append(domain)
if failed_domains:
self.result.is_failure(f"The following domain(s) are not resolved to an IP address: {', '.join(failed_domains)}")


class VerifyDNSServers(AntaTest):
"""
Verifies if the DNS (Domain Name Service) servers are correctly configured.

Expected Results:
* success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.
* failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.
"""

name = "VerifyDNSServers"
description = "Verifies if the DNS servers are correctly configured."
categories = ["services"]
commands = [AntaCommand(command="show ip name-server")]

class Input(AntaTest.Input):
"""Inputs for the VerifyDNSServers test."""

dns_servers: List[DnsServers]
"""List of DNS servers to verify."""

class DnsServers(BaseModel):
"""DNS server details"""

server_address: Union[IPv4Address, IPv6Address]
"""The IPv4/IPv6 address of the DNS server."""
vrf: str = "default"
"""The VRF for the DNS server. Defaults to 'default' if not provided."""
priority: int = Field(ge=0, le=4)
"""The priority of the DNS server from 0 to 4, lower is first."""

@AntaTest.anta_test
def test(self) -> None:
command_output = self.instance_commands[0].json_output["nameServerConfigs"]
self.result.is_success()
for server in self.inputs.dns_servers:
address = str(server.server_address)
vrf = server.vrf
priority = server.priority
input_dict = {"ipAddr": address, "vrf": vrf}

if get_item(command_output, "ipAddr", address) is None:
self.result.is_failure(f"DNS server `{address}` is not configured with any VRF.")
continue
carl-baillargeon marked this conversation as resolved.
Show resolved Hide resolved

if (output := get_dict_superset(command_output, input_dict)) is None:
self.result.is_failure(f"DNS server `{address}` is not configured with VRF `{vrf}`.")
continue

if output["priority"] != priority:
self.result.is_failure(f"For DNS server `{address}`, the expected priority is `{priority}`, but `{output['priority']}` was found instead.")
64 changes: 64 additions & 0 deletions anta/tools/get_dict_superset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.

"""Get one dictionary from a list of dictionaries by matching the given key and values."""
from __future__ import annotations

from typing import Any, Optional


def get_dict_superset(
list_of_dicts: list[dict[Any, Any]],
input_dict: dict[Any, Any],
default: Optional[Any] = None,
required: bool = False,
var_name: Optional[str] = None,
custom_error_msg: Optional[str] = None,
) -> Any:
"""Get the first dictionary from a list of dictionaries that is a superset of the input dict.

Returns the supplied default value or None if there is no match and "required" is False.

Will return the first matching item if there are multiple matching items.

Parameters
----------
list_of_dicts: list(dict)
List of Dictionaries to get list items from
input_dict : dict
Dictionary to check subset with a list of dict
default: any
Default value returned if the key and value are not found
required: bool
Fail if there is no match
var_name : str
String used for raising an exception with the full variable name
custom_error_msg : str
Custom error message to raise when required is True and the value is not found

Returns
-------
any
Dict or default value

Raises
------
ValueError
If the keys and values are not found and "required" == True
"""
if not isinstance(list_of_dicts, list) or not list_of_dicts or not isinstance(input_dict, dict) or not input_dict:
if required:
error_msg = custom_error_msg or f"{var_name} not found in the provided list."
raise ValueError(error_msg)
return default

for list_item in list_of_dicts:
if isinstance(list_item, dict) and input_dict.items() <= list_item.items():
return list_item

if required:
error_msg = custom_error_msg or f"{var_name} not found in the provided list."
raise ValueError(error_msg)

return default
8 changes: 8 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ anta.tests.services:
- arista.com
- www.google.com
- arista.ca
- VerifyDNSServers:
dns_servers:
- server_address: 10.14.0.1
vrf: default
priority: 1
- server_address: 10.14.0.11
vrf: MGMT
priority: 0

anta.tests.snmp:
- VerifySnmpStatus:
Expand Down
71 changes: 70 additions & 1 deletion tests/units/anta_tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from typing import Any

from anta.tests.services import VerifyDNSLookup, VerifyHostname
from anta.tests.services import VerifyDNSLookup, VerifyDNSServers, VerifyHostname
from tests.lib.anta import test # noqa: F401; pylint: disable=W0611

DATA: list[dict[str, Any]] = [
Expand Down Expand Up @@ -55,4 +55,73 @@
"inputs": {"domain_names": ["arista.ca", "www.google.com", "google.ca"]},
"expected": {"result": "failure", "messages": ["The following domain(s) are not resolved to an IP address: arista.ca, google.ca"]},
},
{
"name": "success",
"test": VerifyDNSServers,
"eos_data": [
{
"nameServerConfigs": [{"ipAddr": "10.14.0.1", "vrf": "default", "priority": 0}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}],
}
],
"inputs": {
"dns_servers": [{"server_address": "10.14.0.1", "vrf": "default", "priority": 0}, {"server_address": "10.14.0.11", "vrf": "MGMT", "priority": 1}]
},
"expected": {"result": "success"},
},
{
"name": "failure-dns-missing",
"test": VerifyDNSServers,
"eos_data": [
{
"nameServerConfigs": [{"ipAddr": "10.14.0.1", "vrf": "default", "priority": 0}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}],
}
],
"inputs": {
"dns_servers": [{"server_address": "10.14.0.10", "vrf": "default", "priority": 0}, {"server_address": "10.14.0.21", "vrf": "MGMT", "priority": 1}]
},
"expected": {
"result": "failure",
"messages": ["DNS server `10.14.0.10` is not configured with any VRF.", "DNS server `10.14.0.21` is not configured with any VRF."],
},
},
{
"name": "failure-no-dns-found",
"test": VerifyDNSServers,
"eos_data": [
{
"nameServerConfigs": [],
}
],
"inputs": {
"dns_servers": [{"server_address": "10.14.0.10", "vrf": "default", "priority": 0}, {"server_address": "10.14.0.21", "vrf": "MGMT", "priority": 1}]
},
"expected": {
"result": "failure",
"messages": ["DNS server `10.14.0.10` is not configured with any VRF.", "DNS server `10.14.0.21` is not configured with any VRF."],
},
},
{
"name": "failure-incorrect-dns-details",
"test": VerifyDNSServers,
"eos_data": [
{
"nameServerConfigs": [{"ipAddr": "10.14.0.1", "vrf": "CS", "priority": 1}, {"ipAddr": "10.14.0.11", "vrf": "MGMT", "priority": 1}],
}
],
"inputs": {
"dns_servers": [
{"server_address": "10.14.0.1", "vrf": "CS", "priority": 0},
{"server_address": "10.14.0.11", "vrf": "default", "priority": 0},
{"server_address": "10.14.0.110", "vrf": "MGMT", "priority": 0},
]
},
"expected": {
"result": "failure",
"messages": [
"For DNS server `10.14.0.1`, the expected priority is `0`, but `1` was found instead.",
"DNS server `10.14.0.11` is not configured with VRF `default`.",
"DNS server `10.14.0.110` is not configured with any VRF.",
],
},
},
]
149 changes: 149 additions & 0 deletions tests/units/tools/test_get_dict_superset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright (c) 2023-2024 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.tools.get_dict_superset`."""
from __future__ import annotations

from contextlib import nullcontext as does_not_raise
from typing import Any

import pytest

from anta.tools.get_dict_superset import get_dict_superset

# pylint: disable=duplicate-code
DUMMY_DATA = [
("id", 0),
{
"id": 1,
"name": "Alice",
"age": 30,
"email": "alice@example.com",
},
{
"id": 2,
"name": "Bob",
"age": 35,
"email": "bob@example.com",
},
{
"id": 3,
"name": "Charlie",
"age": 40,
"email": "charlie@example.com",
},
]


@pytest.mark.parametrize(
"list_of_dicts, input_dict, default, required, var_name, custom_error_msg, expected_result, expected_raise",
[
pytest.param([], {"id": 1, "name": "Alice"}, None, False, None, None, None, does_not_raise(), id="empty list"),
pytest.param(
[],
{"id": 1, "name": "Alice"},
None,
True,
None,
None,
None,
pytest.raises(ValueError, match="not found in the provided list."),
id="empty list and required",
),
pytest.param(DUMMY_DATA, {"id": 10, "name": "Jack"}, None, False, None, None, None, does_not_raise(), id="missing item"),
pytest.param(DUMMY_DATA, {"id": 1, "name": "Alice"}, None, False, None, None, DUMMY_DATA[1], does_not_raise(), id="found item"),
pytest.param(DUMMY_DATA, {"id": 10, "name": "Jack"}, "default_value", False, None, None, "default_value", does_not_raise(), id="default value"),
pytest.param(
DUMMY_DATA, {"id": 10, "name": "Jack"}, None, True, None, None, None, pytest.raises(ValueError, match="not found in the provided list."), id="required"
),
pytest.param(
DUMMY_DATA,
{"id": 10, "name": "Jack"},
None,
True,
"custom_var_name",
None,
None,
pytest.raises(ValueError, match="custom_var_name not found in the provided list."),
id="custom var_name",
),
pytest.param(
DUMMY_DATA, {"id": 1, "name": "Alice"}, None, True, "custom_var_name", "Custom error message", DUMMY_DATA[1], does_not_raise(), id="custom error message"
),
pytest.param(
DUMMY_DATA,
{"id": 10, "name": "Jack"},
None,
True,
"custom_var_name",
"Custom error message",
None,
pytest.raises(ValueError, match="Custom error message"),
id="custom error message and required",
),
pytest.param(DUMMY_DATA, {"id": 1, "name": "Jack"}, None, False, None, None, None, does_not_raise(), id="id ok but name not ok"),
pytest.param(
"not a list",
{"id": 1, "name": "Alice"},
None,
True,
None,
None,
None,
pytest.raises(ValueError, match="not found in the provided list."),
id="non-list input for list_of_dicts",
),
pytest.param(
DUMMY_DATA, "not a dict", None, True, None, None, None, pytest.raises(ValueError, match="not found in the provided list."), id="non-dictionary input"
),
pytest.param(DUMMY_DATA, {}, None, False, None, None, None, does_not_raise(), id="empty dictionary input"),
pytest.param(
DUMMY_DATA,
{"id": 1, "name": "Alice", "extra_key": "extra_value"},
None,
True,
None,
None,
None,
pytest.raises(ValueError, match="not found in the provided list."),
id="input dictionary with extra keys",
),
pytest.param(
DUMMY_DATA,
{"id": 1},
None,
False,
None,
None,
DUMMY_DATA[1],
does_not_raise(),
id="input dictionary is a subset of more than one dictionary in list_of_dicts",
),
pytest.param(
DUMMY_DATA,
{"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com", "extra_key": "extra_value"},
None,
True,
None,
None,
None,
pytest.raises(ValueError, match="not found in the provided list."),
id="input dictionary is a superset of a dictionary in list_of_dicts",
),
],
)
def test_get_dict_superset(
list_of_dicts: list[dict[Any, Any]],
input_dict: Any,
default: Any | None,
required: bool,
var_name: str | None,
custom_error_msg: str | None,
expected_result: str,
expected_raise: Any,
) -> None:
"""Test get_dict_superset."""
# pylint: disable=too-many-arguments
with expected_raise:
assert get_dict_superset(list_of_dicts, input_dict, default, required, var_name, custom_error_msg) == expected_result
Loading