Skip to content

ANTA Tests for device configuration

Tests

Module related to the device configuration tests.

VerifyRunningConfig

Atomic support badge Preview badge

Preview

Input models and behavior may change between minor releases without a deprecation notice.

Verifies the running-config against a set of rules.

This test supports exact, substring, and regex matching with optional numeric threshold comparisons. See the examples below for the full range of supported use cases.

Expected Results
  • Success: The test will pass if all rules pass validation.
  • Failure: The test will fail if any rule does not pass validation.
Examples
anta.tests.configuration:
  - VerifyRunningConfig:
      rules:

        # ━━━ TOP-LEVEL COMMANDS ━━━
        # When section is omitted, entries are checked against top-level
        # commands of the running-config.

        # exact (default mode) — command must exist verbatim
        - entries:
            - match: "aaa authorization exec default local"
              description: "AAA authorization"

        # exact + absent — command must NOT exist
        - entries:
            - match: "enable password"
              absent: true
              description: "No static enable password"

        # contains — at least one command must contain this substring
        - entries:
            - match: "ntp server"
              mode: contains
              description: "NTP server configured"

        # contains + absent — no command may contain this substring
        - entries:
            - match: "snmp-server community"
              mode: contains
              absent: true
              description: "No SNMP community strings"

        # regex — at least one command must match this pattern
        - entries:
            - match: "logging host \\S+"
              mode: regex
              description: "Remote syslog configured"

        # regex + absent — no command may match this pattern
        - entries:
            - match: "username .* secret 0 "
              mode: regex
              absent: true
              description: "No plaintext secrets"

        # ━━━ SINGLE SECTION ━━━
        # Target a specific running-config section. Each element uses exact key
        # lookup first, falling back to a regex fullmatch.

        # Multiple entries validated within the same section
        - section: ["management api http-commands"]
          entries:
            - match: "no shutdown"
              description: "eAPI enabled"
            - match: "protocol http"
              mode: contains
              absent: true
              description: "No plaintext HTTP"

        # ━━━ THRESHOLD CHECKS ━━━
        # Compare a captured numeric value against a bound.
        # Requires mode: regex with a capture group.

        # ge — captured value must be >= threshold
        - section: ["interface Ethernet1"]
          entries:
            - match: "mtu (\\d+)"
              mode: regex
              description: "Uplink MTU"
              threshold:
                value: 1500
                operator: ge

        # eq — captured value must equal threshold (default operator)
        - section: ["router bgp \\d+"]
          entries:
            - match: "graceful-restart restart-time (\\d+)"
              mode: regex
              description: "BGP graceful-restart timer"
              threshold:
                value: 300
                operator: eq

        # le — captured value must be <= threshold
        - section: ["router bgp \\d+"]
          entries:
            - match: "maximum-paths (\\d+)"
              mode: regex
              description: "BGP ECMP paths"
              threshold:
                value: 4
                operator: le

        # ━━━ NESTED SECTIONS ━━━
        # Navigate multiple nested sections of the running-config.

        - section: ["management api http-commands", "vrf MGMT"]
          entries:
            - match: "no shutdown"
              description: "eAPI enabled in MGMT VRF"

        - section: ["router bgp \\d+", "address-family evpn"]
          entries:
            - match: "neighbor EVPN-OVERLAY-PEERS activate"

        # ━━━ WILDCARD SECTIONS ━━━
        # Regex patterns matching multiple sections. Each match
        # produces its own independent atomic result.

        # All Ethernet interfaces must have a description
        - section: ["interface Ethernet\\d+"]
          entries:
            - match: "description"
              mode: contains
              description: "Ethernet description"

        # All BGP VRFs must have a router-id
        - section: ["router bgp \\d+", "vrf .*"]
          entries:
            - match: "router-id"
              mode: contains
              description: "BGP VRF router-id"

        # Wildcard + threshold — fleet-wide MTU check
        - section: ["interface Ethernet\\d+$"]
          entries:
            - match: "mtu (\\d+)"
              mode: regex
              description: "Global MTU policy"
              threshold:
                value: 1500
                operator: ge

        # ━━━ SECTION EXISTENCE CHECK ━━━
        # Use entries: [] to verify a section exists without checking commands.

        - section: ["router bgp \\d+"]
          entries: []

Inputs

Name Type Description Default
rules list[ConfigRule]
List of rules to validate. Each rule defines an optional section scope and the entries to verify.
-
Source code in anta/tests/configuration.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
@preview_test_class
class VerifyRunningConfig(AntaTest):
    r"""Verifies the running-config against a set of rules.

    This test supports exact, substring, and regex matching with optional numeric threshold comparisons.
    See the examples below for the full range of supported use cases.

    Expected Results
    ----------------
    * Success: The test will pass if all rules pass validation.
    * Failure: The test will fail if any rule does not pass validation.

    Examples
    --------
    ```yaml
    anta.tests.configuration:
      - VerifyRunningConfig:
          rules:

            # ━━━ TOP-LEVEL COMMANDS ━━━
            # When section is omitted, entries are checked against top-level
            # commands of the running-config.

            # exact (default mode) — command must exist verbatim
            - entries:
                - match: "aaa authorization exec default local"
                  description: "AAA authorization"

            # exact + absent — command must NOT exist
            - entries:
                - match: "enable password"
                  absent: true
                  description: "No static enable password"

            # contains — at least one command must contain this substring
            - entries:
                - match: "ntp server"
                  mode: contains
                  description: "NTP server configured"

            # contains + absent — no command may contain this substring
            - entries:
                - match: "snmp-server community"
                  mode: contains
                  absent: true
                  description: "No SNMP community strings"

            # regex — at least one command must match this pattern
            - entries:
                - match: "logging host \\S+"
                  mode: regex
                  description: "Remote syslog configured"

            # regex + absent — no command may match this pattern
            - entries:
                - match: "username .* secret 0 "
                  mode: regex
                  absent: true
                  description: "No plaintext secrets"

            # ━━━ SINGLE SECTION ━━━
            # Target a specific running-config section. Each element uses exact key
            # lookup first, falling back to a regex fullmatch.

            # Multiple entries validated within the same section
            - section: ["management api http-commands"]
              entries:
                - match: "no shutdown"
                  description: "eAPI enabled"
                - match: "protocol http"
                  mode: contains
                  absent: true
                  description: "No plaintext HTTP"

            # ━━━ THRESHOLD CHECKS ━━━
            # Compare a captured numeric value against a bound.
            # Requires mode: regex with a capture group.

            # ge — captured value must be >= threshold
            - section: ["interface Ethernet1"]
              entries:
                - match: "mtu (\\d+)"
                  mode: regex
                  description: "Uplink MTU"
                  threshold:
                    value: 1500
                    operator: ge

            # eq — captured value must equal threshold (default operator)
            - section: ["router bgp \\d+"]
              entries:
                - match: "graceful-restart restart-time (\\d+)"
                  mode: regex
                  description: "BGP graceful-restart timer"
                  threshold:
                    value: 300
                    operator: eq

            # le — captured value must be <= threshold
            - section: ["router bgp \\d+"]
              entries:
                - match: "maximum-paths (\\d+)"
                  mode: regex
                  description: "BGP ECMP paths"
                  threshold:
                    value: 4
                    operator: le

            # ━━━ NESTED SECTIONS ━━━
            # Navigate multiple nested sections of the running-config.

            - section: ["management api http-commands", "vrf MGMT"]
              entries:
                - match: "no shutdown"
                  description: "eAPI enabled in MGMT VRF"

            - section: ["router bgp \\d+", "address-family evpn"]
              entries:
                - match: "neighbor EVPN-OVERLAY-PEERS activate"

            # ━━━ WILDCARD SECTIONS ━━━
            # Regex patterns matching multiple sections. Each match
            # produces its own independent atomic result.

            # All Ethernet interfaces must have a description
            - section: ["interface Ethernet\\d+"]
              entries:
                - match: "description"
                  mode: contains
                  description: "Ethernet description"

            # All BGP VRFs must have a router-id
            - section: ["router bgp \\d+", "vrf .*"]
              entries:
                - match: "router-id"
                  mode: contains
                  description: "BGP VRF router-id"

            # Wildcard + threshold — fleet-wide MTU check
            - section: ["interface Ethernet\\d+$"]
              entries:
                - match: "mtu (\\d+)"
                  mode: regex
                  description: "Global MTU policy"
                  threshold:
                    value: 1500
                    operator: ge

            # ━━━ SECTION EXISTENCE CHECK ━━━
            # Use entries: [] to verify a section exists without checking commands.

            - section: ["router bgp \\d+"]
              entries: []
    ```
    """

    categories: ClassVar[list[str]] = ["configuration"]
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config", revision=1)]
    _atomic_support: ClassVar[bool] = True

    class Input(AntaTest.Input):
        """Input model for the VerifyRunningConfig test."""

        rules: list[ConfigRule]
        """List of rules to validate. Each rule defines an optional section scope and the entries to verify."""

    @AntaTest.anta_test
    def test(self) -> None:
        """Main test function for VerifyRunningConfig."""
        self.result.is_success()
        output = self.instance_commands[0].json_output["cmds"]
        top_level_cmds = list(output.keys())

        for rule in self.inputs.rules:
            if rule.section is None:
                self._validate_rule(rule, cmds=top_level_cmds, section_path=None)
            else:
                resolved_sections = self._resolve_section_path(output, rule.section)
                if not resolved_sections:
                    # Section missing — one failure atomic is enough; no entries to validate.
                    description = f"Section '{' > '.join(rule.section)}' in the running-config"
                    self.result.add(description=description).is_failure("Not found")
                    continue
                for section_path, cmds in resolved_sections.items():
                    self._validate_rule(rule, cmds=cmds, section_path=section_path)

    def _resolve_section_path(self, config: dict[str, Any], section_patterns: list[str]) -> dict[str, list[str]]:
        """Resolve a section path against the running-config tree.

        Navigates the config tree level by level. Each pattern is matched by exact key lookup first;
        if no exact key exists, `re.fullmatch` is applied against all keys at that level.

        Returns a mapping of resolved path to command list. An empty dict means no section matched.
        """
        pattern, *remaining = section_patterns

        if pattern in config and isinstance(config[pattern], dict):
            # Exact key found; EOS nests sub-commands under a "cmds" key.
            matches = [(pattern, config[pattern].get("cmds", {}))]
        else:
            # Regex fallback; re.fullmatch prevents "Ethernet1" from partially matching "Ethernet11".
            matches = [(key, node.get("cmds", {})) for key, node in config.items() if isinstance(node, dict) and re.fullmatch(pattern, key)]

        results: dict[str, list[str]] = {}
        for key, sub_config in matches:
            if not remaining:
                # Last pattern — we've reached the leaf; collect the commands.
                results[key] = list(sub_config.keys())
            else:
                # More patterns to resolve — recurse deeper and join the paths.
                for child_path, child_cmds in self._resolve_section_path(sub_config, remaining).items():
                    results[f"{key} > {child_path}"] = child_cmds
        return results

    def _validate_rule(self, rule: ConfigRule, cmds: list[str], *, section_path: str | None) -> None:
        """Validate all entries of a rule."""
        for entry in rule.entries:
            description = entry.build_description(section_path)
            atomic_result = self.result.add(description=description, status=AntaTestStatus.SUCCESS)
            self._validate_entry(entry, cmds, atomic_result)

    def _validate_entry(self, entry: ConfigEntry, cmds: list[str], atomic_result: AtomicTestResult) -> None:
        """Validate a single entry against the resolved command list."""
        matched = entry.matches(cmds)

        # Entry must be present but wasn't found.
        if not entry.absent and not matched:
            atomic_result.is_failure("Not found")
            return

        # Entry must be absent but was found.
        if entry.absent and matched:
            atomic_result.is_failure("Expected to not match" if entry.mode == "regex" else "Expected to be absent")
            return

        # Validate threshold if provided.
        self._validate_threshold(entry, matched, atomic_result)

    def _validate_threshold(self, entry: ConfigEntry, matched: list[str], atomic_result: AtomicTestResult) -> None:
        """Validate threshold constraints against matched commands."""
        if entry.threshold is None:
            # Nothing to validate.
            return

        for cmd in matched:
            match_obj = re.search(entry.match, cmd)
            if match_obj and match_obj.groups():
                try:
                    captured = int(match_obj.group(1))
                except (ValueError, TypeError):
                    atomic_result.is_failure(f"Captured value '{match_obj.group(1)}' is not an integer")
                    continue
                if not entry.threshold.evaluate(captured):
                    atomic_result.is_failure(f"Actual: {captured}")

VerifyRunningConfigDiffs

Verifies there is no difference between the running-config and the startup-config.

Expected Results
  • Success: The test will pass if there is no difference between the running-config and the startup-config.
  • Failure: The test will fail if there is a difference between the running-config and the startup-config.
Examples
anta.tests.configuration:
  - VerifyRunningConfigDiffs:
Source code in anta/tests/configuration.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class VerifyRunningConfigDiffs(AntaTest):
    """Verifies there is no difference between the running-config and the startup-config.

    Expected Results
    ----------------
    * Success: The test will pass if there is no difference between the running-config and the startup-config.
    * Failure: The test will fail if there is a difference between the running-config and the startup-config.

    Examples
    --------
    ```yaml
    anta.tests.configuration:
      - VerifyRunningConfigDiffs:
    ```
    """

    categories: ClassVar[list[str]] = ["configuration"]
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show running-config diffs", ofmt="text")]

    @AntaTest.anta_test
    def test(self) -> None:
        """Main test function for VerifyRunningConfigDiffs."""
        command_output = self.instance_commands[0].text_output
        if command_output == "":
            self.result.is_success()
        else:
            self.result.is_failure(command_output)

VerifyRunningConfigLines

Static Badge Static Badge
Replaced with: VerifyRunningConfig

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
anta.tests.configuration:
  - VerifyRunningConfigLines:
      regex_patterns:
        - "^enable password.*$"
        - "bla bla"

Inputs

Name Type Description Default
regex_patterns list[RegexString]
List of regular expressions.
-
Source code in anta/tests/configuration.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@deprecated_test_class(new_tests=["VerifyRunningConfig"], removal_in_version="v2.0.0")
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"
    ```
    """

    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))

VerifyZeroTouch

Verifies ZeroTouch is disabled.

Expected Results
  • Success: The test will pass if ZeroTouch is disabled.
  • Failure: The test will fail if ZeroTouch is enabled.
Examples
anta.tests.configuration:
  - VerifyZeroTouch:
Source code in anta/tests/configuration.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class VerifyZeroTouch(AntaTest):
    """Verifies ZeroTouch is disabled.

    Expected Results
    ----------------
    * Success: The test will pass if ZeroTouch is disabled.
    * Failure: The test will fail if ZeroTouch is enabled.

    Examples
    --------
    ```yaml
    anta.tests.configuration:
      - VerifyZeroTouch:
    ```
    """

    categories: ClassVar[list[str]] = ["configuration"]
    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show zerotouch", revision=1)]

    @AntaTest.anta_test
    def test(self) -> None:
        """Main test function for VerifyZeroTouch."""
        command_output = self.instance_commands[0].json_output
        if command_output["mode"] == "disabled":
            self.result.is_success()
        else:
            self.result.is_failure("ZTP is NOT disabled")

Input models

Module containing input models for configuration tests.

ConfigEntry

A single check to validate within a resolved section or across top-level commands.

Name Type Description Default
description str | None
Optional metadata describing the configuration entry. Used for reporting.
None
match str
String or pattern to match against commands. For regex mode, standard regex syntax applies.
-
mode Literal['exact', 'contains', 'regex']
Match mode: `exact` (verbatim), `contains` (substring), or `regex` (pattern).
'exact'
absent bool
When `True`, the match must NOT be found.
False
threshold Threshold | None
Optional numeric threshold for regex mode. The first capture group is compared against this bound.
None
Source code in anta/input_models/configuration.py
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class ConfigEntry(BaseModel):
    """A single check to validate within a resolved section or across top-level commands."""

    model_config = ConfigDict(extra="forbid")
    description: str | None = None
    """Optional metadata describing the configuration entry. Used for reporting."""
    match: str
    """String or pattern to match against commands. For regex mode, standard regex syntax applies."""
    mode: Literal["exact", "contains", "regex"] = "exact"
    """Match mode: `exact` (verbatim), `contains` (substring), or `regex` (pattern)."""
    absent: bool = False
    """When `True`, the match must NOT be found."""
    threshold: Threshold | None = None
    """Optional numeric threshold for regex mode. The first capture group is compared against this bound."""

    @model_validator(mode="after")
    def validate_entry(self) -> Self:
        """Validate regex syntax for regex mode entries and all threshold constraints."""
        if self.threshold is not None:
            if self.absent:
                msg = "'absent' must be 'false' when 'threshold' is set"
                raise ValueError(msg)
            if self.mode != "regex":
                msg = "'mode' must be 'regex' when 'threshold' is set"
                raise ValueError(msg)
        if self.mode == "regex":
            try:
                compiled = re.compile(self.match)
            except re.error as e:
                msg = f"'match' is not a valid regular expression: {e}"
                raise ValueError(msg) from e
            if self.threshold is not None and compiled.groups == 0:
                msg = "'match' must have a capture group when 'threshold' is set"
                raise ValueError(msg)
        return self

    def matches(self, commands: list[str]) -> list[str]:
        """Return the subset of `commands` that satisfy this entry's match criteria."""
        if self.mode == "exact":
            return [cmd for cmd in commands if cmd == self.match]
        if self.mode == "contains":
            return [cmd for cmd in commands if self.match in cmd]
        # re.search (not fullmatch) — the pattern may appear anywhere within the command string.
        return [cmd for cmd in commands if re.search(self.match, cmd)]

    def build_description(self, section_path: str | None = None) -> str:
        """Build the description from this entry fields and an optional resolved section path."""
        location = f"'{section_path}'" if section_path else "the running-config"

        if self.threshold is not None:
            auto = f"Captured value of '{self.match}' {self.threshold} in {location}"
        elif self.mode == "exact":
            auto = f"Command '{self.match}' in {location}"
        elif self.mode == "contains":
            auto = f"Substring '{self.match}' in {location}"
        else:
            auto = f"Pattern '{self.match}' in {location}"

        return f"{self.description} - {auto}" if self.description else auto

ConfigRule

A rule targeting a scope in the running-config and the entries to validate within it.

Name Type Description Default
section Annotated[list[Annotated[RegexString, Field(min_length=1)]], Field(min_length=1)] | None
Optional section path to scope into before validating. Each element matches by exact key, then `re.fullmatch` fallback.
None
entries list[ConfigEntry]
Entries to validate within the resolved scope.
-
Source code in anta/input_models/configuration.py
108
109
110
111
112
113
114
115
class ConfigRule(BaseModel):
    """A rule targeting a scope in the running-config and the entries to validate within it."""

    model_config = ConfigDict(extra="forbid")
    section: Annotated[list[Annotated[RegexString, Field(min_length=1)]], Field(min_length=1)] | None = None
    """Optional section path to scope into before validating. Each element matches by exact key, then `re.fullmatch` fallback."""
    entries: list[ConfigEntry]
    """Entries to validate within the resolved scope."""

Threshold

Numeric threshold for a regex-mode rule entry.

Name Type Description Default
value int
The bound to compare the captured value against.
-
operator Literal['le', 'ge', 'eq']
Comparison operator.
'eq'
Source code in anta/input_models/configuration.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Threshold(BaseModel):
    """Numeric threshold for a regex-mode rule entry."""

    model_config = ConfigDict(extra="forbid")
    value: int
    """The bound to compare the captured value against."""
    operator: Literal["le", "ge", "eq"] = "eq"
    """Comparison operator."""

    def evaluate(self, captured: int) -> bool:
        """Return True if `captured` satisfies this threshold constraint."""
        if self.operator == "le":
            return captured <= self.value
        if self.operator == "ge":
            return captured >= self.value
        return captured == self.value

    def __str__(self) -> str:
        """Return a human-readable string representation of the Threshold for reporting."""
        operators = {"le": f"<= {self.value}", "ge": f">= {self.value}", "eq": f"== {self.value}"}
        return operators[self.operator]