From 5c49babcb4b0d19eb0021ad12cc1baf06b291d53 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 10 Dec 2024 13:12:34 -0500 Subject: [PATCH 01/13] Refactor isis module tests --- anta/input_models/routing/isis.py | 124 +++++++++ anta/tests/routing/isis.py | 276 ++++++-------------- tests/units/anta_tests/routing/test_isis.py | 36 ++- 3 files changed, 223 insertions(+), 213 deletions(-) create mode 100644 anta/input_models/routing/isis.py diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py new file mode 100644 index 000000000..eaba9bb25 --- /dev/null +++ b/anta/input_models/routing/isis.py @@ -0,0 +1,124 @@ +# 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. +"""Module containing input models for routing ISIS tests.""" + +from __future__ import annotations + +from ipaddress import IPv4Address +from typing import Any, Literal +from warnings import warn + +from pydantic import BaseModel, ConfigDict + +from anta.custom_types import Interface + + +class ISISInstance(BaseModel): + """Model for a ISIS instance.""" + + model_config = ConfigDict(extra="forbid") + name: str + """ISIS instance name.""" + vrf: str = "default" + """VRF name where ISIS instance is configured.""" + dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" + """Configured dataplane for the instance.""" + segments: list[Segment] | None = None + """List of ISIS segments""" + + def __str__(self) -> str: + """Return a human-readable string representation of the ISISInstance for reporting.""" + return f"Instance: {self.name} VRF: {self.vrf}" + + +class Segment(BaseModel): + """Segment model definition.""" + + model_config = ConfigDict(extra="forbid") + interface: Interface + """Interface name to check.""" + level: Literal[1, 2] = 2 + """ISIS level configured for interface. Default is 2.""" + sid_origin: Literal["dynamic"] = "dynamic" + "Specifies the origin of the Segment ID." + address: IPv4Address + """IP address of the remote end of the segment(segment endpoint).""" + + def __str__(self) -> str: + """Return a human-readable string representation of the Segment for reporting.""" + return f"Interface: {self.interface} Endpoint: {self.address}" + + +class ISISInterface(BaseModel): + """Model for a ISIS Interface.""" + + model_config = ConfigDict(extra="forbid") + name: Interface + """Interface name to check.""" + vrf: str = "default" + """VRF name where ISIS instance is configured.""" + level: Literal[1, 2] = 2 + """ISIS level (1 or 2) configured for the interface. Default is 2.""" + count: int | None = None + """The total number of IS-IS neighbors associated with interface.""" + mode: Literal["point-to-point", "broadcast", "passive"] | None = None + """The operational mode of the IS-IS interface.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the ISISInterface for reporting.""" + return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level if self.level else 'IS Type(1-2)'}" + + +class InterfaceCount(ISISInterface): # pragma: no cover + """Alias for the ISISInterface model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInterface model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the ISISInterface class, emitting a deprecation warning.""" + warn( + message="InterfaceCount model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) + + +class InterfaceState(ISISInterface): # pragma: no cover + """Alias for the ISISInterface model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInterface model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the ISISInterface class, emitting a deprecation warning.""" + warn( + message="InterfaceState model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) + + +class IsisInstance(ISISInstance): # pragma: no cover + """Alias for the ISISInstance model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInstance model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the ISISInstance class, emitting a deprecation warning.""" + warn( + message="IsisInstance model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInstance model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 54a4f14ec..33af12d74 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -13,43 +13,25 @@ from pydantic import BaseModel from anta.custom_types import Interface +from anta.input_models.routing.isis import InterfaceCount, InterfaceState, ISISInstance, IsisInstance, ISISInterface from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value -def _count_isis_neighbor(isis_neighbor_json: dict[str, Any]) -> int: - """Count the number of isis neighbors. - - Parameters - ---------- - isis_neighbor_json - The JSON output of the `show isis neighbors` command. - - Returns - ------- - int - The number of isis neighbors. - - """ - count = 0 - for vrf_data in isis_neighbor_json["vrfs"].values(): - for instance_data in vrf_data["isisInstances"].values(): - count += len(instance_data.get("neighbors", {})) - return count - - -def _get_not_full_isis_neighbors(isis_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: - """Return the isis neighbors whose adjacency state is not `up`. +def _get_isis_neighbor_details(isis_neighbor_json: dict[str, Any], neighbor_state: Literal["up", "down"] | None = None) -> list[dict[str, Any]]: + """Return the list of isis neighbors. Parameters ---------- isis_neighbor_json The JSON output of the `show isis neighbors` command. + neighbor_state + Value of the neihbor state we are looking for. Defaults to `None`. Returns ------- list[dict[str, Any]] - A list of isis neighbors whose adjacency state is not `UP`. + A list of isis neighbors. """ return [ @@ -57,51 +39,31 @@ def _get_not_full_isis_neighbors(isis_neighbor_json: dict[str, Any]) -> list[dic "vrf": vrf, "instance": instance, "neighbor": adjacency["hostname"], - "state": state, + "neighbor_address": adjacency["routerIdV4"], + "interface": adjacency["interfaceName"], + "state": adjacency["state"], } for vrf, vrf_data in isis_neighbor_json["vrfs"].items() - for instance, instance_data in vrf_data.get("isisInstances").items() - for neighbor, neighbor_data in instance_data.get("neighbors").items() - for adjacency in neighbor_data.get("adjacencies") - if (state := adjacency["state"]) != "up" + for instance, instance_data in vrf_data.get("isisInstances", {}).items() + for neighbor, neighbor_data in instance_data.get("neighbors", {}).items() + for adjacency in neighbor_data.get("adjacencies", []) + if neighbor_state is None or adjacency["state"] == neighbor_state ] -def _get_full_isis_neighbors(isis_neighbor_json: dict[str, Any], neighbor_state: Literal["up", "down"] = "up") -> list[dict[str, Any]]: - """Return the isis neighbors whose adjacency state is `up`. +def _get_isis_interface_details(isis_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: + """Return the isis interface details. Parameters ---------- isis_neighbor_json - The JSON output of the `show isis neighbors` command. - neighbor_state - Value of the neihbor state we are looking for. Defaults to `up`. + The JSON output of the `show isis interface brief` command. Returns ------- - list[dict[str, Any]] - A list of isis neighbors whose adjacency state is not `UP`. - + dict[str, Any]] + A dict of isis interfaces. """ - return [ - { - "vrf": vrf, - "instance": instance, - "neighbor": adjacency["hostname"], - "neighbor_address": adjacency["routerIdV4"], - "interface": adjacency["interfaceName"], - "state": state, - } - for vrf, vrf_data in isis_neighbor_json["vrfs"].items() - for instance, instance_data in vrf_data.get("isisInstances").items() - for neighbor, neighbor_data in instance_data.get("neighbors").items() - for adjacency in neighbor_data.get("adjacencies") - if (state := adjacency["state"]) == neighbor_state - ] - - -def _get_isis_neighbors_count(isis_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: - """Count number of IS-IS neighbor of the device.""" return [ {"vrf": vrf, "interface": interface, "mode": mode, "count": int(level_data["numAdjacencies"]), "level": int(level)} for vrf, vrf_data in isis_neighbor_json["vrfs"].items() @@ -146,7 +108,7 @@ class VerifyISISNeighborState(AntaTest): Expected Results ---------------- * Success: The test will pass if all IS-IS neighbors are in UP state. - * Failure: The test will fail if some IS-IS neighbors are not in UP state. + * Failure: The test will fail if any IS-IS neighbor adjance session is down. * Skipped: The test will be skipped if no IS-IS neighbor is found. Examples @@ -164,14 +126,19 @@ class VerifyISISNeighborState(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISNeighborState.""" + self.result.is_success() + + # Verify the ISIS neighbor configure. If not then skip the test. command_output = self.instance_commands[0].json_output - if _count_isis_neighbor(command_output) == 0: + neighbor_details = _get_isis_neighbor_details(command_output) + if not neighbor_details: self.result.is_skipped("No IS-IS neighbor detected") return - self.result.is_success() - not_full_neighbors = _get_not_full_isis_neighbors(command_output) - if not_full_neighbors: - self.result.is_failure(f"Some neighbors are not in the correct state (UP): {not_full_neighbors}.") + + # Verify that no neighbor has a session in the down state + not_full_neighbors = _get_isis_neighbor_details(command_output, neighbor_state="down") + for neighbor in not_full_neighbors: + self.result.is_failure(f"Instance: {neighbor['instance']} VRF: {neighbor['vrf']} Neighbor: {neighbor['neighbor']} - Session (adjacency) down") class VerifyISISNeighborCount(AntaTest): @@ -208,39 +175,29 @@ class VerifyISISNeighborCount(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISNeighborCount test.""" - interfaces: list[InterfaceCount] + interfaces: list[ISISInterface] """list of interfaces with their information.""" - - class InterfaceCount(BaseModel): - """Input model for the VerifyISISNeighborCount test.""" - - name: Interface - """Interface name to check.""" - level: int = 2 - """IS-IS level to check.""" - count: int - """Number of IS-IS neighbors.""" + InterfaceCount: ClassVar[type[InterfaceCount]] = InterfaceCount @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISNeighborCount.""" - command_output = self.instance_commands[0].json_output self.result.is_success() - isis_neighbor_count = _get_isis_neighbors_count(command_output) + + command_output = self.instance_commands[0].json_output + isis_neighbor_count = _get_isis_interface_details(command_output) if len(isis_neighbor_count) == 0: self.result.is_skipped("No IS-IS neighbor detected") return + for interface in self.inputs.interfaces: eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data["interface"] == interface.name and ifl_data["level"] == interface.level] if not eos_data: - self.result.is_failure(f"No neighbor detected for interface {interface.name}") + self.result.is_failure(f"{interface} - Not configured") continue - if eos_data[0]["count"] != interface.count: - self.result.is_failure( - f"Interface {interface.name}: " - f"expected Level {interface.level}: count {interface.count}, " - f"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}" - ) + + if (act_count := eos_data[0]["count"]) != interface.count: + self.result.is_failure(f"{interface} - Neighbor count mismatch - Expected: {interface.count} Actual: {act_count}") class VerifyISISInterfaceMode(AntaTest): @@ -280,27 +237,16 @@ class VerifyISISInterfaceMode(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISNeighborCount test.""" - interfaces: list[InterfaceState] + interfaces: list[ISISInterface] """list of interfaces with their information.""" - - class InterfaceState(BaseModel): - """Input model for the VerifyISISNeighborCount test.""" - - name: Interface - """Interface name to check.""" - level: Literal[1, 2] = 2 - """ISIS level configured for interface. Default is 2.""" - mode: Literal["point-to-point", "broadcast", "passive"] - """Number of IS-IS neighbors.""" - vrf: str = "default" - """VRF where the interface should be configured""" + InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISInterfaceMode.""" - command_output = self.instance_commands[0].json_output self.result.is_success() + command_output = self.instance_commands[0].json_output if len(command_output["vrfs"]) == 0: self.result.is_skipped("IS-IS is not configured on device") return @@ -317,14 +263,14 @@ def test(self) -> None: interface_type = get_value(dictionary=interface_data, key="interfaceType", default="unset") # Check for interfaceType if interface.mode == "point-to-point" and interface.mode != interface_type: - self.result.is_failure(f"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}") + self.result.is_failure(f"{interface} - Incorrect mode - Expected: point-to-point Actual: {interface_type}") # Check for passive elif interface.mode == "passive": json_path = f"intfLevels.{interface.level}.passive" if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False: - self.result.is_failure(f"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode") + self.result.is_failure(f"{interface} - Not running in passive mode") else: - self.result.is_failure(f"Interface {interface.name} not found in VRF {interface.vrf}") + self.result.is_failure(f"{interface} - Not configured") class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): @@ -358,45 +304,20 @@ class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingAdjacencySegments test.""" - instances: list[IsisInstance] - - class IsisInstance(BaseModel): - """ISIS Instance model definition.""" - - name: str - """ISIS instance name.""" - vrf: str = "default" - """VRF name where ISIS instance is configured.""" - segments: list[Segment] - """List of Adjacency segments configured in this instance.""" - - class Segment(BaseModel): - """Segment model definition.""" - - interface: Interface - """Interface name to check.""" - level: Literal[1, 2] = 2 - """ISIS level configured for interface. Default is 2.""" - sid_origin: Literal["dynamic"] = "dynamic" - """Adjacency type""" - address: IPv4Address - """IP address of remote end of segment.""" + instances: list[ISISInstance] + """list of ISIS instances with their information.""" + IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISSegmentRoutingAdjacencySegments.""" - command_output = self.instance_commands[0].json_output self.result.is_success() + command_output = self.instance_commands[0].json_output if len(command_output["vrfs"]) == 0: self.result.is_skipped("IS-IS is not configured on device") return - # initiate defaults - failure_message = [] - skip_vrfs = [] - skip_instances = [] - # Check if VRFs and instances are present in output. for instance in self.inputs.instances: vrf_data = get_value( @@ -405,34 +326,33 @@ def test(self) -> None: default=None, ) if vrf_data is None: - skip_vrfs.append(instance.vrf) - failure_message.append(f"VRF {instance.vrf} is not configured to run segment routging.") + self.result.is_failure(f"{instance} - VRF not configured") + continue - elif get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None: - skip_instances.append(instance.name) - failure_message.append(f"Instance {instance.name} is not found in vrf {instance.vrf}.") + if get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None: + self.result.is_failure(f"{instance} - Not configured") + continue - # Check Adjacency segments - for instance in self.inputs.instances: - if instance.vrf not in skip_vrfs and instance.name not in skip_instances: - for input_segment in instance.segments: - eos_segment = _get_adjacency_segment_data_by_neighbor( - neighbor=str(input_segment.address), - instance=instance.name, - vrf=instance.vrf, - command_output=command_output, + for input_segment in instance.segments: + eos_segment = _get_adjacency_segment_data_by_neighbor( + neighbor=str(input_segment.address), + instance=instance.name, + vrf=instance.vrf, + command_output=command_output, + ) + if eos_segment is None: + self.result.is_failure(f"{instance} {input_segment} - Segment not configured") + + elif not all( + [ + (act_origin := eos_segment["sidOrigin"]) == input_segment.sid_origin, + (endpoint := eos_segment["ipAddress"]) == str(input_segment.address), + (actual_level := eos_segment["level"]) == input_segment.level, + ] + ): + self.result.is_failure( + f"{instance} {input_segment} - Not correctly configured - Origin: {act_origin} Endpoint: {endpoint} Level: {actual_level}" ) - if eos_segment is None: - failure_message.append(f"Your segment has not been found: {input_segment}.") - - elif ( - eos_segment["localIntf"] != input_segment.interface - or eos_segment["level"] != input_segment.level - or eos_segment["sidOrigin"] != input_segment.sid_origin - ): - failure_message.append(f"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.") - if failure_message: - self.result.is_failure("\n".join(failure_message)) class VerifyISISSegmentRoutingDataplane(AntaTest): @@ -463,57 +383,27 @@ class VerifyISISSegmentRoutingDataplane(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingDataplane test.""" - instances: list[IsisInstance] - - class IsisInstance(BaseModel): - """ISIS Instance model definition.""" - - name: str - """ISIS instance name.""" - vrf: str = "default" - """VRF name where ISIS instance is configured.""" - dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured dataplane for the instance.""" + instances: list[ISISInstance] + """list of ISIS instances with their information.""" + IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISSegmentRoutingDataplane.""" - command_output = self.instance_commands[0].json_output self.result.is_success() - if len(command_output["vrfs"]) == 0: - self.result.is_skipped("IS-IS-SR is not running on device.") + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("No IS-IS neighbor detected") return - # initiate defaults - failure_message = [] - skip_vrfs = [] - skip_instances = [] - # Check if VRFs and instances are present in output. for instance in self.inputs.instances: - vrf_data = get_value( - dictionary=command_output, - key=f"vrfs.{instance.vrf}", - default=None, - ) - if vrf_data is None: - skip_vrfs.append(instance.vrf) - failure_message.append(f"VRF {instance.vrf} is not configured to run segment routing.") - - elif get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None: - skip_instances.append(instance.name) - failure_message.append(f"Instance {instance.name} is not found in vrf {instance.vrf}.") - - # Check Adjacency segments - for instance in self.inputs.instances: - if instance.vrf not in skip_vrfs and instance.name not in skip_instances: - eos_dataplane = get_value(dictionary=command_output, key=f"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane", default=None) - if instance.dataplane.upper() != eos_dataplane: - failure_message.append(f"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})") + if not (instance_data := get_value(command_output, f"{instance.vrf}..isisInstances..{instance.name}", separator="..")): + self.result.is_failure(f"{instance} - Not configured") + continue - if failure_message: - self.result.is_failure("\n".join(failure_message)) + if instance.dataplane.upper() != (plane := instance_data["dataPlane"]): + self.result.is_failure(f"{instance} - Dataplane not correctly configured - Expected: {instance.dataplane.upper()} Actual: {plane}") class VerifyISISSegmentRoutingTunnels(AntaTest): diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 84f5bdcf7..33ab20ac7 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -163,7 +163,7 @@ "inputs": None, "expected": { "result": "failure", - "messages": ["Some neighbors are not in the correct state (UP): [{'vrf': 'default', 'instance': 'CORE-ISIS', 'neighbor': 's1-p01', 'state': 'down'}]."], + "messages": ["Instance: CORE-ISIS VRF: default Neighbor: s1-p01 - Session (adjacency) down"], }, }, { @@ -306,7 +306,7 @@ }, "expected": { "result": "failure", - "messages": ["No neighbor detected for interface Ethernet2"], + "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured"], }, }, { @@ -349,7 +349,7 @@ }, "expected": { "result": "failure", - "messages": ["Interface Ethernet1: expected Level 2: count 1, got Level 2: count 3"], + "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Neighbor count mismatch - Expected: 1 Actual: 3"], }, }, { @@ -516,7 +516,7 @@ }, "expected": { "result": "failure", - "messages": ["Interface Ethernet2 in VRF default is not running in passive mode"], + "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not running in passive mode"], }, }, { @@ -601,7 +601,7 @@ }, "expected": { "result": "failure", - "messages": ["Interface Ethernet1 in VRF default is not running in point-to-point reporting broadcast"], + "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect mode - Expected: point-to-point Actual: broadcast"], }, }, { @@ -687,9 +687,9 @@ "expected": { "result": "failure", "messages": [ - "Interface Loopback0 not found in VRF default", - "Interface Ethernet2 not found in VRF default", - "Interface Ethernet1 not found in VRF default", + "Interface: Loopback0 VRF: default Level: 2 - Not configured", + "Interface: Ethernet2 VRF: default Level: 2 - Not configured", + "Interface: Ethernet1 VRF: default Level: 2 - Not configured", ], }, }, @@ -885,7 +885,7 @@ }, "expected": { "result": "failure", - "messages": ["Your segment has not been found: interface='Ethernet3' level=2 sid_origin='dynamic' address=IPv4Address('10.0.1.2')."], + "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet3 Endpoint: 10.0.1.2 - Segment not configured"], }, }, { @@ -968,7 +968,7 @@ }, "expected": { "result": "failure", - "messages": ["VRF custom is not configured to run segment routging."], + "messages": ["Instance: CORE-ISIS VRF: custom - VRF not configured"], }, }, { @@ -1051,7 +1051,7 @@ }, "expected": { "result": "failure", - "messages": ["Instance CORE-ISIS2 is not found in vrf default."], + "messages": ["Instance: CORE-ISIS2 VRF: default - Not configured"], }, }, { @@ -1115,11 +1115,7 @@ "expected": { "result": "failure", "messages": [ - ( - "Your segment is not correct: Expected: interface='Ethernet2' level=1 sid_origin='dynamic' address=IPv4Address('10.0.1.3') - " - "Found: {'ipAddress': '10.0.1.3', 'localIntf': 'Ethernet2', 'sid': 116384, 'lan': False, 'sidOrigin': 'dynamic', 'protection': " - "'unprotected', 'flags': {'b': False, 'v': True, 'l': True, 'f': False, 's': False}, 'level': 2}." - ) + "Instance: CORE-ISIS VRF: default Interface: Ethernet2 Endpoint: 10.0.1.3 - Not correctly configured - Origin: dynamic Endpoint: 10.0.1.3 Level: 2" ], }, }, @@ -1186,7 +1182,7 @@ }, "expected": { "result": "failure", - "messages": ["ISIS instance CORE-ISIS is not running dataplane unset (MPLS)"], + "messages": ["Instance: CORE-ISIS VRF: default - Dataplane not correctly configured - Expected: UNSET Actual: MPLS"], }, }, { @@ -1219,7 +1215,7 @@ }, "expected": { "result": "failure", - "messages": ["Instance CORE-ISIS2 is not found in vrf default."], + "messages": ["Instance: CORE-ISIS2 VRF: default - Not configured"], }, }, { @@ -1252,7 +1248,7 @@ }, "expected": { "result": "failure", - "messages": ["VRF wrong_vrf is not configured to run segment routing."], + "messages": ["Instance: CORE-ISIS VRF: wrong_vrf - Not configured"], }, }, { @@ -1270,7 +1266,7 @@ }, "expected": { "result": "skipped", - "messages": ["IS-IS-SR is not running on device"], + "messages": ["No IS-IS neighbor detected"], }, }, { From 484a366a33070590565f3745f4713e5aafb049ca Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 12 Dec 2024 00:03:57 -0500 Subject: [PATCH 02/13] Updated unit tests --- anta/input_models/routing/isis.py | 8 ++-- anta/tests/routing/isis.py | 45 ++++++++++++++++----- docs/api/tests.routing.isis.md | 17 ++++++++ examples/tests.yaml | 6 +-- tests/units/anta_tests/routing/test_isis.py | 4 +- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index eaba9bb25..853369d15 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -19,13 +19,13 @@ class ISISInstance(BaseModel): model_config = ConfigDict(extra="forbid") name: str - """ISIS instance name.""" + """The name of the IS-IS instance.""" vrf: str = "default" - """VRF name where ISIS instance is configured.""" + """VRF context where the IS-IS instance is configured. Defaults to `default`.""" dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured dataplane for the instance.""" + """Configured dataplane for the IS-IS instance.""" segments: list[Segment] | None = None - """List of ISIS segments""" + """A list of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingDataplane` test""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInstance for reporting.""" diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 33af12d74..226265823 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -142,12 +142,21 @@ def test(self) -> None: class VerifyISISNeighborCount(AntaTest): - """Verifies number of IS-IS neighbors per level and per interface. + """Verifies the number of IS-IS neighbors. + + This test performs the following checks for each specified interface: + + 1. Validates the IS-IS neighbors configured on specified interface. + 2. Validates the number of IS-IS neighbors for each interface at specified level. Expected Results ---------------- - * Success: The test will pass if the number of neighbors is correct. - * Failure: The test will fail if the number of neighbors is incorrect. + * Success: If all of the following occur: + - The IS-IS neighbors configured on specified interface. + - The number of IS-IS neighbors for each interface at specified level matches the given input. + * Failure: If any of the following occur: + - The IS-IS neighbors are not configured on specified interface. + - The number of IS-IS neighbors for each interface at specified level does not matches the given input. * Skipped: The test will be skipped if no IS-IS neighbor is found. Examples @@ -201,7 +210,12 @@ def test(self) -> None: class VerifyISISInterfaceMode(AntaTest): - """Verifies ISIS Interfaces are running in correct mode. + """Verifies the operational mode of IS-IS Interfaces. + + This test performs the following checks: + + 1. Validates that all specified IS-IS interfaces are configured. + 2. Validates the operational mode of each IS-IS interface (e.g., "active," "passive," or "unset"). Expected Results ---------------- @@ -248,7 +262,7 @@ def test(self) -> None: command_output = self.instance_commands[0].json_output if len(command_output["vrfs"]) == 0: - self.result.is_skipped("IS-IS is not configured on device") + self.result.is_skipped("No IS-IS neighbor detected") return # Check for p2p interfaces @@ -274,7 +288,7 @@ def test(self) -> None: class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): - """Verify that all expected Adjacency segments are correctly visible for each interface. + """Verifies the ISIS SR Adjacency Segments. Expected Results ---------------- @@ -315,7 +329,7 @@ def test(self) -> None: command_output = self.instance_commands[0].json_output if len(command_output["vrfs"]) == 0: - self.result.is_skipped("IS-IS is not configured on device") + self.result.is_skipped("No IS-IS neighbor detected") return # Check if VRFs and instances are present in output. @@ -356,13 +370,22 @@ def test(self) -> None: class VerifyISISSegmentRoutingDataplane(AntaTest): - """Verify dataplane of a list of ISIS-SR instances. + """Verifies the Dataplane of ISIS-SR Instances. + + This test performs the following checks: + + 1. Validates that listed ISIS-SR instance exists. + 2. Validates the configured dataplane matches the expected value for each instance. Expected Results ---------------- - * Success: The test will pass if all instances have correct dataplane configured - * Failure: The test will fail if one of the instances has incorrect dataplane configured - * Skipped: The test will be skipped if ISIS is not running + * Success: If all of the following occur: + - All specified ISIS-SR instances are configured. + - Each instance has the correct dataplane. + * Failure: If any of the following occur: + - Any specified ISIS-SR instance is not configured. + - Any instance has an incorrect dataplane configuration. + * Skipped: The test will be skipped if no IS-IS neighbor is found. Examples -------- diff --git a/docs/api/tests.routing.isis.md b/docs/api/tests.routing.isis.md index 16ca7ffeb..e6b8c30c1 100644 --- a/docs/api/tests.routing.isis.md +++ b/docs/api/tests.routing.isis.md @@ -7,6 +7,8 @@ anta_title: ANTA catalog for IS-IS tests ~ that can be found in the LICENSE file. --> +# Tests + ::: anta.tests.routing.isis options: @@ -20,3 +22,18 @@ anta_title: ANTA catalog for IS-IS tests - "!test" - "!render" - "!^_[^_]" + +# Input models + +::: anta.input_models.routing.isis + + options: + show_root_heading: false + show_root_toc_entry: false + show_bases: false + anta_hide_test_module_description: true + merge_init_into_class: false + show_labels: true + filters: + - "!^__init__" + - "!^__str__" diff --git a/examples/tests.yaml b/examples/tests.yaml index e14b40823..a02b02060 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -518,7 +518,7 @@ anta.tests.routing.isis: vrf: default # level is set to 2 by default - VerifyISISNeighborCount: - # Verifies number of IS-IS neighbors per level and per interface. + # Verifies the number of IS-IS neighbors. interfaces: - name: Ethernet1 level: 1 @@ -532,7 +532,7 @@ anta.tests.routing.isis: - VerifyISISNeighborState: # Verifies all IS-IS neighbors are in UP state. - VerifyISISSegmentRoutingAdjacencySegments: - # Verify that all expected Adjacency segments are correctly visible for each interface. + # Verifies the ISIS SR Adjacency Segments. instances: - name: CORE-ISIS vrf: default @@ -541,7 +541,7 @@ anta.tests.routing.isis: address: 10.0.1.3 sid_origin: dynamic - VerifyISISSegmentRoutingDataplane: - # Verify dataplane of a list of ISIS-SR instances. + # Verifies the Dataplane of ISIS-SR Instances. instances: - name: CORE-ISIS vrf: default diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 33ab20ac7..8c5471aef 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -704,7 +704,7 @@ {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, ] }, - "expected": {"result": "skipped", "messages": ["IS-IS is not configured on device"]}, + "expected": {"result": "skipped", "messages": ["No IS-IS neighbor detected"]}, }, { "name": "Skipped of VerifyISISSegmentRoutingAdjacencySegments no VRF.", @@ -725,7 +725,7 @@ } ] }, - "expected": {"result": "skipped", "messages": ["IS-IS is not configured on device"]}, + "expected": {"result": "skipped", "messages": ["No IS-IS neighbor detected"]}, }, { "test": VerifyISISSegmentRoutingAdjacencySegments, From b7aa14615afd88ae0485916bb52bc005726633f8 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Tue, 17 Dec 2024 23:58:20 -0500 Subject: [PATCH 03/13] Addressed review comments: updated input models, failure msgs --- anta/input_models/routing/isis.py | 77 +++------------------ anta/tests/routing/isis.py | 29 ++++---- tests/units/anta_tests/routing/test_isis.py | 8 +-- 3 files changed, 29 insertions(+), 85 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index 853369d15..1b3b28d1c 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -6,8 +6,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, Literal -from warnings import warn +from typing import Literal from pydantic import BaseModel, ConfigDict @@ -15,7 +14,7 @@ class ISISInstance(BaseModel): - """Model for a ISIS instance.""" + """Model for an IS-IS instance.""" model_config = ConfigDict(extra="forbid") name: str @@ -23,9 +22,9 @@ class ISISInstance(BaseModel): vrf: str = "default" """VRF context where the IS-IS instance is configured. Defaults to `default`.""" dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured dataplane for the IS-IS instance.""" + """Configured dataplane for the IS-IS instance. Required field in the `VerifyISISSegmentRoutingDataplane` test.""" segments: list[Segment] | None = None - """A list of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingDataplane` test""" + """A list of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInstance for reporting.""" @@ -39,7 +38,7 @@ class Segment(BaseModel): interface: Interface """Interface name to check.""" level: Literal[1, 2] = 2 - """ISIS level configured for interface. Default is 2.""" + """IS-IS level configured for interface. Default is 2.""" sid_origin: Literal["dynamic"] = "dynamic" "Specifies the origin of the Segment ID." address: IPv4Address @@ -47,78 +46,24 @@ class Segment(BaseModel): def __str__(self) -> str: """Return a human-readable string representation of the Segment for reporting.""" - return f"Interface: {self.interface} Endpoint: {self.address}" + return f"Interface: {self.interface} IP Addr: {self.address}" class ISISInterface(BaseModel): - """Model for a ISIS Interface.""" + """Model for a IS-IS Interface.""" model_config = ConfigDict(extra="forbid") name: Interface """Interface name to check.""" vrf: str = "default" - """VRF name where ISIS instance is configured.""" + """VRF name where IS-IS instance is configured.""" level: Literal[1, 2] = 2 - """ISIS level (1 or 2) configured for the interface. Default is 2.""" + """IS-IS level (1 or 2) configured for the interface. Default is 2.""" count: int | None = None """The total number of IS-IS neighbors associated with interface.""" mode: Literal["point-to-point", "broadcast", "passive"] | None = None - """The operational mode of the IS-IS interface.""" + """The configured IS-IS circuit type for this interface.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInterface for reporting.""" - return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level if self.level else 'IS Type(1-2)'}" - - -class InterfaceCount(ISISInterface): # pragma: no cover - """Alias for the ISISInterface model to maintain backward compatibility. - - When initialized, it will emit a deprecation warning and call the ISISInterface model. - - TODO: Remove this class in ANTA v2.0.0. - """ - - def __init__(self, **data: Any) -> None: # noqa: ANN401 - """Initialize the ISISInterface class, emitting a deprecation warning.""" - warn( - message="InterfaceCount model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", - category=DeprecationWarning, - stacklevel=2, - ) - super().__init__(**data) - - -class InterfaceState(ISISInterface): # pragma: no cover - """Alias for the ISISInterface model to maintain backward compatibility. - - When initialized, it will emit a deprecation warning and call the ISISInterface model. - - TODO: Remove this class in ANTA v2.0.0. - """ - - def __init__(self, **data: Any) -> None: # noqa: ANN401 - """Initialize the ISISInterface class, emitting a deprecation warning.""" - warn( - message="InterfaceState model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", - category=DeprecationWarning, - stacklevel=2, - ) - super().__init__(**data) - - -class IsisInstance(ISISInstance): # pragma: no cover - """Alias for the ISISInstance model to maintain backward compatibility. - - When initialized, it will emit a deprecation warning and call the ISISInstance model. - - TODO: Remove this class in ANTA v2.0.0. - """ - - def __init__(self, **data: Any) -> None: # noqa: ANN401 - """Initialize the ISISInstance class, emitting a deprecation warning.""" - warn( - message="IsisInstance model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInstance model instead.", - category=DeprecationWarning, - stacklevel=2, - ) - super().__init__(**data) + return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level}" diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 226265823..f0802d24f 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -13,7 +13,7 @@ from pydantic import BaseModel from anta.custom_types import Interface -from anta.input_models.routing.isis import InterfaceCount, InterfaceState, ISISInstance, IsisInstance, ISISInterface +from anta.input_models.routing.isis import ISISInstance, ISISInterface from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -186,7 +186,7 @@ class Input(AntaTest.Input): interfaces: list[ISISInterface] """list of interfaces with their information.""" - InterfaceCount: ClassVar[type[InterfaceCount]] = InterfaceCount + InterfaceCount: ClassVar[type[ISISInterface]] = ISISInterface @AntaTest.anta_test def test(self) -> None: @@ -253,7 +253,7 @@ class Input(AntaTest.Input): interfaces: list[ISISInterface] """list of interfaces with their information.""" - InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState + InterfaceState: ClassVar[type[ISISInterface]] = ISISInterface @AntaTest.anta_test def test(self) -> None: @@ -277,7 +277,7 @@ def test(self) -> None: interface_type = get_value(dictionary=interface_data, key="interfaceType", default="unset") # Check for interfaceType if interface.mode == "point-to-point" and interface.mode != interface_type: - self.result.is_failure(f"{interface} - Incorrect mode - Expected: point-to-point Actual: {interface_type}") + self.result.is_failure(f"{interface} - Incorrect circuit type - Expected: point-to-point Actual: {interface_type}") # Check for passive elif interface.mode == "passive": json_path = f"intfLevels.{interface.level}.passive" @@ -320,7 +320,7 @@ class Input(AntaTest.Input): instances: list[ISISInstance] """list of ISIS instances with their information.""" - IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance + IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance @AntaTest.anta_test def test(self) -> None: @@ -356,18 +356,19 @@ def test(self) -> None: ) if eos_segment is None: self.result.is_failure(f"{instance} {input_segment} - Segment not configured") + continue - elif not all( - [ - (act_origin := eos_segment["sidOrigin"]) == input_segment.sid_origin, - (endpoint := eos_segment["ipAddress"]) == str(input_segment.address), - (actual_level := eos_segment["level"]) == input_segment.level, - ] - ): + if (act_origin := eos_segment["sidOrigin"]) != input_segment.sid_origin: self.result.is_failure( - f"{instance} {input_segment} - Not correctly configured - Origin: {act_origin} Endpoint: {endpoint} Level: {actual_level}" + f"{instance} {input_segment} - Incorrect Segment Identifier origin - Expected: {input_segment.sid_origin} Actual: {act_origin}" ) + if (endpoint := eos_segment["ipAddress"]) != str(input_segment.address): + self.result.is_failure(f"{instance} {input_segment} - Incorrect Segment endpoint - Expected: {input_segment.address} Actual: {endpoint}") + + if (actual_level := eos_segment["level"]) != input_segment.level: + self.result.is_failure(f"{instance} {input_segment} - Incorrect IS-IS level - Expected: {input_segment.level} Actual: {actual_level}") + class VerifyISISSegmentRoutingDataplane(AntaTest): """Verifies the Dataplane of ISIS-SR Instances. @@ -408,7 +409,7 @@ class Input(AntaTest.Input): instances: list[ISISInstance] """list of ISIS instances with their information.""" - IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance + IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance @AntaTest.anta_test def test(self) -> None: diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 8c5471aef..a62e87638 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -601,7 +601,7 @@ }, "expected": { "result": "failure", - "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect mode - Expected: point-to-point Actual: broadcast"], + "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect circuit type - Expected: point-to-point Actual: broadcast"], }, }, { @@ -885,7 +885,7 @@ }, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet3 Endpoint: 10.0.1.2 - Segment not configured"], + "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet3 IP Addr: 10.0.1.2 - Segment not configured"], }, }, { @@ -1114,9 +1114,7 @@ }, "expected": { "result": "failure", - "messages": [ - "Instance: CORE-ISIS VRF: default Interface: Ethernet2 Endpoint: 10.0.1.3 - Not correctly configured - Origin: dynamic Endpoint: 10.0.1.3 Level: 2" - ], + "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet2 IP Addr: 10.0.1.3 - Incorrect IS-IS level - Expected: 1 Actual: 2"], }, }, { From 9f92ab17525fe19f515acb33fafb4072d3692097 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Wed, 22 Jan 2025 06:13:14 -0500 Subject: [PATCH 04/13] Updated tests: fixed bugs vrf all --- anta/input_models/routing/isis.py | 2 +- anta/tests/routing/isis.py | 171 +++++++------------- examples/tests.yaml | 5 +- tests/units/anta_tests/routing/test_isis.py | 76 --------- 4 files changed, 63 insertions(+), 191 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index 1b3b28d1c..bec35a44f 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2024 Arista Networks, Inc. +# Copyright (c) 2023-2025 Arista Networks, Inc. # Use of this source code is governed by the Apache License 2.0 # that can be found in the LICENSE file. """Module containing input models for routing ISIS tests.""" diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 117e22cb0..a66c32f1f 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -18,76 +18,6 @@ from anta.tools import get_value -def _get_isis_neighbor_details(isis_neighbor_json: dict[str, Any], neighbor_state: Literal["up", "down"] | None = None) -> list[dict[str, Any]]: - """Return the list of isis neighbors. - - Parameters - ---------- - isis_neighbor_json - The JSON output of the `show isis neighbors` command. - neighbor_state - Value of the neihbor state we are looking for. Defaults to `None`. - - Returns - ------- - list[dict[str, Any]] - A list of isis neighbors. - - """ - return [ - { - "vrf": vrf, - "instance": instance, - "neighbor": adjacency["hostname"], - "neighbor_address": adjacency["routerIdV4"], - "interface": adjacency["interfaceName"], - "state": adjacency["state"], - } - for vrf, vrf_data in isis_neighbor_json["vrfs"].items() - for instance, instance_data in vrf_data.get("isisInstances", {}).items() - for neighbor, neighbor_data in instance_data.get("neighbors", {}).items() - for adjacency in neighbor_data.get("adjacencies", []) - if neighbor_state is None or adjacency["state"] == neighbor_state - ] - - -def _get_isis_interface_details(isis_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: - """Return the isis interface details. - - Parameters - ---------- - isis_neighbor_json - The JSON output of the `show isis interface brief` command. - - Returns - ------- - dict[str, Any]] - A dict of isis interfaces. - """ - return [ - {"vrf": vrf, "interface": interface, "mode": mode, "count": int(level_data["numAdjacencies"]), "level": int(level)} - for vrf, vrf_data in isis_neighbor_json["vrfs"].items() - for instance, instance_data in vrf_data.get("isisInstances").items() - for interface, interface_data in instance_data.get("interfaces").items() - for level, level_data in interface_data.get("intfLevels").items() - if (mode := level_data["passive"]) is not True - ] - - -def _get_interface_data(interface: str, vrf: str, command_output: dict[str, Any]) -> dict[str, Any] | None: - """Extract data related to an IS-IS interface for testing.""" - if (vrf_data := get_value(command_output, f"vrfs.{vrf}")) is None: - return None - - for instance_data in vrf_data.get("isisInstances").values(): - if (intf_dict := get_value(dictionary=instance_data, key="interfaces")) is not None: - try: - return next(ifl_data for ifl, ifl_data in intf_dict.items() if ifl == interface) - except StopIteration: - return None - return None - - def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: str, command_output: dict[str, Any]) -> dict[str, Any] | None: """Extract data related to an IS-IS interface for testing.""" search_path = f"vrfs.{vrf}.isisInstances.{instance}.adjacencySegments" @@ -103,12 +33,12 @@ def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: s class VerifyISISNeighborState(AntaTest): - """Verifies all IS-IS neighbors are in UP state. + """Verifies the health of all the IS-IS neighbors. Expected Results ---------------- * Success: The test will pass if all IS-IS neighbors are in UP state. - * Failure: The test will fail if any IS-IS neighbor adjance session is down. + * Failure: The test will fail if any IS-IS neighbor adjacency is down. * Skipped: The test will be skipped if no IS-IS neighbor is found. Examples @@ -117,28 +47,40 @@ class VerifyISISNeighborState(AntaTest): anta.tests.routing: isis: - VerifyISISNeighborState: + check_all_vrfs: bool = False ``` """ categories: ClassVar[list[str]] = ["isis"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis neighbors", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis neighbors vrf all", revision=1)] + + class Input(AntaTest.Input): + """Input model for the VerifyISISNeighborCount test.""" + + check_all_vrfs: bool = False + """If enabled it verifies the all ISIS instances in all the configured vrfs. Defaults to `False` and verified the `default` vrf only.""" @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISNeighborState.""" self.result.is_success() - # Verify the ISIS neighbor configure. If not then skip the test. - command_output = self.instance_commands[0].json_output - neighbor_details = _get_isis_neighbor_details(command_output) - if not neighbor_details: - self.result.is_skipped("No IS-IS neighbor detected") + # Verify if ISIS neighbors are configured. Skip the test if none are found. + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("IS-IS is not configured on device") return - # Verify that no neighbor has a session in the down state - not_full_neighbors = _get_isis_neighbor_details(command_output, neighbor_state="down") - for neighbor in not_full_neighbors: - self.result.is_failure(f"Instance: {neighbor['instance']} VRF: {neighbor['vrf']} Neighbor: {neighbor['neighbor']} - Session (adjacency) down") + vrfs_to_check = command_output + if not self.inputs.check_all_vrfs: + vrfs_to_check = {"default": command_output["default"]} + + for vrf, vrf_data in vrfs_to_check.items(): + for isis_instance, instace_data in vrf_data["isisInstances"].items(): + neighbors = instace_data.get("neighbors", {}) + for neighbor in neighbors.values(): + for adjacencies in neighbor["adjacencies"]: + if adjacencies["state"] != "up": + self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {adjacencies['interfaceName']} - Session (adjacency) down") class VerifyISISNeighborCount(AntaTest): @@ -149,6 +91,9 @@ class VerifyISISNeighborCount(AntaTest): 1. Validates the IS-IS neighbors configured on specified interface. 2. Validates the number of IS-IS neighbors for each interface at specified level. + !! Warning + Test supports the `default` vrf only. + Expected Results ---------------- * Success: If all of the following occur: @@ -185,7 +130,7 @@ class Input(AntaTest.Input): """Input model for the VerifyISISNeighborCount test.""" interfaces: list[ISISInterface] - """list of interfaces with their information.""" + """List of IS-IS interfaces with their information.""" InterfaceCount: ClassVar[type[ISISInterface]] = ISISInterface @AntaTest.anta_test @@ -194,18 +139,24 @@ def test(self) -> None: self.result.is_success() command_output = self.instance_commands[0].json_output - isis_neighbor_count = _get_isis_interface_details(command_output) - if len(isis_neighbor_count) == 0: - self.result.is_skipped("No IS-IS neighbor detected") + + # Verify if ISIS neighbors are configured. Skip the test if none are found. + if not (instance_detail := get_value(command_output, "vrfs..default..isisInstances", separator="..")): + self.result.is_skipped("IS-IS is not configured on device") return for interface in self.inputs.interfaces: - eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data["interface"] == interface.name and ifl_data["level"] == interface.level] - if not eos_data: + interface_detail = {} + for instance_data in instance_detail.values(): + if interface_data := get_value(instance_data, f"interfaces..{interface.name}..intfLevels..{interface.level}", separator=".."): + interface_detail = interface_data + break + + if not interface_detail: self.result.is_failure(f"{interface} - Not configured") continue - if (act_count := eos_data[0]["count"]) != interface.count: + if (act_count := interface_detail.get("numAdjacencies", 0)) != interface.count: self.result.is_failure(f"{interface} - Neighbor count mismatch - Expected: {interface.count} Actual: {act_count}") @@ -244,9 +195,8 @@ class VerifyISISInterfaceMode(AntaTest): ``` """ - description = "Verifies interface mode for IS-IS" categories: ClassVar[list[str]] = ["isis"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief vrf all", revision=1)] class Input(AntaTest.Input): """Input model for the VerifyISISNeighborCount test.""" @@ -261,30 +211,30 @@ def test(self) -> None: self.result.is_success() command_output = self.instance_commands[0].json_output + + # Verify if ISIS neighbors are configured. Skip the test if none are found. if len(command_output["vrfs"]) == 0: self.result.is_skipped("No IS-IS neighbor detected") return - # Check for p2p interfaces for interface in self.inputs.interfaces: - interface_data = _get_interface_data( - interface=interface.name, - vrf=interface.vrf, - command_output=command_output, - ) - # Check for correct VRF - if interface_data is not None: - interface_type = get_value(dictionary=interface_data, key="interfaceType", default="unset") - # Check for interfaceType - if interface.mode == "point-to-point" and interface.mode != interface_type: - self.result.is_failure(f"{interface} - Incorrect circuit type - Expected: point-to-point Actual: {interface_type}") - # Check for passive - elif interface.mode == "passive": - json_path = f"intfLevels.{interface.level}.passive" - if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False: - self.result.is_failure(f"{interface} - Not running in passive mode") - else: + interface_detail = {} + instance_detail = get_value(command_output, f"vrfs..{interface.vrf}..isisInstances", separator="..", default={}) + for instance_data in instance_detail.values(): + if interface_data := get_value(instance_data, f"interfaces..{interface.name}", separator=".."): + interface_detail = interface_data + break + + if not interface_detail: self.result.is_failure(f"{interface} - Not configured") + continue + + # Check for passive + if interface.mode == "passive" and get_value(interface_detail, f"intfLevels.{interface.level}.passive", default=False) is False: + self.result.is_failure(f"{interface} - Not running in passive mode") + + elif interface.mode != (interface_type := get_value(interface_detail, "interfaceType", default="unset")): + self.result.is_failure(f"{interface} - Incorrect circuit type - Expected: {interface.mode} Actual: {interface_type}") class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): @@ -363,9 +313,6 @@ def test(self) -> None: f"{instance} {input_segment} - Incorrect Segment Identifier origin - Expected: {input_segment.sid_origin} Actual: {act_origin}" ) - if (endpoint := eos_segment["ipAddress"]) != str(input_segment.address): - self.result.is_failure(f"{instance} {input_segment} - Incorrect Segment endpoint - Expected: {input_segment.address} Actual: {endpoint}") - if (actual_level := eos_segment["level"]) != input_segment.level: self.result.is_failure(f"{instance} {input_segment} - Incorrect IS-IS level - Expected: {input_segment.level} Actual: {actual_level}") diff --git a/examples/tests.yaml b/examples/tests.yaml index f6df45463..6cf5ec301 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -586,7 +586,7 @@ anta.tests.routing.generic: maximum: 20 anta.tests.routing.isis: - VerifyISISInterfaceMode: - # Verifies interface mode for IS-IS + # Verifies the operational mode of IS-IS Interfaces. interfaces: - name: Loopback0 mode: passive @@ -612,7 +612,8 @@ anta.tests.routing.isis: count: 2 # level is set to 2 by default - VerifyISISNeighborState: - # Verifies all IS-IS neighbors are in UP state. + # Verifies the health of all the IS-IS neighbors. + check_all_vrfs: bool = False - VerifyISISSegmentRoutingAdjacencySegments: # Verifies the ISIS SR Adjacency Segments. instances: diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index a4c80843c..134b4a423 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -18,7 +18,6 @@ VerifyISISSegmentRoutingAdjacencySegments, VerifyISISSegmentRoutingDataplane, VerifyISISSegmentRoutingTunnels, - _get_interface_data, ) from tests.units.anta_tests import test @@ -1835,78 +1834,3 @@ }, }, ] - - -COMMAND_OUTPUT = { - "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "interfaces": { - "Loopback0": { - "enabled": True, - "intfLevels": { - "2": { - "ipv4Metric": 10, - "sharedSecretProfile": "", - "isisAdjacencies": [], - "passive": True, - "v4Protection": "disabled", - "v6Protection": "disabled", - } - }, - "areaProxyBoundary": False, - }, - "Ethernet1": { - "intfLevels": { - "2": { - "ipv4Metric": 10, - "numAdjacencies": 1, - "linkId": "84", - "sharedSecretProfile": "", - "isisAdjacencies": [], - "passive": False, - "v4Protection": "link", - "v6Protection": "disabled", - } - }, - "interfaceSpeed": 1000, - "areaProxyBoundary": False, - }, - } - } - } - }, - "EMPTY": {"isisInstances": {}}, - "NO_INTERFACES": {"isisInstances": {"CORE-ISIS": {}}}, - } -} -EXPECTED_LOOPBACK_0_OUTPUT = { - "enabled": True, - "intfLevels": { - "2": { - "ipv4Metric": 10, - "sharedSecretProfile": "", - "isisAdjacencies": [], - "passive": True, - "v4Protection": "disabled", - "v6Protection": "disabled", - } - }, - "areaProxyBoundary": False, -} - - -@pytest.mark.parametrize( - ("interface", "vrf", "expected_value"), - [ - pytest.param("Loopback0", "WRONG_VRF", None, id="VRF_not_found"), - pytest.param("Loopback0", "EMPTY", None, id="VRF_no_ISIS_instances"), - pytest.param("Loopback0", "NO_INTERFACES", None, id="ISIS_instance_no_interfaces"), - pytest.param("Loopback42", "default", None, id="interface_not_found"), - pytest.param("Loopback0", "default", EXPECTED_LOOPBACK_0_OUTPUT, id="interface_found"), - ], -) -def test__get_interface_data(interface: str, vrf: str, expected_value: dict[str, Any] | None) -> None: - """Test anta.tests.routing.isis._get_interface_data.""" - assert _get_interface_data(interface, vrf, COMMAND_OUTPUT) == expected_value From b4d79d69107ce0b62473b58859a5be256c786e8c Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Wed, 22 Jan 2025 06:56:52 -0500 Subject: [PATCH 05/13] fixed unit tests --- anta/tests/routing/isis.py | 4 ++-- tests/units/anta_tests/routing/test_isis.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index a66c32f1f..119d00e85 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -55,7 +55,7 @@ class VerifyISISNeighborState(AntaTest): commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis neighbors vrf all", revision=1)] class Input(AntaTest.Input): - """Input model for the VerifyISISNeighborCount test.""" + """Input model for the VerifyISISNeighborState test.""" check_all_vrfs: bool = False """If enabled it verifies the all ISIS instances in all the configured vrfs. Defaults to `False` and verified the `default` vrf only.""" @@ -233,7 +233,7 @@ def test(self) -> None: if interface.mode == "passive" and get_value(interface_detail, f"intfLevels.{interface.level}.passive", default=False) is False: self.result.is_failure(f"{interface} - Not running in passive mode") - elif interface.mode != (interface_type := get_value(interface_detail, "interfaceType", default="unset")): + if interface.mode not in ("passive", interface_type := get_value(interface_detail, "interfaceType", default="unset")): self.result.is_failure(f"{interface} - Incorrect circuit type - Expected: {interface.mode} Actual: {interface_type}") diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 134b4a423..054eb3ef3 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -162,19 +162,19 @@ "inputs": None, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default Neighbor: s1-p01 - Session (adjacency) down"], + "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet1 - Session (adjacency) down"], }, }, { "name": "skipped - no neighbor", "test": VerifyISISNeighborState, "eos_data": [ - {"vrfs": {"default": {"isisInstances": {"CORE-ISIS": {"neighbors": {}}}}}}, + {"vrfs": {}}, ], "inputs": None, "expected": { "result": "skipped", - "messages": ["No IS-IS neighbor detected"], + "messages": ["IS-IS is not configured on device"], }, }, { @@ -253,7 +253,7 @@ "name": "skipped - no neighbor", "test": VerifyISISNeighborCount, "eos_data": [ - {"vrfs": {"default": {"isisInstances": {"CORE-ISIS": {"interfaces": {}}}}}}, + {"vrfs": {"default": {"isisInstances": {}}}}, ], "inputs": { "interfaces": [ @@ -262,7 +262,7 @@ }, "expected": { "result": "skipped", - "messages": ["No IS-IS neighbor detected"], + "messages": ["IS-IS is not configured on device"], }, }, { From e67d5dc42cdf2480e071d4c5064cab6057af8041 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Wed, 29 Jan 2025 23:52:18 -0500 Subject: [PATCH 06/13] updated VerifyISISSegmentRoutingTunnels test --- anta/input_models/routing/isis.py | 44 ++++++++++- anta/tests/routing/isis.py | 83 +++++++-------------- tests/units/anta_tests/routing/test_isis.py | 14 ++-- 3 files changed, 76 insertions(+), 65 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index bec35a44f..91f092335 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -5,7 +5,7 @@ from __future__ import annotations -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv4Network from typing import Literal from pydantic import BaseModel, ConfigDict @@ -67,3 +67,45 @@ class ISISInterface(BaseModel): def __str__(self) -> str: """Return a human-readable string representation of the ISISInterface for reporting.""" return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level}" + + +class SRTunnelEntry(BaseModel): + """Model for a IS-IS SR tunnel.""" + + model_config = ConfigDict(extra="forbid") + endpoint: IPv4Network + """Endpoint of the tunnel.""" + vias: list[TunnelPath] | None = None + """Optional list of path to reach endpoint.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the SRTunnelEntry for reporting.""" + return f"Endpoint: {self.endpoint}" + + +class TunnelPath(BaseModel): + """Model for a IS-IS tunnel path.""" + + model_config = ConfigDict(extra="forbid") + nexthop: IPv4Address | None = None + """Nexthop of the tunnel.""" + type: Literal["ip", "tunnel"] | None = None + """Type of the tunnel.""" + interface: Interface | None = None + """Interface of the tunnel.""" + tunnel_id: Literal["TI-LFA", "ti-lfa", "unset"] | None = None + """Computation method of the tunnel.""" + + def __str__(self) -> str: + """Return a human-readable string representation of the TunnelPath for reporting.""" + base_string = "" + if self.nexthop: + base_string += f" Next-hop: {self.nexthop}" + if self.type: + base_string += f" Type: {self.type}" + if self.interface: + base_string += f" Interface: {self.interface}" + if self.tunnel_id: + base_string += f" TunnelID: {self.tunnel_id}" + + return base_string.lstrip() diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 119d00e85..90cf5a342 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -7,13 +7,9 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from ipaddress import IPv4Address, IPv4Network -from typing import Any, ClassVar, Literal +from typing import Any, ClassVar -from pydantic import BaseModel - -from anta.custom_types import Interface -from anta.input_models.routing.isis import ISISInstance, ISISInterface +from anta.input_models.routing.isis import ISISInstance, ISISInterface, SRTunnelEntry from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -77,10 +73,9 @@ def test(self) -> None: for vrf, vrf_data in vrfs_to_check.items(): for isis_instance, instace_data in vrf_data["isisInstances"].items(): neighbors = instace_data.get("neighbors", {}) - for neighbor in neighbors.values(): - for adjacencies in neighbor["adjacencies"]: - if adjacencies["state"] != "up": - self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {adjacencies['interfaceName']} - Session (adjacency) down") + interfaces = [adj["interfaceName"] for neighbor in neighbors.values() for adj in neighbor["adjacencies"] if adj["state"] != "up"] + for interface in interfaces: + self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {interface} - Session (adjacency) down") class VerifyISISNeighborCount(AntaTest): @@ -414,32 +409,12 @@ class VerifyISISSegmentRoutingTunnels(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingTunnels test.""" - entries: list[Entry] + entries: list[SRTunnelEntry] """List of tunnels to check on device.""" - class Entry(BaseModel): - """Definition of a tunnel entry.""" - - endpoint: IPv4Network - """Endpoint IP of the tunnel.""" - vias: list[Vias] | None = None - """Optional list of path to reach endpoint.""" - - class Vias(BaseModel): - """Definition of a tunnel path.""" - - nexthop: IPv4Address | None = None - """Nexthop of the tunnel. If None, then it is not tested. Default: None""" - type: Literal["ip", "tunnel"] | None = None - """Type of the tunnel. If None, then it is not tested. Default: None""" - interface: Interface | None = None - """Interface of the tunnel. If None, then it is not tested. Default: None""" - tunnel_id: Literal["TI-LFA", "ti-lfa", "unset"] | None = None - """Computation method of the tunnel. If None, then it is not tested. Default: None""" - - def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = "endpoint") -> dict[str, Any] | None: + def _eos_entry_lookup(self, search_value: str, entries: dict[str, Any], search_key: str = "endpoint") -> dict[str, Any] | None: return next( - (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)), + (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == search_value), None, ) @@ -453,41 +428,32 @@ def test(self) -> None: command_output = self.instance_commands[0].json_output self.result.is_success() - # initiate defaults - failure_message = [] - if len(command_output["entries"]) == 0: self.result.is_skipped("IS-IS-SR is not running on device.") return for input_entry in self.inputs.entries: - eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output["entries"]) + eos_entry = self._eos_entry_lookup(search_value=str(input_entry.endpoint), entries=command_output["entries"]) if eos_entry is None: - failure_message.append(f"Tunnel to {input_entry} is not found.") + self.result.is_failure(f"{input_entry} - Tunnel not found") elif input_entry.vias is not None: - failure_src = [] for via_input in input_entry.vias: if not self._check_tunnel_type(via_input, eos_entry): - failure_src.append("incorrect tunnel type") + self.result.is_failure(f"{input_entry} {via_input} - incorrect tunnel type") if not self._check_tunnel_nexthop(via_input, eos_entry): - failure_src.append("incorrect nexthop") + self.result.is_failure(f"{input_entry} {via_input} - incorrect nexthop") if not self._check_tunnel_interface(via_input, eos_entry): - failure_src.append("incorrect interface") + self.result.is_failure(f"{input_entry} {via_input} - incorrect interface") if not self._check_tunnel_id(via_input, eos_entry): - failure_src.append("incorrect tunnel ID") - - if failure_src: - failure_message.append(f"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}") - - if failure_message: - self.result.is_failure("\n".join(failure_message)) + self.result.is_failure(f"{input_entry} {via_input} - incorrect tunnel ID") - def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_type(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: """Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`. Parameters ---------- - via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : dict[str, Any] + VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input tunnel type to check. eos_entry : dict[str, Any] The EOS entry containing the tunnel types. @@ -509,12 +475,13 @@ def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.En ) return True - def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_nexthop(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: """Check if the tunnel nexthop matches the given input. Parameters ---------- - via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : dict[str, Any] + VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input via object. eos_entry : dict[str, Any] The EOS entry dictionary. @@ -536,12 +503,13 @@ def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input ) return True - def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_interface(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: """Check if the tunnel interface exists in the given EOS entry. Parameters ---------- - via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : dict[str, Any] + VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input via object. eos_entry : dict[str, Any] The EOS entry dictionary. @@ -563,12 +531,13 @@ def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Inp ) return True - def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_id(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: """Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias. Parameters ---------- - via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : dict[str, Any] + VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input vias to check. eos_entry : dict[str, Any]) The EOS entry to compare against. diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 054eb3ef3..aec4f0dfd 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -1398,7 +1398,7 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to endpoint=IPv4Network('1.0.0.122/32') vias=None is not found."], + "messages": ["Endpoint: 1.0.0.122/32 - Tunnel not found"], }, }, { @@ -1479,7 +1479,7 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to 1.0.0.13/32 is incorrect: incorrect tunnel type"], + "messages": ["Endpoint: 1.0.0.13/32 Type: tunnel - incorrect tunnel type"], }, }, { @@ -1567,12 +1567,12 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect nexthop"], + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - incorrect nexthop"], }, }, { "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect nexthop", + "name": "fails with incorrect interface", "eos_data": [ { "entries": { @@ -1655,7 +1655,7 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect interface"], + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.1 Type: ip Interface: Ethernet4 - incorrect interface"], }, }, { @@ -1743,7 +1743,7 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect nexthop"], + "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - incorrect nexthop"], }, }, { @@ -1830,7 +1830,7 @@ }, "expected": { "result": "failure", - "messages": ["Tunnel to 1.0.0.111/32 is incorrect: incorrect tunnel ID"], + "messages": ["Endpoint: 1.0.0.111/32 Type: tunnel TunnelID: unset - incorrect tunnel ID"], }, }, ] From a8158fd2ffbb558ccc8a565f95a5f3d861ea3800 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 30 Jan 2025 01:38:23 -0500 Subject: [PATCH 07/13] Updated unit tests --- anta/tests/routing/isis.py | 4 +- tests/units/anta_tests/routing/test_isis.py | 115 ++++++++++++++++---- 2 files changed, 96 insertions(+), 23 deletions(-) diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 90cf5a342..a0ad41097 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -17,11 +17,9 @@ def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: str, command_output: dict[str, Any]) -> dict[str, Any] | None: """Extract data related to an IS-IS interface for testing.""" search_path = f"vrfs.{vrf}.isisInstances.{instance}.adjacencySegments" - if get_value(dictionary=command_output, key=search_path, default=None) is None: + if (isis_instance := get_value(dictionary=command_output, key=search_path, default=None)) is None: return None - isis_instance = get_value(dictionary=command_output, key=search_path, default=None) - return next( (segment_data for segment_data in isis_instance if neighbor == segment_data["ipAddress"]), None, diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index aec4f0dfd..0880705b6 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -59,7 +59,27 @@ } } } - } + }, + "customer": { + "isisInstances": { + "CORE-ISIS": { + "neighbors": { + "0168.0000.0112": { + "adjacencies": [ + { + "hostname": "s1-p02", + "circuitId": "87", + "interfaceName": "Ethernet2", + "state": "down", + "lastHelloTime": 1713688405, + "routerIdV4": "1.0.0.112", + } + ] + } + } + } + } + }, } }, ], @@ -91,31 +111,31 @@ }, }, }, - "customer": { - "isisInstances": { - "CORE-ISIS": { - "neighbors": { - "0168.0000.0112": { - "adjacencies": [ - { - "hostname": "s1-p02", - "circuitId": "87", - "interfaceName": "Ethernet2", - "state": "up", - "lastHelloTime": 1713688405, - "routerIdV4": "1.0.0.112", - } - ] - } + }, + "customer": { + "isisInstances": { + "CORE-ISIS": { + "neighbors": { + "0168.0000.0112": { + "adjacencies": [ + { + "hostname": "s1-p02", + "circuitId": "87", + "interfaceName": "Ethernet2", + "state": "up", + "lastHelloTime": 1713688405, + "routerIdV4": "1.0.0.112", + } + ] } } } - }, - } + } + }, } }, ], - "inputs": None, + "inputs": {"check_all_vrfs": True}, "expected": {"result": "success"}, }, { @@ -177,6 +197,61 @@ "messages": ["IS-IS is not configured on device"], }, }, + { + "name": "failure different vrfs", + "test": VerifyISISNeighborState, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "neighbors": { + "0168.0000.0111": { + "adjacencies": [ + { + "hostname": "s1-p01", + "circuitId": "83", + "interfaceName": "Ethernet1", + "state": "up", + "lastHelloTime": 1713688408, + "routerIdV4": "1.0.0.111", + } + ] + }, + }, + }, + }, + }, + "customer": { + "isisInstances": { + "CORE-ISIS": { + "neighbors": { + "0168.0000.0112": { + "adjacencies": [ + { + "hostname": "s1-p02", + "circuitId": "87", + "interfaceName": "Ethernet2", + "state": "down", + "lastHelloTime": 1713688405, + "routerIdV4": "1.0.0.112", + } + ] + } + } + } + } + }, + } + }, + ], + "inputs": {"check_all_vrfs": True}, + "expected": { + "result": "failure", + "messages": ["Instance: CORE-ISIS VRF: customer Interface: Ethernet2 - Session (adjacency) down"], + }, + }, { "name": "success only default vrf", "test": VerifyISISNeighborCount, From d31f93c09913691acf8ffd8990ad9c05022185a4 Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Wed, 5 Feb 2025 08:12:01 -0500 Subject: [PATCH 08/13] Update a few things --- anta/input_models/routing/isis.py | 26 +++--- anta/tests/routing/isis.py | 133 ++++++++++++++++-------------- examples/tests.yaml | 14 ++-- 3 files changed, 89 insertions(+), 84 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index 91f092335..172043276 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -20,11 +20,11 @@ class ISISInstance(BaseModel): name: str """The name of the IS-IS instance.""" vrf: str = "default" - """VRF context where the IS-IS instance is configured. Defaults to `default`.""" + """VRF context of the IS-IS instance.""" dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured dataplane for the IS-IS instance. Required field in the `VerifyISISSegmentRoutingDataplane` test.""" + """Configured dataplane for the IS-IS instance.""" segments: list[Segment] | None = None - """A list of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" + """List of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInstance for reporting.""" @@ -36,13 +36,13 @@ class Segment(BaseModel): model_config = ConfigDict(extra="forbid") interface: Interface - """Interface name to check.""" + """Interface name.""" level: Literal[1, 2] = 2 - """IS-IS level configured for interface. Default is 2.""" + """IS-IS level configured.""" sid_origin: Literal["dynamic"] = "dynamic" - "Specifies the origin of the Segment ID." + """Specifies the origin of the Segment ID.""" address: IPv4Address - """IP address of the remote end of the segment(segment endpoint).""" + """IPv4 address of the remote end of the segment.""" def __str__(self) -> str: """Return a human-readable string representation of the Segment for reporting.""" @@ -50,19 +50,19 @@ def __str__(self) -> str: class ISISInterface(BaseModel): - """Model for a IS-IS Interface.""" + """Model for an IS-IS enabled interface.""" model_config = ConfigDict(extra="forbid") name: Interface - """Interface name to check.""" + """Interface name.""" vrf: str = "default" - """VRF name where IS-IS instance is configured.""" + """VRF context of the interface.""" level: Literal[1, 2] = 2 - """IS-IS level (1 or 2) configured for the interface. Default is 2.""" + """IS-IS level of the interface.""" count: int | None = None - """The total number of IS-IS neighbors associated with interface.""" + """Expected number of IS-IS neighbors on this interface. Required field in the `VerifyISISNeighborCount` test.""" mode: Literal["point-to-point", "broadcast", "passive"] | None = None - """The configured IS-IS circuit type for this interface.""" + """IS-IS network type of the interface. Required field in the `VerifyISISInterfaceMode` test.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInterface for reporting.""" diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index a0ad41097..8e92a6f67 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -9,6 +9,8 @@ from typing import Any, ClassVar +from pydantic import field_validator + from anta.input_models.routing.isis import ISISInstance, ISISInterface, SRTunnelEntry from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_value @@ -27,13 +29,13 @@ def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: s class VerifyISISNeighborState(AntaTest): - """Verifies the health of all the IS-IS neighbors. + """Verifies the health of IS-IS neighbors. Expected Results ---------------- - * Success: The test will pass if all IS-IS neighbors are in UP state. + * Success: The test will pass if all IS-IS neighbors are in the `up` state. * Failure: The test will fail if any IS-IS neighbor adjacency is down. - * Skipped: The test will be skipped if no IS-IS neighbor is found. + * Skipped: The test will be skipped if IS-IS is not configured or no IS-IS neighbor is found. Examples -------- @@ -41,7 +43,7 @@ class VerifyISISNeighborState(AntaTest): anta.tests.routing: isis: - VerifyISISNeighborState: - check_all_vrfs: bool = False + check_all_vrfs: true ``` """ @@ -52,50 +54,45 @@ class Input(AntaTest.Input): """Input model for the VerifyISISNeighborState test.""" check_all_vrfs: bool = False - """If enabled it verifies the all ISIS instances in all the configured vrfs. Defaults to `False` and verified the `default` vrf only.""" + """If enabled, verifies IS-IS instances of all VRFs.""" @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISNeighborState.""" self.result.is_success() - # Verify if ISIS neighbors are configured. Skip the test if none are found. + # Verify if IS-IS is configured if not (command_output := self.instance_commands[0].json_output["vrfs"]): - self.result.is_skipped("IS-IS is not configured on device") + self.result.is_skipped("IS-IS not configured") return vrfs_to_check = command_output if not self.inputs.check_all_vrfs: vrfs_to_check = {"default": command_output["default"]} + no_neighbor = True for vrf, vrf_data in vrfs_to_check.items(): - for isis_instance, instace_data in vrf_data["isisInstances"].items(): - neighbors = instace_data.get("neighbors", {}) + for isis_instance, instance_data in vrf_data["isisInstances"].items(): + neighbors = instance_data["neighbors"] + if not neighbors: + continue + no_neighbor = False interfaces = [adj["interfaceName"] for neighbor in neighbors.values() for adj in neighbor["adjacencies"] if adj["state"] != "up"] for interface in interfaces: self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {interface} - Session (adjacency) down") + if no_neighbor: + self.result.is_skipped("No IS-IS neighbor detected") -class VerifyISISNeighborCount(AntaTest): - """Verifies the number of IS-IS neighbors. - - This test performs the following checks for each specified interface: - - 1. Validates the IS-IS neighbors configured on specified interface. - 2. Validates the number of IS-IS neighbors for each interface at specified level. - !! Warning - Test supports the `default` vrf only. +class VerifyISISNeighborCount(AntaTest): + """Verifies the number of IS-IS neighbors per level and per interface. Expected Results ---------------- - * Success: If all of the following occur: - - The IS-IS neighbors configured on specified interface. - - The number of IS-IS neighbors for each interface at specified level matches the given input. - * Failure: If any of the following occur: - - The IS-IS neighbors are not configured on specified interface. - - The number of IS-IS neighbors for each interface at specified level does not matches the given input. - * Skipped: The test will be skipped if no IS-IS neighbor is found. + * Success: The test will pass if the number of neighbors is correct. + * Failure: The test will fail if the number of neighbors is incorrect. + * Skipped: The test will be skipped if IS-IS is not configured or no IS-IS neighbor is found. Examples -------- @@ -112,12 +109,11 @@ class VerifyISISNeighborCount(AntaTest): count: 1 - name: Ethernet3 count: 2 - # level is set to 2 by default ``` """ categories: ClassVar[list[str]] = ["isis"] - commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief", revision=1)] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief vrf all", revision=1)] class Input(AntaTest.Input): """Input model for the VerifyISISNeighborCount test.""" @@ -131,41 +127,36 @@ def test(self) -> None: """Main test function for VerifyISISNeighborCount.""" self.result.is_success() - command_output = self.instance_commands[0].json_output - - # Verify if ISIS neighbors are configured. Skip the test if none are found. - if not (instance_detail := get_value(command_output, "vrfs..default..isisInstances", separator="..")): - self.result.is_skipped("IS-IS is not configured on device") + # Verify if IS-IS is configured + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("IS-IS not configured") return for interface in self.inputs.interfaces: interface_detail = {} - for instance_data in instance_detail.values(): + vrf_instances = get_value(command_output, f"{interface.vrf}.isisInstances", separator="..") + for instance_data in vrf_instances.values(): if interface_data := get_value(instance_data, f"interfaces..{interface.name}..intfLevels..{interface.level}", separator=".."): interface_detail = interface_data + # An interface can only be configured in one IS-IS instance at a time break if not interface_detail: self.result.is_failure(f"{interface} - Not configured") continue - if (act_count := interface_detail.get("numAdjacencies", 0)) != interface.count: + if interface_detail["passive"] is False and (act_count := interface_detail["numAdjacencies"]) != interface.count: self.result.is_failure(f"{interface} - Neighbor count mismatch - Expected: {interface.count} Actual: {act_count}") class VerifyISISInterfaceMode(AntaTest): - """Verifies the operational mode of IS-IS Interfaces. - - This test performs the following checks: - - 1. Validates that all specified IS-IS interfaces are configured. - 2. Validates the operational mode of each IS-IS interface (e.g., "active," "passive," or "unset"). + """Verifies the IS-IS interface mode. Expected Results ---------------- - * Success: The test will pass if all listed interfaces are running in correct mode. - * Failure: The test will fail if any of the listed interfaces is not running in correct mode. - * Skipped: The test will be skipped if no ISIS neighbor is found. + * Success: The test will pass if all listed interfaces are running in the correct mode. + * Failure: The test will fail if any of the listed interfaces are not running in the correct mode. + * Skipped: The test will be skipped if IS-IS is not configured or no IS-IS neighbor is found. Examples -------- @@ -176,15 +167,12 @@ class VerifyISISInterfaceMode(AntaTest): interfaces: - name: Loopback0 mode: passive - # vrf is set to default by default - name: Ethernet2 mode: passive level: 2 - # vrf is set to default by default - name: Ethernet1 mode: point-to-point - vrf: default - # level is set to 2 by default + vrf: PROD ``` """ @@ -192,10 +180,10 @@ class VerifyISISInterfaceMode(AntaTest): commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show isis interface brief vrf all", revision=1)] class Input(AntaTest.Input): - """Input model for the VerifyISISNeighborCount test.""" + """Input model for the VerifyISISInterfaceMode test.""" interfaces: list[ISISInterface] - """list of interfaces with their information.""" + """List of IS-IS interfaces with their information.""" InterfaceState: ClassVar[type[ISISInterface]] = ISISInterface @AntaTest.anta_test @@ -203,19 +191,18 @@ def test(self) -> None: """Main test function for VerifyISISInterfaceMode.""" self.result.is_success() - command_output = self.instance_commands[0].json_output - - # Verify if ISIS neighbors are configured. Skip the test if none are found. - if len(command_output["vrfs"]) == 0: - self.result.is_skipped("No IS-IS neighbor detected") + # Verify if IS-IS is configured + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("IS-IS not configured") return for interface in self.inputs.interfaces: interface_detail = {} - instance_detail = get_value(command_output, f"vrfs..{interface.vrf}..isisInstances", separator="..", default={}) - for instance_data in instance_detail.values(): + vrf_instances = get_value(command_output, f"{interface.vrf}..isisInstances", separator="..") + for instance_data in vrf_instances.values(): if interface_data := get_value(instance_data, f"interfaces..{interface.name}", separator=".."): interface_detail = interface_data + # An interface can only be configured in one IS-IS instance at a time break if not interface_detail: @@ -223,11 +210,13 @@ def test(self) -> None: continue # Check for passive - if interface.mode == "passive" and get_value(interface_detail, f"intfLevels.{interface.level}.passive", default=False) is False: - self.result.is_failure(f"{interface} - Not running in passive mode") - - if interface.mode not in ("passive", interface_type := get_value(interface_detail, "interfaceType", default="unset")): - self.result.is_failure(f"{interface} - Incorrect circuit type - Expected: {interface.mode} Actual: {interface_type}") + if interface.mode == "passive": + if get_value(interface_detail, f"intfLevels.{interface.level}.passive", default=False) is False: + self.result.is_failure(f"{interface} - Not running in passive mode") + continue + # Check for point-to-point or broadcast + elif interface.mode != (interface_type := get_value(interface_detail, "interfaceType", default="unset")): + self.result.is_failure(f"{interface} - Incorrect interface mode - Expected: {interface.mode} Actual: {interface_type}") class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): @@ -262,9 +251,19 @@ class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingAdjacencySegments test.""" instances: list[ISISInstance] - """list of ISIS instances with their information.""" + """List of IS-IS instances with their information.""" IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance + @field_validator("instances") + @classmethod + def validate_instances(cls, instances: list[ISISInstance]) -> list[ISISInstance]: + """Validate that 'vrf' field is 'default' in each IS-IS instance.""" + for instance in instances: + if instance.vrf != "default": + msg = f"{instance} 'vrf' field must be 'default'. As of EOS 4.33.1F, IS-IS SR is supported only in default VRF." + raise ValueError(msg) + return instances + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISSegmentRoutingAdjacencySegments.""" @@ -351,6 +350,16 @@ class Input(AntaTest.Input): """list of ISIS instances with their information.""" IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance + @field_validator("instances") + @classmethod + def validate_instances(cls, instances: list[ISISInstance]) -> list[ISISInstance]: + """Validate that 'vrf' field is 'default' in each IS-IS instance.""" + for instance in instances: + if instance.vrf != "default": + msg = f"{instance} 'vrf' field must be 'default'. As of EOS 4.33.1F, IS-IS SR is supported only in default VRF." + raise ValueError(msg) + return instances + @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyISISSegmentRoutingDataplane.""" diff --git a/examples/tests.yaml b/examples/tests.yaml index b554a77d9..9552ec788 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -595,21 +595,18 @@ anta.tests.routing.generic: maximum: 20 anta.tests.routing.isis: - VerifyISISInterfaceMode: - # Verifies the operational mode of IS-IS Interfaces. + # Verifies the IS-IS interface mode. interfaces: - name: Loopback0 mode: passive - # vrf is set to default by default - name: Ethernet2 mode: passive level: 2 - # vrf is set to default by default - name: Ethernet1 mode: point-to-point - vrf: default - # level is set to 2 by default + vrf: PROD - VerifyISISNeighborCount: - # Verifies the number of IS-IS neighbors. + # Verifies the number of IS-IS neighbors per level and per interface. interfaces: - name: Ethernet1 level: 1 @@ -619,10 +616,9 @@ anta.tests.routing.isis: count: 1 - name: Ethernet3 count: 2 - # level is set to 2 by default - VerifyISISNeighborState: - # Verifies the health of all the IS-IS neighbors. - check_all_vrfs: bool = False + # Verifies the health of IS-IS neighbors. + check_all_vrfs: true - VerifyISISSegmentRoutingAdjacencySegments: # Verifies the ISIS SR Adjacency Segments. instances: From e35481fefc9c0c58aca198be908337dd8bef695f Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Wed, 5 Feb 2025 08:53:54 -0500 Subject: [PATCH 09/13] Update docstrings --- anta/tests/routing/isis.py | 16 ++++++++-------- examples/tests.yaml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 8e92a6f67..46fdc7ac8 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -86,13 +86,13 @@ def test(self) -> None: class VerifyISISNeighborCount(AntaTest): - """Verifies the number of IS-IS neighbors per level and per interface. + """Verifies the number of IS-IS neighbors per interface and level. Expected Results ---------------- - * Success: The test will pass if the number of neighbors is correct. - * Failure: The test will fail if the number of neighbors is incorrect. - * Skipped: The test will be skipped if IS-IS is not configured or no IS-IS neighbor is found. + * Success: The test will pass if all of the provided IS-IS interfaces have the expected number of neighbors. + * Failure: The test will fail if any of the provided IS-IS interfaces are not configured or have an incorrect number of neighbors. + * Skipped: The test will be skipped if IS-IS is not configured. Examples -------- @@ -150,13 +150,13 @@ def test(self) -> None: class VerifyISISInterfaceMode(AntaTest): - """Verifies the IS-IS interface mode. + """Verifies IS-IS interfaces are running in the correct mode. Expected Results ---------------- - * Success: The test will pass if all listed interfaces are running in the correct mode. - * Failure: The test will fail if any of the listed interfaces are not running in the correct mode. - * Skipped: The test will be skipped if IS-IS is not configured or no IS-IS neighbor is found. + * Success: The test will pass if the provided IS-IS interfaces are running in the correct mode. + * Failure: The test will fail if any of the provided IS-IS interfaces are not configured or running in the incorrect mode. + * Skipped: The test will be skipped if IS-IS is not configured. Examples -------- diff --git a/examples/tests.yaml b/examples/tests.yaml index 9552ec788..893823641 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -595,7 +595,7 @@ anta.tests.routing.generic: maximum: 20 anta.tests.routing.isis: - VerifyISISInterfaceMode: - # Verifies the IS-IS interface mode. + # Verifies IS-IS interfaces are running in the correct mode. interfaces: - name: Loopback0 mode: passive @@ -606,7 +606,7 @@ anta.tests.routing.isis: mode: point-to-point vrf: PROD - VerifyISISNeighborCount: - # Verifies the number of IS-IS neighbors per level and per interface. + # Verifies the number of IS-IS neighbors per interface and level. interfaces: - name: Ethernet1 level: 1 From 64a7088ef635e5c4faa427310a4f72e93b455d70 Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Wed, 5 Feb 2025 14:16:22 -0500 Subject: [PATCH 10/13] Update unit tests --- .codespellignore | 1 + .pre-commit-config.yaml | 1 + anta/input_models/routing/isis.py | 58 +----- anta/tests/routing/isis.py | 191 +++++++++--------- examples/tests.yaml | 4 +- tests/units/anta_tests/routing/test_isis.py | 202 +++++--------------- 6 files changed, 151 insertions(+), 306 deletions(-) create mode 100644 .codespellignore diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 000000000..a6d3a93ce --- /dev/null +++ b/.codespellignore @@ -0,0 +1 @@ +toi \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a47b84e6d..742354d4d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,6 +83,7 @@ repos: entry: codespell language: python types: [text] + args: ["--ignore-words", ".codespellignore"] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.14.1 diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index 172043276..2be3ec1ba 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -5,7 +5,7 @@ from __future__ import annotations -from ipaddress import IPv4Address, IPv4Network +from ipaddress import IPv4Address from typing import Literal from pydantic import BaseModel, ConfigDict @@ -22,7 +22,7 @@ class ISISInstance(BaseModel): vrf: str = "default" """VRF context of the IS-IS instance.""" dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured dataplane for the IS-IS instance.""" + """Configured data-plane for the IS-IS instance.""" segments: list[Segment] | None = None """List of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" @@ -32,21 +32,21 @@ def __str__(self) -> str: class Segment(BaseModel): - """Segment model definition.""" + """Model for an IS-IS segment.""" model_config = ConfigDict(extra="forbid") interface: Interface - """Interface name.""" + """Local interface name.""" level: Literal[1, 2] = 2 - """IS-IS level configured.""" + """IS-IS level of the segment.""" sid_origin: Literal["dynamic"] = "dynamic" - """Specifies the origin of the Segment ID.""" + """Origin of the segment ID.""" address: IPv4Address - """IPv4 address of the remote end of the segment.""" + """Adjacency IPv4 address of the segment.""" def __str__(self) -> str: """Return a human-readable string representation of the Segment for reporting.""" - return f"Interface: {self.interface} IP Addr: {self.address}" + return f"Local Intf: {self.interface} Adj IP Address: {self.address}" class ISISInterface(BaseModel): @@ -67,45 +67,3 @@ class ISISInterface(BaseModel): def __str__(self) -> str: """Return a human-readable string representation of the ISISInterface for reporting.""" return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level}" - - -class SRTunnelEntry(BaseModel): - """Model for a IS-IS SR tunnel.""" - - model_config = ConfigDict(extra="forbid") - endpoint: IPv4Network - """Endpoint of the tunnel.""" - vias: list[TunnelPath] | None = None - """Optional list of path to reach endpoint.""" - - def __str__(self) -> str: - """Return a human-readable string representation of the SRTunnelEntry for reporting.""" - return f"Endpoint: {self.endpoint}" - - -class TunnelPath(BaseModel): - """Model for a IS-IS tunnel path.""" - - model_config = ConfigDict(extra="forbid") - nexthop: IPv4Address | None = None - """Nexthop of the tunnel.""" - type: Literal["ip", "tunnel"] | None = None - """Type of the tunnel.""" - interface: Interface | None = None - """Interface of the tunnel.""" - tunnel_id: Literal["TI-LFA", "ti-lfa", "unset"] | None = None - """Computation method of the tunnel.""" - - def __str__(self) -> str: - """Return a human-readable string representation of the TunnelPath for reporting.""" - base_string = "" - if self.nexthop: - base_string += f" Next-hop: {self.nexthop}" - if self.type: - base_string += f" Type: {self.type}" - if self.interface: - base_string += f" Interface: {self.interface}" - if self.tunnel_id: - base_string += f" TunnelID: {self.tunnel_id}" - - return base_string.lstrip() diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index 46fdc7ac8..e4c6074b0 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -7,25 +7,15 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import Any, ClassVar +from ipaddress import IPv4Address, IPv4Network +from typing import Any, ClassVar, Literal -from pydantic import field_validator +from pydantic import BaseModel, field_validator -from anta.input_models.routing.isis import ISISInstance, ISISInterface, SRTunnelEntry +from anta.custom_types import Interface +from anta.input_models.routing.isis import ISISInstance, ISISInterface from anta.models import AntaCommand, AntaTemplate, AntaTest -from anta.tools import get_value - - -def _get_adjacency_segment_data_by_neighbor(neighbor: str, instance: str, vrf: str, command_output: dict[str, Any]) -> dict[str, Any] | None: - """Extract data related to an IS-IS interface for testing.""" - search_path = f"vrfs.{vrf}.isisInstances.{instance}.adjacencySegments" - if (isis_instance := get_value(dictionary=command_output, key=search_path, default=None)) is None: - return None - - return next( - (segment_data for segment_data in isis_instance if neighbor == segment_data["ipAddress"]), - None, - ) +from anta.tools import get_item, get_value class VerifyISISNeighborState(AntaTest): @@ -79,7 +69,7 @@ def test(self) -> None: no_neighbor = False interfaces = [adj["interfaceName"] for neighbor in neighbors.values() for adj in neighbor["adjacencies"] if adj["state"] != "up"] for interface in interfaces: - self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {interface} - Session (adjacency) down") + self.result.is_failure(f"Instance: {isis_instance} VRF: {vrf} Interface: {interface} - Adjacency down") if no_neighbor: self.result.is_skipped("No IS-IS neighbor detected") @@ -90,7 +80,7 @@ class VerifyISISNeighborCount(AntaTest): Expected Results ---------------- - * Success: The test will pass if all of the provided IS-IS interfaces have the expected number of neighbors. + * Success: The test will pass if all provided IS-IS interfaces have the expected number of neighbors. * Failure: The test will fail if any of the provided IS-IS interfaces are not configured or have an incorrect number of neighbors. * Skipped: The test will be skipped if IS-IS is not configured. @@ -134,7 +124,7 @@ def test(self) -> None: for interface in self.inputs.interfaces: interface_detail = {} - vrf_instances = get_value(command_output, f"{interface.vrf}.isisInstances", separator="..") + vrf_instances = get_value(command_output, f"{interface.vrf}..isisInstances", default={}, separator="..") for instance_data in vrf_instances.values(): if interface_data := get_value(instance_data, f"interfaces..{interface.name}..intfLevels..{interface.level}", separator=".."): interface_detail = interface_data @@ -154,7 +144,7 @@ class VerifyISISInterfaceMode(AntaTest): Expected Results ---------------- - * Success: The test will pass if the provided IS-IS interfaces are running in the correct mode. + * Success: The test will pass if all provided IS-IS interfaces are running in the correct mode. * Failure: The test will fail if any of the provided IS-IS interfaces are not configured or running in the incorrect mode. * Skipped: The test will be skipped if IS-IS is not configured. @@ -198,7 +188,7 @@ def test(self) -> None: for interface in self.inputs.interfaces: interface_detail = {} - vrf_instances = get_value(command_output, f"{interface.vrf}..isisInstances", separator="..") + vrf_instances = get_value(command_output, f"{interface.vrf}..isisInstances", default={}, separator="..") for instance_data in vrf_instances.values(): if interface_data := get_value(instance_data, f"interfaces..{interface.name}", separator=".."): interface_detail = interface_data @@ -220,13 +210,18 @@ def test(self) -> None: class VerifyISISSegmentRoutingAdjacencySegments(AntaTest): - """Verifies the ISIS SR Adjacency Segments. + """Verifies IS-IS segment routing adjacency segments. + + !!! warning "IS-IS SR Limitation" + As of EOS 4.33.1F, IS-IS SR is supported only in the default VRF. + Please refer to the IS-IS Segment Routing [documentation](https://www.arista.com/en/support/toi/eos-4-17-0f/13789-isis-segment-routing) + for more information. Expected Results ---------------- - * Success: The test will pass if all listed interfaces have correct adjacencies. - * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. - * Skipped: The test will be skipped if no ISIS SR Adjacency is found. + * Success: The test will pass if all provided IS-IS instances have the correct adjacency segments. + * Failure: The test will fail if any of the provided IS-IS instances have no adjacency segments or incorrect segments. + * Skipped: The test will be skipped if IS-IS is not configured. Examples -------- @@ -260,7 +255,7 @@ def validate_instances(cls, instances: list[ISISInstance]) -> list[ISISInstance] """Validate that 'vrf' field is 'default' in each IS-IS instance.""" for instance in instances: if instance.vrf != "default": - msg = f"{instance} 'vrf' field must be 'default'. As of EOS 4.33.1F, IS-IS SR is supported only in default VRF." + msg = f"{instance} 'vrf' field must be 'default'" raise ValueError(msg) return instances @@ -269,63 +264,43 @@ def test(self) -> None: """Main test function for VerifyISISSegmentRoutingAdjacencySegments.""" self.result.is_success() - command_output = self.instance_commands[0].json_output - if len(command_output["vrfs"]) == 0: - self.result.is_skipped("No IS-IS neighbor detected") + # Verify if IS-IS is configured + if not (command_output := self.instance_commands[0].json_output["vrfs"]): + self.result.is_skipped("IS-IS not configured") return - # Check if VRFs and instances are present in output. for instance in self.inputs.instances: - vrf_data = get_value( - dictionary=command_output, - key=f"vrfs.{instance.vrf}", - default=None, - ) - if vrf_data is None: - self.result.is_failure(f"{instance} - VRF not configured") - continue - - if get_value(dictionary=vrf_data, key=f"isisInstances.{instance.name}", default=None) is None: - self.result.is_failure(f"{instance} - Not configured") + if not (act_segments := get_value(command_output, f"{instance.vrf}..isisInstances..{instance.name}..adjacencySegments", default=[], separator="..")): + self.result.is_failure(f"{instance} - No adjacency segments found") continue - for input_segment in instance.segments: - eos_segment = _get_adjacency_segment_data_by_neighbor( - neighbor=str(input_segment.address), - instance=instance.name, - vrf=instance.vrf, - command_output=command_output, - ) - if eos_segment is None: - self.result.is_failure(f"{instance} {input_segment} - Segment not configured") + for segment in instance.segments: + if (act_segment := get_item(act_segments, "ipAddress", str(segment.address))) is None: + self.result.is_failure(f"{instance} {segment} - Adjacency segment not found") continue - if (act_origin := eos_segment["sidOrigin"]) != input_segment.sid_origin: - self.result.is_failure( - f"{instance} {input_segment} - Incorrect Segment Identifier origin - Expected: {input_segment.sid_origin} Actual: {act_origin}" - ) + # Check SID origin + if (act_origin := act_segment["sidOrigin"]) != segment.sid_origin: + self.result.is_failure(f"{instance} {segment} - Incorrect SID origin - Expected: {segment.sid_origin} Actual: {act_origin}") - if (actual_level := eos_segment["level"]) != input_segment.level: - self.result.is_failure(f"{instance} {input_segment} - Incorrect IS-IS level - Expected: {input_segment.level} Actual: {actual_level}") + # Check IS-IS level + if (actual_level := act_segment["level"]) != segment.level: + self.result.is_failure(f"{instance} {segment} - Incorrect IS-IS level - Expected: {segment.level} Actual: {actual_level}") class VerifyISISSegmentRoutingDataplane(AntaTest): - """Verifies the Dataplane of ISIS-SR Instances. - - This test performs the following checks: + """Verifies IS-IS segment routing data-plane configuration. - 1. Validates that listed ISIS-SR instance exists. - 2. Validates the configured dataplane matches the expected value for each instance. + !!! warning "IS-IS SR Limitation" + As of EOS 4.33.1F, IS-IS SR is supported only in the default VRF. + Please refer to the IS-IS Segment Routing [documentation](https://www.arista.com/en/support/toi/eos-4-17-0f/13789-isis-segment-routing) + for more information. Expected Results ---------------- - * Success: If all of the following occur: - - All specified ISIS-SR instances are configured. - - Each instance has the correct dataplane. - * Failure: If any of the following occur: - - Any specified ISIS-SR instance is not configured. - - Any instance has an incorrect dataplane configuration. - * Skipped: The test will be skipped if no IS-IS neighbor is found. + * Success: The test will pass if all provided IS-IS instances have the correct data-plane configured. + * Failure: The test will fail if any of the provided IS-IS instances have an incorrect data-plane configured. + * Skipped: The test will be skipped if IS-IS is not configured. Examples -------- @@ -347,7 +322,7 @@ class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingDataplane test.""" instances: list[ISISInstance] - """list of ISIS instances with their information.""" + """List of IS-IS instances with their information.""" IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance @field_validator("instances") @@ -356,7 +331,7 @@ def validate_instances(cls, instances: list[ISISInstance]) -> list[ISISInstance] """Validate that 'vrf' field is 'default' in each IS-IS instance.""" for instance in instances: if instance.vrf != "default": - msg = f"{instance} 'vrf' field must be 'default'. As of EOS 4.33.1F, IS-IS SR is supported only in default VRF." + msg = f"{instance} 'vrf' field must be 'default'" raise ValueError(msg) return instances @@ -365,18 +340,18 @@ def test(self) -> None: """Main test function for VerifyISISSegmentRoutingDataplane.""" self.result.is_success() + # Verify if IS-IS is configured if not (command_output := self.instance_commands[0].json_output["vrfs"]): - self.result.is_skipped("No IS-IS neighbor detected") + self.result.is_skipped("IS-IS not configured") return - # Check if VRFs and instances are present in output. for instance in self.inputs.instances: if not (instance_data := get_value(command_output, f"{instance.vrf}..isisInstances..{instance.name}", separator="..")): self.result.is_failure(f"{instance} - Not configured") continue - if instance.dataplane.upper() != (plane := instance_data["dataPlane"]): - self.result.is_failure(f"{instance} - Dataplane not correctly configured - Expected: {instance.dataplane.upper()} Actual: {plane}") + if instance.dataplane.upper() != (dataplane := instance_data["dataPlane"]): + self.result.is_failure(f"{instance} - Data-plane not correctly configured - Expected: {instance.dataplane.upper()} Actual: {dataplane}") class VerifyISISSegmentRoutingTunnels(AntaTest): @@ -416,12 +391,32 @@ class VerifyISISSegmentRoutingTunnels(AntaTest): class Input(AntaTest.Input): """Input model for the VerifyISISSegmentRoutingTunnels test.""" - entries: list[SRTunnelEntry] + entries: list[Entry] """List of tunnels to check on device.""" - def _eos_entry_lookup(self, search_value: str, entries: dict[str, Any], search_key: str = "endpoint") -> dict[str, Any] | None: + class Entry(BaseModel): + """Definition of a tunnel entry.""" + + endpoint: IPv4Network + """Endpoint IP of the tunnel.""" + vias: list[Vias] | None = None + """Optional list of path to reach endpoint.""" + + class Vias(BaseModel): + """Definition of a tunnel path.""" + + nexthop: IPv4Address | None = None + """Nexthop of the tunnel. If None, then it is not tested. Default: None""" + type: Literal["ip", "tunnel"] | None = None + """Type of the tunnel. If None, then it is not tested. Default: None""" + interface: Interface | None = None + """Interface of the tunnel. If None, then it is not tested. Default: None""" + tunnel_id: Literal["TI-LFA", "ti-lfa", "unset"] | None = None + """Computation method of the tunnel. If None, then it is not tested. Default: None""" + + def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = "endpoint") -> dict[str, Any] | None: return next( - (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == search_value), + (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)), None, ) @@ -435,32 +430,41 @@ def test(self) -> None: command_output = self.instance_commands[0].json_output self.result.is_success() + # initiate defaults + failure_message = [] + if len(command_output["entries"]) == 0: self.result.is_skipped("IS-IS-SR is not running on device.") return for input_entry in self.inputs.entries: - eos_entry = self._eos_entry_lookup(search_value=str(input_entry.endpoint), entries=command_output["entries"]) + eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output["entries"]) if eos_entry is None: - self.result.is_failure(f"{input_entry} - Tunnel not found") + failure_message.append(f"Tunnel to {input_entry} is not found.") elif input_entry.vias is not None: + failure_src = [] for via_input in input_entry.vias: if not self._check_tunnel_type(via_input, eos_entry): - self.result.is_failure(f"{input_entry} {via_input} - incorrect tunnel type") + failure_src.append("incorrect tunnel type") if not self._check_tunnel_nexthop(via_input, eos_entry): - self.result.is_failure(f"{input_entry} {via_input} - incorrect nexthop") + failure_src.append("incorrect nexthop") if not self._check_tunnel_interface(via_input, eos_entry): - self.result.is_failure(f"{input_entry} {via_input} - incorrect interface") + failure_src.append("incorrect interface") if not self._check_tunnel_id(via_input, eos_entry): - self.result.is_failure(f"{input_entry} {via_input} - incorrect tunnel ID") + failure_src.append("incorrect tunnel ID") + + if failure_src: + failure_message.append(f"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}") + + if failure_message: + self.result.is_failure("\n".join(failure_message)) - def _check_tunnel_type(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: """Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`. Parameters ---------- - via_input : dict[str, Any] - VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input tunnel type to check. eos_entry : dict[str, Any] The EOS entry containing the tunnel types. @@ -482,13 +486,12 @@ def _check_tunnel_type(self, via_input: dict[str, Any], eos_entry: dict[str, Any ) return True - def _check_tunnel_nexthop(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: """Check if the tunnel nexthop matches the given input. Parameters ---------- - via_input : dict[str, Any] - VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input via object. eos_entry : dict[str, Any] The EOS entry dictionary. @@ -510,13 +513,12 @@ def _check_tunnel_nexthop(self, via_input: dict[str, Any], eos_entry: dict[str, ) return True - def _check_tunnel_interface(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: """Check if the tunnel interface exists in the given EOS entry. Parameters ---------- - via_input : dict[str, Any] - VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input via object. eos_entry : dict[str, Any] The EOS entry dictionary. @@ -538,13 +540,12 @@ def _check_tunnel_interface(self, via_input: dict[str, Any], eos_entry: dict[str ) return True - def _check_tunnel_id(self, via_input: dict[str, Any], eos_entry: dict[str, Any]) -> bool: + def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool: """Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias. Parameters ---------- - via_input : dict[str, Any] - VerifyISISSegmentRoutingTunnels.Input.Entry.Vias + via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias The input vias to check. eos_entry : dict[str, Any]) The EOS entry to compare against. diff --git a/examples/tests.yaml b/examples/tests.yaml index 893823641..1f9d9b678 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -620,7 +620,7 @@ anta.tests.routing.isis: # Verifies the health of IS-IS neighbors. check_all_vrfs: true - VerifyISISSegmentRoutingAdjacencySegments: - # Verifies the ISIS SR Adjacency Segments. + # Verifies IS-IS segment routing adjacency segments. instances: - name: CORE-ISIS vrf: default @@ -629,7 +629,7 @@ anta.tests.routing.isis: address: 10.0.1.3 sid_origin: dynamic - VerifyISISSegmentRoutingDataplane: - # Verifies the Dataplane of ISIS-SR Instances. + # Verifies IS-IS segment routing data-plane configuration. instances: - name: CORE-ISIS vrf: default diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index 0880705b6..c0ab80376 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -23,7 +23,7 @@ DATA: list[dict[str, Any]] = [ { - "name": "success only default vrf", + "name": "success-default-vrf", "test": VerifyISISNeighborState, "eos_data": [ { @@ -87,7 +87,7 @@ "expected": {"result": "success"}, }, { - "name": "success different vrfs", + "name": "success-multiple-vrfs", "test": VerifyISISNeighborState, "eos_data": [ { @@ -182,11 +182,11 @@ "inputs": None, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet1 - Session (adjacency) down"], + "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet1 - Adjacency down"], }, }, { - "name": "skipped - no neighbor", + "name": "skipped-not-configured", "test": VerifyISISNeighborState, "eos_data": [ {"vrfs": {}}, @@ -194,11 +194,11 @@ "inputs": None, "expected": { "result": "skipped", - "messages": ["IS-IS is not configured on device"], + "messages": ["IS-IS not configured"], }, }, { - "name": "failure different vrfs", + "name": "failure-multiple-vrfs", "test": VerifyISISNeighborState, "eos_data": [ { @@ -249,11 +249,11 @@ "inputs": {"check_all_vrfs": True}, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: customer Interface: Ethernet2 - Session (adjacency) down"], + "messages": ["Instance: CORE-ISIS VRF: customer Interface: Ethernet2 - Adjacency down"], }, }, { - "name": "success only default vrf", + "name": "success-default-vrf", "test": VerifyISISNeighborCount, "eos_data": [ { @@ -325,10 +325,10 @@ "expected": {"result": "success"}, }, { - "name": "skipped - no neighbor", + "name": "skipped-not-configured", "test": VerifyISISNeighborCount, "eos_data": [ - {"vrfs": {"default": {"isisInstances": {}}}}, + {"vrfs": {}}, ], "inputs": { "interfaces": [ @@ -337,11 +337,11 @@ }, "expected": { "result": "skipped", - "messages": ["IS-IS is not configured on device"], + "messages": ["IS-IS not configured"], }, }, { - "name": "failure - missing interface", + "name": "failure-interface-not-configured", "test": VerifyISISNeighborCount, "eos_data": [ { @@ -384,7 +384,7 @@ }, }, { - "name": "failure - wrong count", + "name": "failure-wrong-count", "test": VerifyISISNeighborCount, "eos_data": [ { @@ -427,7 +427,7 @@ }, }, { - "name": "success VerifyISISInterfaceMode only default vrf", + "name": "success-default-vrf", "test": VerifyISISInterfaceMode, "eos_data": [ { @@ -509,7 +509,7 @@ "expected": {"result": "success"}, }, { - "name": "failure VerifyISISInterfaceMode default vrf with interface not running passive mode", + "name": "failure-interface-not-passive", "test": VerifyISISInterfaceMode, "eos_data": [ { @@ -594,7 +594,7 @@ }, }, { - "name": "failure VerifyISISInterfaceMode default vrf with interface not running point-point mode", + "name": "failure-interface-not-point-to-point", "test": VerifyISISInterfaceMode, "eos_data": [ { @@ -675,11 +675,11 @@ }, "expected": { "result": "failure", - "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect circuit type - Expected: point-to-point Actual: broadcast"], + "messages": ["Interface: Ethernet1 VRF: default Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast"], }, }, { - "name": "failure VerifyISISInterfaceMode default vrf with interface not running correct VRF mode", + "name": "failure-interface-wrong-vrf", "test": VerifyISISInterfaceMode, "eos_data": [ { @@ -768,7 +768,7 @@ }, }, { - "name": "skipped VerifyISISInterfaceMode no vrf", + "name": "skipped-not-configured", "test": VerifyISISInterfaceMode, "eos_data": [{"vrfs": {}}], "inputs": { @@ -778,10 +778,10 @@ {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, ] }, - "expected": {"result": "skipped", "messages": ["No IS-IS neighbor detected"]}, + "expected": {"result": "skipped", "messages": ["IS-IS not configured"]}, }, { - "name": "Skipped of VerifyISISSegmentRoutingAdjacencySegments no VRF.", + "name": "skipped-not-configured", "test": VerifyISISSegmentRoutingAdjacencySegments, "eos_data": [{"vrfs": {}}], "inputs": { @@ -799,11 +799,11 @@ } ] }, - "expected": {"result": "skipped", "messages": ["No IS-IS neighbor detected"]}, + "expected": {"result": "skipped", "messages": ["IS-IS not configured"]}, }, { "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "Success of VerifyISISSegmentRoutingAdjacencySegments in default VRF.", + "name": "success", "eos_data": [ { "vrfs": { @@ -881,7 +881,7 @@ }, { "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "Failure of VerifyISISSegmentRoutingAdjacencySegments in default VRF for incorrect segment definition.", + "name": "failure-segment-not-found", "eos_data": [ { "vrfs": { @@ -959,95 +959,12 @@ }, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet3 IP Addr: 10.0.1.2 - Segment not configured"], + "messages": ["Instance: CORE-ISIS VRF: default Local Intf: Ethernet3 Adj IP Address: 10.0.1.2 - Adjacency segment not found"], }, }, { "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "Failure of VerifyISISSegmentRoutingAdjacencySegments with incorrect VRF.", - "eos_data": [ - { - "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "dataPlane": "MPLS", - "routerId": "1.0.0.11", - "systemId": "0168.0000.0011", - "hostname": "s1-pe01", - "adjSidAllocationMode": "SrOnly", - "adjSidPoolBase": 116384, - "adjSidPoolSize": 16384, - "adjacencySegments": [ - { - "ipAddress": "10.0.1.3", - "localIntf": "Ethernet2", - "sid": 116384, - "lan": False, - "sidOrigin": "dynamic", - "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, - "level": 2, - }, - { - "ipAddress": "10.0.1.1", - "localIntf": "Ethernet1", - "sid": 116385, - "lan": False, - "sidOrigin": "dynamic", - "protection": "unprotected", - "flags": { - "b": False, - "v": True, - "l": True, - "f": False, - "s": False, - }, - "level": 2, - }, - ], - "receivedGlobalAdjacencySegments": [], - "misconfiguredAdjacencySegments": [], - } - } - } - } - } - ], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "custom", - "segments": [ - { - "interface": "Ethernet2", - "address": "10.0.1.3", - "sid_origin": "dynamic", - }, - { - "interface": "Ethernet3", - "address": "10.0.1.2", - "sid_origin": "dynamic", - }, - ], - } - ] - }, - "expected": { - "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: custom - VRF not configured"], - }, - }, - { - "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "Failure of VerifyISISSegmentRoutingAdjacencySegments with incorrect Instance.", + "name": "failure-no-segments-incorrect-instance", "eos_data": [ { "vrfs": { @@ -1125,12 +1042,12 @@ }, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS2 VRF: default - Not configured"], + "messages": ["Instance: CORE-ISIS2 VRF: default - No adjacency segments found"], }, }, { "test": VerifyISISSegmentRoutingAdjacencySegments, - "name": "Failure of VerifyISISSegmentRoutingAdjacencySegments with incorrect segment info.", + "name": "failure-incorrect-segment-level", "eos_data": [ { "vrfs": { @@ -1188,12 +1105,12 @@ }, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default Interface: Ethernet2 IP Addr: 10.0.1.3 - Incorrect IS-IS level - Expected: 1 Actual: 2"], + "messages": ["Instance: CORE-ISIS VRF: default Local Intf: Ethernet2 Adj IP Address: 10.0.1.3 - Incorrect IS-IS level - Expected: 1 Actual: 2"], }, }, { "test": VerifyISISSegmentRoutingDataplane, - "name": "Check VerifyISISSegmentRoutingDataplane is running successfully", + "name": "success", "eos_data": [ { "vrfs": { @@ -1226,7 +1143,7 @@ }, { "test": VerifyISISSegmentRoutingDataplane, - "name": "Check VerifyISISSegmentRoutingDataplane is failing with incorrect dataplane", + "name": "failure-incorrect-dataplane", "eos_data": [ { "vrfs": { @@ -1254,12 +1171,12 @@ }, "expected": { "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: default - Dataplane not correctly configured - Expected: UNSET Actual: MPLS"], + "messages": ["Instance: CORE-ISIS VRF: default - Data-plane not correctly configured - Expected: UNSET Actual: MPLS"], }, }, { "test": VerifyISISSegmentRoutingDataplane, - "name": "Check VerifyISISSegmentRoutingDataplane is failing for unknown instance", + "name": "failure-instance-not-configured", "eos_data": [ { "vrfs": { @@ -1292,53 +1209,20 @@ }, { "test": VerifyISISSegmentRoutingDataplane, - "name": "Check VerifyISISSegmentRoutingDataplane is failing for unknown VRF", - "eos_data": [ - { - "vrfs": { - "default": { - "isisInstances": { - "CORE-ISIS": { - "dataPlane": "MPLS", - "routerId": "1.0.0.11", - "systemId": "0168.0000.0011", - "hostname": "s1-pe01", - } - } - } - } - } - ], - "inputs": { - "instances": [ - { - "name": "CORE-ISIS", - "vrf": "wrong_vrf", - "dataplane": "unset", - }, - ] - }, - "expected": { - "result": "failure", - "messages": ["Instance: CORE-ISIS VRF: wrong_vrf - Not configured"], - }, - }, - { - "test": VerifyISISSegmentRoutingDataplane, - "name": "Check VerifyISISSegmentRoutingDataplane is skipped", + "name": "skipped-not-configured", "eos_data": [{"vrfs": {}}], "inputs": { "instances": [ { "name": "CORE-ISIS", - "vrf": "wrong_vrf", + "vrf": "default", "dataplane": "unset", }, ] }, "expected": { "result": "skipped", - "messages": ["No IS-IS neighbor detected"], + "messages": ["IS-IS not configured"], }, }, { @@ -1473,7 +1357,7 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 - Tunnel not found"], + "messages": ["Tunnel to endpoint=IPv4Network('1.0.0.122/32') vias=None is not found."], }, }, { @@ -1554,7 +1438,7 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.13/32 Type: tunnel - incorrect tunnel type"], + "messages": ["Tunnel to 1.0.0.13/32 is incorrect: incorrect tunnel type"], }, }, { @@ -1642,12 +1526,12 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - incorrect nexthop"], + "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect nexthop"], }, }, { "test": VerifyISISSegmentRoutingTunnels, - "name": "fails with incorrect interface", + "name": "fails with incorrect nexthop", "eos_data": [ { "entries": { @@ -1730,7 +1614,7 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.1 Type: ip Interface: Ethernet4 - incorrect interface"], + "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect interface"], }, }, { @@ -1818,7 +1702,7 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.122/32 Next-hop: 10.0.1.2 Type: ip Interface: Ethernet1 - incorrect nexthop"], + "messages": ["Tunnel to 1.0.0.122/32 is incorrect: incorrect nexthop"], }, }, { @@ -1905,7 +1789,7 @@ }, "expected": { "result": "failure", - "messages": ["Endpoint: 1.0.0.111/32 Type: tunnel TunnelID: unset - incorrect tunnel ID"], + "messages": ["Tunnel to 1.0.0.111/32 is incorrect: incorrect tunnel ID"], }, }, ] From 9a65e13c7ff0ed553082db213e2b7de579e7b8d8 Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Wed, 5 Feb 2025 14:31:41 -0500 Subject: [PATCH 11/13] Update docstrings --- anta/input_models/routing/isis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index 2be3ec1ba..e811930e2 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -1,7 +1,7 @@ # Copyright (c) 2023-2025 Arista Networks, Inc. # Use of this source code is governed by the Apache License 2.0 # that can be found in the LICENSE file. -"""Module containing input models for routing ISIS tests.""" +"""Module containing input models for routing IS-IS tests.""" from __future__ import annotations @@ -22,9 +22,9 @@ class ISISInstance(BaseModel): vrf: str = "default" """VRF context of the IS-IS instance.""" dataplane: Literal["MPLS", "mpls", "unset"] = "MPLS" - """Configured data-plane for the IS-IS instance.""" + """Configured SR data-plane for the IS-IS instance.""" segments: list[Segment] | None = None - """List of IS-IS segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" + """List of IS-IS SR segments associated with the instance. Required field in the `VerifyISISSegmentRoutingAdjacencySegments` test.""" def __str__(self) -> str: """Return a human-readable string representation of the ISISInstance for reporting.""" From 1c6a6b674dc023ea4bbaefc14e823a7a75ad70a3 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 6 Feb 2025 00:40:14 -0500 Subject: [PATCH 12/13] updated unit tests, added alias in input models for backward compatibility --- anta/input_models/routing/isis.py | 59 ++- anta/tests/routing/isis.py | 10 +- tests/units/anta_tests/routing/test_isis.py | 484 ++++++++++++++++++++ 3 files changed, 546 insertions(+), 7 deletions(-) diff --git a/anta/input_models/routing/isis.py b/anta/input_models/routing/isis.py index e811930e2..efeefe604 100644 --- a/anta/input_models/routing/isis.py +++ b/anta/input_models/routing/isis.py @@ -6,7 +6,8 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Literal +from typing import Any, Literal +from warnings import warn from pydantic import BaseModel, ConfigDict @@ -39,7 +40,7 @@ class Segment(BaseModel): """Local interface name.""" level: Literal[1, 2] = 2 """IS-IS level of the segment.""" - sid_origin: Literal["dynamic"] = "dynamic" + sid_origin: Literal["dynamic", "configured"] = "dynamic" """Origin of the segment ID.""" address: IPv4Address """Adjacency IPv4 address of the segment.""" @@ -67,3 +68,57 @@ class ISISInterface(BaseModel): def __str__(self) -> str: """Return a human-readable string representation of the ISISInterface for reporting.""" return f"Interface: {self.name} VRF: {self.vrf} Level: {self.level}" + + +class InterfaceCount(ISISInterface): # pragma: no cover + """Alias for the ISISInterface model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInterface model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the InterfaceCount class, emitting a deprecation warning.""" + warn( + message="InterfaceCount model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) + + +class InterfaceState(ISISInterface): # pragma: no cover + """Alias for the ISISInterface model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInterface model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the InterfaceState class, emitting a deprecation warning.""" + warn( + message="InterfaceState model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInterface model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) + + +class IsisInstance(ISISInstance): # pragma: no cover + """Alias for the ISISInstance model to maintain backward compatibility. + + When initialized, it will emit a deprecation warning and call the ISISInstance model. + + TODO: Remove this class in ANTA v2.0.0. + """ + + def __init__(self, **data: Any) -> None: # noqa: ANN401 + """Initialize the IsisInstance class, emitting a deprecation warning.""" + warn( + message="IsisInstance model is deprecated and will be removed in ANTA v2.0.0. Use the ISISInstance model instead.", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(**data) diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index e4c6074b0..f5d75cefa 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -13,7 +13,7 @@ from pydantic import BaseModel, field_validator from anta.custom_types import Interface -from anta.input_models.routing.isis import ISISInstance, ISISInterface +from anta.input_models.routing.isis import InterfaceCount, InterfaceState, ISISInstance, IsisInstance, ISISInterface from anta.models import AntaCommand, AntaTemplate, AntaTest from anta.tools import get_item, get_value @@ -110,7 +110,7 @@ class Input(AntaTest.Input): interfaces: list[ISISInterface] """List of IS-IS interfaces with their information.""" - InterfaceCount: ClassVar[type[ISISInterface]] = ISISInterface + InterfaceCount: ClassVar[type[InterfaceCount]] = InterfaceCount @AntaTest.anta_test def test(self) -> None: @@ -174,7 +174,7 @@ class Input(AntaTest.Input): interfaces: list[ISISInterface] """List of IS-IS interfaces with their information.""" - InterfaceState: ClassVar[type[ISISInterface]] = ISISInterface + InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState @AntaTest.anta_test def test(self) -> None: @@ -247,7 +247,7 @@ class Input(AntaTest.Input): instances: list[ISISInstance] """List of IS-IS instances with their information.""" - IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance + IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance @field_validator("instances") @classmethod @@ -323,7 +323,7 @@ class Input(AntaTest.Input): instances: list[ISISInstance] """List of IS-IS instances with their information.""" - IsisInstance: ClassVar[type[ISISInstance]] = ISISInstance + IsisInstance: ClassVar[type[IsisInstance]] = IsisInstance @field_validator("instances") @classmethod diff --git a/tests/units/anta_tests/routing/test_isis.py b/tests/units/anta_tests/routing/test_isis.py index c0ab80376..a3696ff81 100644 --- a/tests/units/anta_tests/routing/test_isis.py +++ b/tests/units/anta_tests/routing/test_isis.py @@ -252,6 +252,29 @@ "messages": ["Instance: CORE-ISIS VRF: customer Interface: Ethernet2 - Adjacency down"], }, }, + { + "name": "skipped-no-neighbor-detected", + "test": VerifyISISNeighborState, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "neighbors": {}, + }, + }, + }, + "customer": {"isisInstances": {"CORE-ISIS": {"neighbors": {}}}}, + } + }, + ], + "inputs": {"check_all_vrfs": True}, + "expected": { + "result": "skipped", + "messages": ["No IS-IS neighbor detected"], + }, + }, { "name": "success-default-vrf", "test": VerifyISISNeighborCount, @@ -324,6 +347,104 @@ }, "expected": {"result": "success"}, }, + { + "name": "success-multiple-VRFs", + "test": VerifyISISNeighborCount, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "interfaces": { + "Loopback0": { + "enabled": True, + "intfLevels": { + "2": { + "ipv4Metric": 10, + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": True, + "v4Protection": "disabled", + "v6Protection": "disabled", + } + }, + "areaProxyBoundary": False, + }, + "Ethernet1": { + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + "Ethernet2": { + "enabled": True, + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "88", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + "PROD": { + "isisInstances": { + "PROD-ISIS": { + "interfaces": { + "Ethernet3": { + "enabled": True, + "intfLevels": { + "1": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "88", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + } + }, + ], + "inputs": { + "interfaces": [ + {"name": "Ethernet1", "level": 2, "count": 1}, + {"name": "Ethernet2", "level": 2, "count": 1}, + {"name": "Ethernet3", "vrf": "PROD", "level": 1, "count": 1}, + ] + }, + "expected": {"result": "success"}, + }, { "name": "skipped-not-configured", "test": VerifyISISNeighborCount, @@ -383,6 +504,75 @@ "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured"], }, }, + { + "name": "success-interface-is-in-wrong-vrf", + "test": VerifyISISNeighborCount, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "interfaces": { + "Ethernet1": { + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + "PROD": { + "isisInstances": { + "PROD-ISIS": { + "interfaces": { + "Ethernet2": { + "enabled": True, + "intfLevels": { + "1": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "88", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + } + }, + ], + "inputs": { + "interfaces": [ + {"name": "Ethernet2", "level": 2, "count": 1}, + {"name": "Ethernet1", "vrf": "PROD", "level": 1, "count": 1}, + ] + }, + "expected": { + "result": "failure", + "messages": ["Interface: Ethernet2 VRF: default Level: 2 - Not configured", "Interface: Ethernet1 VRF: PROD Level: 1 - Not configured"], + }, + }, { "name": "failure-wrong-count", "test": VerifyISISNeighborCount, @@ -508,6 +698,117 @@ }, "expected": {"result": "success"}, }, + { + "name": "success-multiple-VRFs", + "test": VerifyISISInterfaceMode, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "interfaces": { + "Loopback0": { + "enabled": True, + "index": 2, + "snpa": "0:0:0:0:0:0", + "mtu": 65532, + "interfaceAddressFamily": "ipv4", + "interfaceType": "loopback", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": True, + "v4Protection": "disabled", + "v6Protection": "disabled", + } + }, + "areaProxyBoundary": False, + }, + "Ethernet1": { + "enabled": True, + "index": 132, + "snpa": "P2P", + "interfaceType": "point-to-point", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + "PROD": { + "isisInstances": { + "PROD-ISIS": { + "interfaces": { + "Ethernet4": { + "enabled": True, + "index": 132, + "snpa": "P2P", + "interfaceType": "point-to-point", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + "Ethernet5": { + "enabled": True, + "interfaceType": "broadcast", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 0, + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": True, + "v4Protection": "disabled", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + } + } + ], + "inputs": { + "interfaces": [ + {"name": "Loopback0", "mode": "passive"}, + {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, + {"name": "Ethernet4", "mode": "point-to-point", "vrf": "PROD"}, + {"name": "Ethernet5", "mode": "passive", "vrf": "PROD"}, + ] + }, + "expected": {"result": "success"}, + }, { "name": "failure-interface-not-passive", "test": VerifyISISInterfaceMode, @@ -780,6 +1081,124 @@ }, "expected": {"result": "skipped", "messages": ["IS-IS not configured"]}, }, + { + "name": "failure-multiple-VRFs", + "test": VerifyISISInterfaceMode, + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "interfaces": { + "Loopback0": { + "enabled": True, + "index": 2, + "snpa": "0:0:0:0:0:0", + "mtu": 65532, + "interfaceAddressFamily": "ipv4", + "interfaceType": "loopback", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": True, + "v4Protection": "disabled", + "v6Protection": "disabled", + } + }, + "areaProxyBoundary": False, + }, + "Ethernet1": { + "enabled": True, + "index": 132, + "snpa": "P2P", + "interfaceType": "broadcast", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + "PROD": { + "isisInstances": { + "PROD-ISIS": { + "interfaces": { + "Ethernet4": { + "enabled": True, + "index": 132, + "snpa": "P2P", + "interfaceType": "broadcast", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 1, + "linkId": "84", + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "link", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + "Ethernet5": { + "enabled": True, + "interfaceType": "broadcast", + "intfLevels": { + "2": { + "ipv4Metric": 10, + "numAdjacencies": 0, + "sharedSecretProfile": "", + "isisAdjacencies": [], + "passive": False, + "v4Protection": "disabled", + "v6Protection": "disabled", + } + }, + "interfaceSpeed": 1000, + "areaProxyBoundary": False, + }, + } + } + } + }, + } + } + ], + "inputs": { + "interfaces": [ + {"name": "Loopback0", "mode": "passive"}, + {"name": "Ethernet1", "mode": "point-to-point", "vrf": "default"}, + {"name": "Ethernet4", "mode": "point-to-point", "vrf": "PROD"}, + {"name": "Ethernet5", "mode": "passive", "vrf": "PROD"}, + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Interface: Ethernet1 VRF: default Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast", + "Interface: Ethernet4 VRF: PROD Level: 2 - Incorrect interface mode - Expected: point-to-point Actual: broadcast", + "Interface: Ethernet5 VRF: PROD Level: 2 - Not running in passive mode", + ], + }, + }, { "name": "skipped-not-configured", "test": VerifyISISSegmentRoutingAdjacencySegments, @@ -1108,6 +1527,71 @@ "messages": ["Instance: CORE-ISIS VRF: default Local Intf: Ethernet2 Adj IP Address: 10.0.1.3 - Incorrect IS-IS level - Expected: 1 Actual: 2"], }, }, + { + "test": VerifyISISSegmentRoutingAdjacencySegments, + "name": "failure-incorrect-sid-origin", + "eos_data": [ + { + "vrfs": { + "default": { + "isisInstances": { + "CORE-ISIS": { + "dataPlane": "MPLS", + "routerId": "1.0.0.11", + "systemId": "0168.0000.0011", + "hostname": "s1-pe01", + "adjSidAllocationMode": "SrOnly", + "adjSidPoolBase": 116384, + "adjSidPoolSize": 16384, + "adjacencySegments": [ + { + "ipAddress": "10.0.1.3", + "localIntf": "Ethernet2", + "sid": 116384, + "lan": False, + "sidOrigin": "configured", + "protection": "unprotected", + "flags": { + "b": False, + "v": True, + "l": True, + "f": False, + "s": False, + }, + "level": 2, + }, + ], + "receivedGlobalAdjacencySegments": [], + "misconfiguredAdjacencySegments": [], + } + } + } + } + } + ], + "inputs": { + "instances": [ + { + "name": "CORE-ISIS", + "vrf": "default", + "segments": [ + { + "interface": "Ethernet2", + "address": "10.0.1.3", + "sid_origin": "dynamic", + "level": 2, # Wrong level + }, + ], + } + ] + }, + "expected": { + "result": "failure", + "messages": [ + "Instance: CORE-ISIS VRF: default Local Intf: Ethernet2 Adj IP Address: 10.0.1.3 - Incorrect SID origin - Expected: dynamic Actual: configured" + ], + }, + }, { "test": VerifyISISSegmentRoutingDataplane, "name": "success", From 4d2c1a5b290c307019464f9ee0cf171a75c9cc20 Mon Sep 17 00:00:00 2001 From: vitthalmagadum Date: Thu, 6 Feb 2025 01:04:40 -0500 Subject: [PATCH 13/13] Added unit tests for validators --- anta/tests/routing/isis.py | 2 +- tests/units/input_models/routing/test_isis.py | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/units/input_models/routing/test_isis.py diff --git a/anta/tests/routing/isis.py b/anta/tests/routing/isis.py index f5d75cefa..11e420a0e 100644 --- a/anta/tests/routing/isis.py +++ b/anta/tests/routing/isis.py @@ -203,7 +203,7 @@ def test(self) -> None: if interface.mode == "passive": if get_value(interface_detail, f"intfLevels.{interface.level}.passive", default=False) is False: self.result.is_failure(f"{interface} - Not running in passive mode") - continue + # Check for point-to-point or broadcast elif interface.mode != (interface_type := get_value(interface_detail, "interfaceType", default="unset")): self.result.is_failure(f"{interface} - Incorrect interface mode - Expected: {interface.mode} Actual: {interface_type}") diff --git a/tests/units/input_models/routing/test_isis.py b/tests/units/input_models/routing/test_isis.py new file mode 100644 index 000000000..f22bfa6fd --- /dev/null +++ b/tests/units/input_models/routing/test_isis.py @@ -0,0 +1,70 @@ +# Copyright (c) 2023-2025 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.input_models.routing.isis.py.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from pydantic import ValidationError + +from anta.tests.routing.isis import VerifyISISSegmentRoutingAdjacencySegments, VerifyISISSegmentRoutingDataplane + +if TYPE_CHECKING: + from anta.input_models.routing.isis import ISISInstance + + +class TestVerifyISISSegmentRoutingAdjacencySegmentsInput: + """Test anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments.Input.""" + + @pytest.mark.parametrize( + ("instances"), + [ + pytest.param( + [{"name": "CORE-ISIS", "vrf": "default", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}]}], id="valid_vrf" + ), + ], + ) + def test_valid(self, instances: list[ISISInstance]) -> None: + """Test VerifyISISSegmentRoutingAdjacencySegments.Input valid inputs.""" + VerifyISISSegmentRoutingAdjacencySegments.Input(instances=instances) + + @pytest.mark.parametrize( + ("instances"), + [ + pytest.param( + [{"name": "CORE-ISIS", "vrf": "PROD", "segments": [{"interface": "Ethernet2", "address": "10.0.1.3", "sid_origin": "dynamic"}]}], id="invalid_vrf" + ), + ], + ) + def test_invalid(self, instances: list[ISISInstance]) -> None: + """Test VerifyISISSegmentRoutingAdjacencySegments.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifyISISSegmentRoutingAdjacencySegments.Input(instances=instances) + + +class TestVerifyISISSegmentRoutingDataplaneInput: + """Test anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane.Input.""" + + @pytest.mark.parametrize( + ("instances"), + [ + pytest.param([{"name": "CORE-ISIS", "vrf": "default", "dataplane": "MPLS"}], id="valid_vrf"), + ], + ) + def test_valid(self, instances: list[ISISInstance]) -> None: + """Test VerifyISISSegmentRoutingDataplane.Input valid inputs.""" + VerifyISISSegmentRoutingDataplane.Input(instances=instances) + + @pytest.mark.parametrize( + ("instances"), + [ + pytest.param([{"name": "CORE-ISIS", "vrf": "PROD", "dataplane": "MPLS"}], id="invalid_vrf"), + ], + ) + def test_invalid(self, instances: list[ISISInstance]) -> None: + """Test VerifyISISSegmentRoutingDataplane.Input invalid inputs.""" + with pytest.raises(ValidationError): + VerifyISISSegmentRoutingDataplane.Input(instances=instances)