diff --git a/anta/custom_types.py b/anta/custom_types.py index 5de7a612c..e8b054e01 100644 --- a/anta/custom_types.py +++ b/anta/custom_types.py @@ -81,6 +81,16 @@ def bgp_multiprotocol_capabilities_abbreviations(value: str) -> str: return value +def validate_regex(value: str) -> str: + """Validate that the input value is a valid regex format.""" + try: + re.compile(value) + except re.error as e: + msg = f"Invalid regex: {e}" + raise ValueError(msg) from e + return value + + # ANTA framework TestStatus = Literal["unset", "success", "failure", "error", "skipped"] @@ -129,3 +139,4 @@ def bgp_multiprotocol_capabilities_abbreviations(value: str) -> str: Revision = Annotated[int, Field(ge=1, le=99)] Hostname = Annotated[str, Field(pattern=r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")] Port = Annotated[int, Field(ge=1, le=65535)] +RegexString = Annotated[str, AfterValidator(validate_regex)] diff --git a/anta/tests/configuration.py b/anta/tests/configuration.py index 7f3f0bde1..4a1bd31d1 100644 --- a/anta/tests/configuration.py +++ b/anta/tests/configuration.py @@ -7,8 +7,10 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations +import re from typing import TYPE_CHECKING, ClassVar +from anta.custom_types import RegexString from anta.models import AntaCommand, AntaTest if TYPE_CHECKING: @@ -75,3 +77,57 @@ def test(self) -> None: self.result.is_success() else: self.result.is_failure(command_output) + + +class VerifyRunningConfigLines(AntaTest): + """Verifies the given regular expression patterns are present in the running-config. + + !!! warning + Since this uses regular expression searches on the whole running-config, it can + drastically impact performance and should only be used if no other test is available. + + If possible, try using another ANTA test that is more specific. + + Expected Results + ---------------- + * Success: The test will pass if all the patterns are found in the running-config. + * Failure: The test will fail if any of the patterns are NOT found in the running-config. + + Examples + -------- + ```yaml + anta.tests.configuration: + - VerifyRunningConfigLines: + regex_patterns: + - "^enable password.*$" + - "bla bla" + ``` + """ + + name = "VerifyRunningConfigLines" + description = "Search the Running-Config for the given RegEx patterns." + categories: ClassVar[list[str]] = ["configuration"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config", ofmt="text")] + + class Input(AntaTest.Input): + """Input model for the VerifyRunningConfigLines test.""" + + regex_patterns: list[RegexString] + """List of regular expressions.""" + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyRunningConfigLines.""" + failure_msgs = [] + command_output = self.instance_commands[0].text_output + + for pattern in self.inputs.regex_patterns: + re_search = re.compile(pattern, flags=re.MULTILINE) + + if not re_search.search(command_output): + failure_msgs.append(f"'{pattern}'") + + if not failure_msgs: + self.result.is_success() + else: + self.result.is_failure("Following patterns were not found: " + ",".join(failure_msgs)) diff --git a/tests/units/anta_tests/test_configuration.py b/tests/units/anta_tests/test_configuration.py index 0444db6f5..7f198a33c 100644 --- a/tests/units/anta_tests/test_configuration.py +++ b/tests/units/anta_tests/test_configuration.py @@ -7,7 +7,7 @@ from typing import Any -from anta.tests.configuration import VerifyRunningConfigDiffs, VerifyZeroTouch +from anta.tests.configuration import VerifyRunningConfigDiffs, VerifyRunningConfigLines, VerifyZeroTouch from tests.lib.anta import test # noqa: F401; pylint: disable=W0611 DATA: list[dict[str, Any]] = [ @@ -32,5 +32,42 @@ "inputs": None, "expected": {"result": "success"}, }, - {"name": "failure", "test": VerifyRunningConfigDiffs, "eos_data": ["blah blah"], "inputs": None, "expected": {"result": "failure", "messages": ["blah blah"]}}, + { + "name": "failure", + "test": VerifyRunningConfigDiffs, + "eos_data": ["blah blah"], + "inputs": None, + "expected": {"result": "failure", "messages": ["blah blah"]}, + }, + { + "name": "success", + "test": VerifyRunningConfigLines, + "eos_data": ["blah blah"], + "inputs": {"regex_patterns": ["blah"]}, + "expected": {"result": "success"}, + }, + { + "name": "success", + "test": VerifyRunningConfigLines, + "eos_data": ["enable password something\nsome other line"], + "inputs": {"regex_patterns": ["^enable password .*$", "^.*other line$"]}, + "expected": {"result": "success"}, + }, + { + "name": "failure", + "test": VerifyRunningConfigLines, + "eos_data": ["enable password something\nsome other line"], + "inputs": {"regex_patterns": ["bla", "bleh"]}, + "expected": {"result": "failure", "messages": ["Following patterns were not found: 'bla','bleh'"]}, + }, + { + "name": "failure-invalid-regex", + "test": VerifyRunningConfigLines, + "eos_data": ["enable password something\nsome other line"], + "inputs": {"regex_patterns": ["["]}, + "expected": { + "result": "error", + "messages": ["1 validation error for Input\nregex_patterns.0\n Value error, Invalid regex: unterminated character set at position 0"], + }, + }, ]