diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69a40ad95..6e9430db3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - "" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.7 + rev: v0.9.9 hooks: - id: ruff name: Run Ruff linter diff --git a/anta/reporter/md_reporter.py b/anta/reporter/md_reporter.py index 16db69f9d..2d2d88245 100644 --- a/anta/reporter/md_reporter.py +++ b/anta/reporter/md_reporter.py @@ -177,8 +177,8 @@ def safe_markdown(self, text: str | None) -> str: if text is None: return "" - # Replace newlines with spaces to keep content on one line - text = text.replace("\n", " ") + # Replace newlines with
to preserve line breaks in HTML + text = text.replace("\n", "
") # Replace backticks with single quotes return text.replace("`", "'") @@ -286,7 +286,7 @@ class TestResults(MDReportBase): def generate_rows(self) -> Generator[str, None, None]: """Generate the rows of the all test results table.""" for result in self.results.results: - messages = self.safe_markdown(", ".join(result.messages)) + messages = self.safe_markdown(result.messages[0]) if len(result.messages) == 1 else self.safe_markdown("
".join(result.messages)) categories = ", ".join(sorted(convert_categories(result.categories))) yield ( f"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} " diff --git a/anta/tests/hardware.py b/anta/tests/hardware.py index 8ffe4e50d..f74c640bb 100644 --- a/anta/tests/hardware.py +++ b/anta/tests/hardware.py @@ -49,14 +49,14 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTransceiversManufacturers.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - wrong_manufacturers = { - interface: value["mfgName"] for interface, value in command_output["xcvrSlots"].items() if value["mfgName"] not in self.inputs.manufacturers - } - if not wrong_manufacturers: - self.result.is_success() - else: - self.result.is_failure(f"Some transceivers are from unapproved manufacturers: {wrong_manufacturers}") + for interface, value in command_output["xcvrSlots"].items(): + if value["mfgName"] not in self.inputs.manufacturers: + self.result.is_failure( + f"Interface: {interface} - Transceiver is from unapproved manufacturers - Expected: {', '.join(self.inputs.manufacturers)}" + f" Actual: {value['mfgName']}" + ) class VerifyTemperature(AntaTest): @@ -82,12 +82,11 @@ class VerifyTemperature(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTemperature.""" + self.result.is_success() command_output = self.instance_commands[0].json_output temperature_status = command_output.get("systemStatus", "") - if temperature_status == "temperatureOk": - self.result.is_success() - else: - self.result.is_failure(f"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'") + if temperature_status != "temperatureOk": + self.result.is_failure(f"Device temperature exceeds acceptable limits - Expected: temperatureOk Actual: {temperature_status}") class VerifyTransceiversTemperature(AntaTest): @@ -113,20 +112,14 @@ class VerifyTransceiversTemperature(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyTransceiversTemperature.""" + self.result.is_success() command_output = self.instance_commands[0].json_output sensors = command_output.get("tempSensors", "") - wrong_sensors = { - sensor["name"]: { - "hwStatus": sensor["hwStatus"], - "alertCount": sensor["alertCount"], - } - for sensor in sensors - if sensor["hwStatus"] != "ok" or sensor["alertCount"] != 0 - } - if not wrong_sensors: - self.result.is_success() - else: - self.result.is_failure(f"The following sensors are operating outside the acceptable temperature range or have raised alerts: {wrong_sensors}") + for sensor in sensors: + if sensor["hwStatus"] != "ok": + self.result.is_failure(f"Sensor: {sensor['name']} - Invalid hardware state - Expected: ok Actual: {sensor['hwStatus']}") + if sensor["alertCount"] != 0: + self.result.is_failure(f"Sensor: {sensor['name']} - Non-zero alert counter - Actual: {sensor['alertCount']}") class VerifyEnvironmentSystemCooling(AntaTest): @@ -156,7 +149,7 @@ def test(self) -> None: sys_status = command_output.get("systemStatus", "") self.result.is_success() if sys_status != "coolingOk": - self.result.is_failure(f"Device system cooling is not OK: '{sys_status}'") + self.result.is_failure(f"Device system cooling status invalid - Expected: coolingOk Actual: {sys_status}") class VerifyEnvironmentCooling(AntaTest): @@ -177,8 +170,6 @@ class VerifyEnvironmentCooling(AntaTest): ``` """ - name = "VerifyEnvironmentCooling" - description = "Verifies the status of power supply fans and all fan trays." categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show system environment cooling", revision=1)] @@ -198,12 +189,16 @@ def test(self) -> None: for power_supply in command_output.get("powerSupplySlots", []): for fan in power_supply.get("fans", []): if (state := fan["status"]) not in self.inputs.states: - self.result.is_failure(f"Fan {fan['label']} on PowerSupply {power_supply['label']} is: '{state}'") + self.result.is_failure( + f"Power Slot: {power_supply['label']} Fan: {fan['label']} - Invalid state - Expected: {', '.join(self.inputs.states)} Actual: {state}" + ) # Then go through fan trays for fan_tray in command_output.get("fanTraySlots", []): for fan in fan_tray.get("fans", []): if (state := fan["status"]) not in self.inputs.states: - self.result.is_failure(f"Fan {fan['label']} on Fan Tray {fan_tray['label']} is: '{state}'") + self.result.is_failure( + f"Fan Tray: {fan_tray['label']} Fan: {fan['label']} - Invalid state - Expected: {', '.join(self.inputs.states)} Actual: {state}" + ) class VerifyEnvironmentPower(AntaTest): @@ -237,19 +232,16 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyEnvironmentPower.""" + self.result.is_success() command_output = self.instance_commands[0].json_output power_supplies = command_output.get("powerSupplies", "{}") - wrong_power_supplies = { - powersupply: {"state": value["state"]} for powersupply, value in dict(power_supplies).items() if value["state"] not in self.inputs.states - } - if not wrong_power_supplies: - self.result.is_success() - else: - self.result.is_failure(f"The following power supplies status are not in the accepted states list: {wrong_power_supplies}") + for power_supply, value in dict(power_supplies).items(): + if (state := value["state"]) not in self.inputs.states: + self.result.is_failure(f"Power Slot: {power_supply} - Invalid power supplies state - Expected: {', '.join(self.inputs.states)} Actual: {state}") class VerifyAdverseDrops(AntaTest): - """Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips). + """Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches. Expected Results ---------------- @@ -264,7 +256,6 @@ class VerifyAdverseDrops(AntaTest): ``` """ - description = "Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches." categories: ClassVar[list[str]] = ["hardware"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show hardware counter drop", revision=1)] @@ -272,9 +263,8 @@ class VerifyAdverseDrops(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyAdverseDrops.""" + self.result.is_success() command_output = self.instance_commands[0].json_output total_adverse_drop = command_output.get("totalAdverseDrops", "") - if total_adverse_drop == 0: - self.result.is_success() - else: - self.result.is_failure(f"Device totalAdverseDrops counter is: '{total_adverse_drop}'") + if total_adverse_drop != 0: + self.result.is_failure(f"Non-zero total adverse drops counter - Actual: {total_adverse_drop}") diff --git a/anta/tests/interfaces.py b/anta/tests/interfaces.py index 5701839de..052bbe1c7 100644 --- a/anta/tests/interfaces.py +++ b/anta/tests/interfaces.py @@ -8,7 +8,7 @@ from __future__ import annotations import re -from typing import Any, ClassVar, TypeVar +from typing import ClassVar, TypeVar from pydantic import BaseModel, Field, field_validator from pydantic_extra_types.mac_address import MacAddress @@ -61,8 +61,8 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceUtilization.""" + self.result.is_success() duplex_full = "duplexFull" - failed_interfaces: dict[str, dict[str, float]] = {} rates = self.instance_commands[0].json_output interfaces = self.instance_commands[1].json_output @@ -78,15 +78,13 @@ def test(self) -> None: self.logger.debug("Interface %s has been ignored due to null bandwidth value", intf) continue + # If one or more interfaces have a usage above the threshold, test fails. for bps_rate in ("inBpsRate", "outBpsRate"): usage = rate[bps_rate] / bandwidth * 100 if usage > self.inputs.threshold: - failed_interfaces.setdefault(intf, {})[bps_rate] = usage - - if not failed_interfaces: - self.result.is_success() - else: - self.result.is_failure(f"The following interfaces have a usage > {self.inputs.threshold}%: {failed_interfaces}") + self.result.is_failure( + f"Interface: {intf} BPS Rate: {bps_rate} - Usage exceeds the threshold - Expected: < {self.inputs.threshold}% Actual: {usage}%" + ) class VerifyInterfaceErrors(AntaTest): @@ -111,15 +109,12 @@ class VerifyInterfaceErrors(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceErrors.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - wrong_interfaces: list[dict[str, dict[str, int]]] = [] for interface, counters in command_output["interfaceErrorCounters"].items(): - if any(value > 0 for value in counters.values()) and all(interface not in wrong_interface for wrong_interface in wrong_interfaces): - wrong_interfaces.append({interface: counters}) - if not wrong_interfaces: - self.result.is_success() - else: - self.result.is_failure(f"The following interface(s) have non-zero error counters: {wrong_interfaces}") + counters_data = [f"{counter}: {value}" for counter, value in counters.items() if value > 0] + if counters_data: + self.result.is_failure(f"Interface: {interface} - Non-zero error counter(s) - {', '.join(counters_data)}") class VerifyInterfaceDiscards(AntaTest): @@ -144,14 +139,12 @@ class VerifyInterfaceDiscards(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceDiscards.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - wrong_interfaces: list[dict[str, dict[str, int]]] = [] - for interface, outer_v in command_output["interfaces"].items(): - wrong_interfaces.extend({interface: outer_v} for value in outer_v.values() if value > 0) - if not wrong_interfaces: - self.result.is_success() - else: - self.result.is_failure(f"The following interfaces have non 0 discard counter(s): {wrong_interfaces}") + for interface, interface_data in command_output["interfaces"].items(): + counters_data = [f"{counter}: {value}" for counter, value in interface_data.items() if value > 0] + if counters_data: + self.result.is_failure(f"Interface: {interface} - Non-zero discard counter(s): {', '.join(counters_data)}") class VerifyInterfaceErrDisabled(AntaTest): @@ -176,12 +169,11 @@ class VerifyInterfaceErrDisabled(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyInterfaceErrDisabled.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - errdisabled_interfaces = [interface for interface, value in command_output["interfaceStatuses"].items() if value["linkStatus"] == "errdisabled"] - if errdisabled_interfaces: - self.result.is_failure(f"The following interfaces are in error disabled state: {errdisabled_interfaces}") - else: - self.result.is_success() + for interface, value in command_output["interfaceStatuses"].items(): + if value["linkStatus"] == "errdisabled": + self.result.is_failure(f"Interface: {interface} - Link status Error disabled") class VerifyInterfacesStatus(AntaTest): @@ -253,16 +245,16 @@ def test(self) -> None: # If line protocol status is provided, prioritize checking against both status and line protocol status if interface.line_protocol_status: - if interface.status != status or interface.line_protocol_status != proto: + if any([interface.status != status, interface.line_protocol_status != proto]): actual_state = f"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}" - self.result.is_failure(f"{interface.name} - {actual_state}") + self.result.is_failure(f"{interface.name} - Status mismatch - {actual_state}") # If line protocol status is not provided and interface status is "up", expect both status and proto to be "up" # If interface status is not "up", check only the interface status without considering line protocol status - elif interface.status == "up" and (status != "up" or proto != "up"): - self.result.is_failure(f"{interface.name} - Expected: up/up, Actual: {status}/{proto}") + elif all([interface.status == "up", status != "up" or proto != "up"]): + self.result.is_failure(f"{interface.name} - Status mismatch - Expected: up/up, Actual: {status}/{proto}") elif interface.status != status: - self.result.is_failure(f"{interface.name} - Expected: {interface.status}, Actual: {status}") + self.result.is_failure(f"{interface.name} - Status mismatch - Expected: {interface.status}, Actual: {status}") class VerifyStormControlDrops(AntaTest): @@ -289,16 +281,15 @@ class VerifyStormControlDrops(AntaTest): def test(self) -> None: """Main test function for VerifyStormControlDrops.""" command_output = self.instance_commands[0].json_output - storm_controlled_interfaces: dict[str, dict[str, Any]] = {} + storm_controlled_interfaces = [] + self.result.is_success() + for interface, interface_dict in command_output["interfaces"].items(): for traffic_type, traffic_type_dict in interface_dict["trafficTypes"].items(): if "drop" in traffic_type_dict and traffic_type_dict["drop"] != 0: - storm_controlled_interface_dict = storm_controlled_interfaces.setdefault(interface, {}) - storm_controlled_interface_dict.update({traffic_type: traffic_type_dict["drop"]}) - if not storm_controlled_interfaces: - self.result.is_success() - else: - self.result.is_failure(f"The following interfaces have none 0 storm-control drop counters {storm_controlled_interfaces}") + storm_controlled_interfaces.append(f"{traffic_type}: {traffic_type_dict['drop']}") + if storm_controlled_interfaces: + self.result.is_failure(f"Interface: {interface} - Non-zero storm-control drop counter(s) - {', '.join(storm_controlled_interfaces)}") class VerifyPortChannels(AntaTest): @@ -323,15 +314,12 @@ class VerifyPortChannels(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPortChannels.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - po_with_inactive_ports: list[dict[str, str]] = [] - for portchannel, portchannel_dict in command_output["portChannels"].items(): - if len(portchannel_dict["inactivePorts"]) != 0: - po_with_inactive_ports.extend({portchannel: portchannel_dict["inactivePorts"]}) - if not po_with_inactive_ports: - self.result.is_success() - else: - self.result.is_failure(f"The following port-channels have inactive port(s): {po_with_inactive_ports}") + for port_channel, port_channel_details in command_output["portChannels"].items(): + # Verify that the no inactive ports in all port channels. + if inactive_ports := port_channel_details["inactivePorts"]: + self.result.is_failure(f"{port_channel} - Inactive port(s) - {', '.join(inactive_ports.keys())}") class VerifyIllegalLACP(AntaTest): @@ -356,16 +344,13 @@ class VerifyIllegalLACP(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyIllegalLACP.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - po_with_illegal_lacp: list[dict[str, dict[str, int]]] = [] - for portchannel, portchannel_dict in command_output["portChannels"].items(): - po_with_illegal_lacp.extend( - {portchannel: interface} for interface, interface_dict in portchannel_dict["interfaces"].items() if interface_dict["illegalRxCount"] != 0 - ) - if not po_with_illegal_lacp: - self.result.is_success() - else: - self.result.is_failure(f"The following port-channels have received illegal LACP packets on the following ports: {po_with_illegal_lacp}") + for port_channel, port_channel_dict in command_output["portChannels"].items(): + for interface, interface_details in port_channel_dict["interfaces"].items(): + # Verify that the no illegal LACP packets in all port channels. + if interface_details["illegalRxCount"] != 0: + self.result.is_failure(f"{port_channel} Interface: {interface} - Illegal LACP packets found") class VerifyLoopbackCount(AntaTest): @@ -398,23 +383,20 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyLoopbackCount.""" + self.result.is_success() command_output = self.instance_commands[0].json_output loopback_count = 0 - down_loopback_interfaces = [] - for interface in command_output["interfaces"]: - interface_dict = command_output["interfaces"][interface] + for interface, interface_details in command_output["interfaces"].items(): if "Loopback" in interface: loopback_count += 1 - if not (interface_dict["lineProtocolStatus"] == "up" and interface_dict["interfaceStatus"] == "connected"): - down_loopback_interfaces.append(interface) - if loopback_count == self.inputs.number and len(down_loopback_interfaces) == 0: - self.result.is_success() - else: - self.result.is_failure() - if loopback_count != self.inputs.number: - self.result.is_failure(f"Found {loopback_count} Loopbacks when expecting {self.inputs.number}") - elif len(down_loopback_interfaces) != 0: # pragma: no branch - self.result.is_failure(f"The following Loopbacks are not up: {down_loopback_interfaces}") + if (status := interface_details["lineProtocolStatus"]) != "up": + self.result.is_failure(f"Interface: {interface} - Invalid line protocol status - Expected: up Actual: {status}") + + if (status := interface_details["interfaceStatus"]) != "connected": + self.result.is_failure(f"Interface: {interface} - Invalid interface status - Expected: connected Actual: {status}") + + if loopback_count != self.inputs.number: + self.result.is_failure(f"Loopback interface(s) count mismatch: Expected {self.inputs.number} Actual: {loopback_count}") class VerifySVI(AntaTest): @@ -439,16 +421,13 @@ class VerifySVI(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifySVI.""" + self.result.is_success() command_output = self.instance_commands[0].json_output - down_svis = [] - for interface in command_output["interfaces"]: - interface_dict = command_output["interfaces"][interface] - if "Vlan" in interface and not (interface_dict["lineProtocolStatus"] == "up" and interface_dict["interfaceStatus"] == "connected"): - down_svis.append(interface) - if len(down_svis) == 0: - self.result.is_success() - else: - self.result.is_failure(f"The following SVIs are not up: {down_svis}") + for interface, int_data in command_output["interfaces"].items(): + if "Vlan" in interface and (status := int_data["lineProtocolStatus"]) != "up": + self.result.is_failure(f"SVI: {interface} - Invalid line protocol status - Expected: up Actual: {status}") + if "Vlan" in interface and int_data["interfaceStatus"] != "connected": + self.result.is_failure(f"SVI: {interface} - Invalid interface status - Expected: connected Actual: {int_data['interfaceStatus']}") class VerifyL3MTU(AntaTest): @@ -493,8 +472,7 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyL3MTU.""" - # Parameter to save incorrect interface settings - wrong_l3mtu_intf: list[dict[str, int]] = [] + self.result.is_success() command_output = self.instance_commands[0].json_output # Set list of interfaces with specific settings specific_interfaces: list[str] = [] @@ -504,14 +482,14 @@ def test(self) -> None: for interface, values in command_output["interfaces"].items(): if re.findall(r"[a-z]+", interface, re.IGNORECASE)[0] not in self.inputs.ignored_interfaces and values["forwardingModel"] == "routed": if interface in specific_interfaces: - wrong_l3mtu_intf.extend({interface: values["mtu"]} for custom_data in self.inputs.specific_mtu if values["mtu"] != custom_data[interface]) + invalid_mtu = next( + (values["mtu"] for custom_data in self.inputs.specific_mtu if values["mtu"] != (expected_mtu := custom_data[interface])), None + ) + if invalid_mtu: + self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {expected_mtu} Actual: {invalid_mtu}") # Comparison with generic setting elif values["mtu"] != self.inputs.mtu: - wrong_l3mtu_intf.append({interface: values["mtu"]}) - if wrong_l3mtu_intf: - self.result.is_failure(f"Some interfaces do not have correct MTU configured:\n{wrong_l3mtu_intf}") - else: - self.result.is_success() + self.result.is_failure(f"Interface: {interface} - Incorrect MTU - Expected: {self.inputs.mtu} Actual: {values['mtu']}") class VerifyIPProxyARP(AntaTest): diff --git a/anta/tests/routing/ospf.py b/anta/tests/routing/ospf.py index 08bed8f89..a99ac18af 100644 --- a/anta/tests/routing/ospf.py +++ b/anta/tests/routing/ospf.py @@ -7,90 +7,15 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, ClassVar from anta.models import AntaCommand, AntaTest +from anta.tools import get_value if TYPE_CHECKING: from anta.models import AntaTemplate -def _count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int: - """Count the number of OSPF neighbors. - - Parameters - ---------- - ospf_neighbor_json - The JSON output of the `show ip ospf neighbor` command. - - Returns - ------- - int - The number of OSPF neighbors. - - """ - count = 0 - for vrf_data in ospf_neighbor_json["vrfs"].values(): - for instance_data in vrf_data["instList"].values(): - count += len(instance_data.get("ospfNeighborEntries", [])) - return count - - -def _get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]: - """Return the OSPF neighbors whose adjacency state is not `full`. - - Parameters - ---------- - ospf_neighbor_json - The JSON output of the `show ip ospf neighbor` command. - - Returns - ------- - list[dict[str, Any]] - A list of OSPF neighbors whose adjacency state is not `full`. - - """ - return [ - { - "vrf": vrf, - "instance": instance, - "neighbor": neighbor_data["routerId"], - "state": state, - } - for vrf, vrf_data in ospf_neighbor_json["vrfs"].items() - for instance, instance_data in vrf_data["instList"].items() - for neighbor_data in instance_data.get("ospfNeighborEntries", []) - if (state := neighbor_data["adjacencyState"]) != "full" - ] - - -def _get_ospf_max_lsa_info(ospf_process_json: dict[str, Any]) -> list[dict[str, Any]]: - """Return information about OSPF instances and their LSAs. - - Parameters - ---------- - ospf_process_json - OSPF process information in JSON format. - - Returns - ------- - list[dict[str, Any]] - A list of dictionaries containing OSPF LSAs information. - - """ - return [ - { - "vrf": vrf, - "instance": instance, - "maxLsa": instance_data.get("maxLsaInformation", {}).get("maxLsa"), - "maxLsaThreshold": instance_data.get("maxLsaInformation", {}).get("maxLsaThreshold"), - "numLsa": instance_data.get("lsaInformation", {}).get("numLsa"), - } - for vrf, vrf_data in ospf_process_json.get("vrfs", {}).items() - for instance, instance_data in vrf_data.get("instList", {}).items() - ] - - class VerifyOSPFNeighborState(AntaTest): """Verifies all OSPF neighbors are in FULL state. @@ -115,14 +40,29 @@ class VerifyOSPFNeighborState(AntaTest): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyOSPFNeighborState.""" - command_output = self.instance_commands[0].json_output - if _count_ospf_neighbor(command_output) == 0: - self.result.is_skipped("no OSPF neighbor found") - return self.result.is_success() - not_full_neighbors = _get_not_full_ospf_neighbors(command_output) - if not_full_neighbors: - self.result.is_failure(f"Some neighbors are not correctly configured: {not_full_neighbors}.") + + # If OSPF is not configured on device, test skipped. + if not (command_output := get_value(self.instance_commands[0].json_output, "vrfs")): + self.result.is_skipped("OSPF not configured") + return + + no_neighbor = True + for vrf, vrf_data in command_output.items(): + for instance, instance_data in vrf_data["instList"].items(): + neighbors = instance_data["ospfNeighborEntries"] + if not neighbors: + continue + no_neighbor = False + interfaces = [(neighbor["routerId"], state) for neighbor in neighbors if (state := neighbor["adjacencyState"]) != "full"] + for interface in interfaces: + self.result.is_failure( + f"Instance: {instance} VRF: {vrf} Interface: {interface[0]} - Incorrect adjacency state - Expected: Full Actual: {interface[1]}" + ) + + # If OSPF neighbors are not configured on device, test skipped. + if no_neighbor: + self.result.is_skipped("No OSPF neighbor detected") class VerifyOSPFNeighborCount(AntaTest): @@ -156,20 +96,34 @@ class Input(AntaTest.Input): @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyOSPFNeighborCount.""" - command_output = self.instance_commands[0].json_output - if (neighbor_count := _count_ospf_neighbor(command_output)) == 0: - self.result.is_skipped("no OSPF neighbor found") - return self.result.is_success() - if neighbor_count != self.inputs.number: - self.result.is_failure(f"device has {neighbor_count} neighbors (expected {self.inputs.number})") - not_full_neighbors = _get_not_full_ospf_neighbors(command_output) - if not_full_neighbors: - self.result.is_failure(f"Some neighbors are not correctly configured: {not_full_neighbors}.") + # If OSPF is not configured on device, test skipped. + if not (command_output := get_value(self.instance_commands[0].json_output, "vrfs")): + self.result.is_skipped("OSPF not configured") + return + + no_neighbor = True + interfaces = [] + for vrf_data in command_output.values(): + for instance_data in vrf_data["instList"].values(): + neighbors = instance_data["ospfNeighborEntries"] + if not neighbors: + continue + no_neighbor = False + interfaces.extend([neighbor["routerId"] for neighbor in neighbors if neighbor["adjacencyState"] == "full"]) + + # If OSPF neighbors are not configured on device, test skipped. + if no_neighbor: + self.result.is_skipped("No OSPF neighbor detected") + return + + # If the number of OSPF neighbors expected to be in the FULL state does not match with actual one, test fails. + if len(interfaces) != self.inputs.number: + self.result.is_failure(f"Neighbor count mismatch - Expected: {self.inputs.number} Actual: {len(interfaces)}") class VerifyOSPFMaxLSA(AntaTest): - """Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. + """Verifies all OSPF instances did not cross the maximum LSA threshold. Expected Results ---------------- @@ -186,23 +140,23 @@ class VerifyOSPFMaxLSA(AntaTest): ``` """ - description = "Verifies all OSPF instances did not cross the maximum LSA threshold." categories: ClassVar[list[str]] = ["ospf"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show ip ospf", revision=1)] @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyOSPFMaxLSA.""" - command_output = self.instance_commands[0].json_output - ospf_instance_info = _get_ospf_max_lsa_info(command_output) - if not ospf_instance_info: - self.result.is_skipped("No OSPF instance found.") + self.result.is_success() + + # If OSPF is not configured on device, test skipped. + if not (command_output := get_value(self.instance_commands[0].json_output, "vrfs")): + self.result.is_skipped("OSPF not configured") return - all_instances_within_threshold = all(instance["numLsa"] <= instance["maxLsa"] * (instance["maxLsaThreshold"] / 100) for instance in ospf_instance_info) - if all_instances_within_threshold: - self.result.is_success() - else: - exceeded_instances = [ - instance["instance"] for instance in ospf_instance_info if instance["numLsa"] > instance["maxLsa"] * (instance["maxLsaThreshold"] / 100) - ] - self.result.is_failure(f"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.") + + for vrf_data in command_output.values(): + for instance, instance_data in vrf_data.get("instList", {}).items(): + max_lsa = instance_data["maxLsaInformation"]["maxLsa"] + max_lsa_threshold = instance_data["maxLsaInformation"]["maxLsaThreshold"] + num_lsa = get_value(instance_data, "lsaInformation.numLsa") + if num_lsa > (max_lsa_threshold := round(max_lsa * (max_lsa_threshold / 100))): + self.result.is_failure(f"Instance: {instance} - Crossed the maximum LSA threshold - Expected: < {max_lsa_threshold} Actual: {num_lsa}") diff --git a/tests/data/test_md_report.md b/tests/data/test_md_report.md index db8d47f9a..1b1acd1a1 100644 --- a/tests/data/test_md_report.md +++ b/tests/data/test_md_report.md @@ -15,65 +15,78 @@ | Total Tests | Total Tests Success | Total Tests Skipped | Total Tests Failure | Total Tests Error | | ----------- | ------------------- | ------------------- | ------------------- | ------------------| -| 30 | 7 | 2 | 19 | 2 | +| 30 | 4 | 9 | 15 | 2 | ### Summary Totals Device Under Test | Device Under Test | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | Categories Skipped | Categories Failed | | ------------------| ----------- | ------------- | ------------- | ------------- | ----------- | -------------------| ------------------| -| DC1-LEAF1A | 15 | 5 | 0 | 9 | 1 | - | AAA, BFD, BGP, Connectivity, SNMP, STP, Services, Software, System | -| DC1-SPINE1 | 15 | 2 | 2 | 10 | 1 | MLAG, VXLAN | AAA, BFD, BGP, Connectivity, Routing, SNMP, STP, Services, Software, System | +| s1-spine1 | 30 | 4 | 9 | 15 | 2 | AVT, Field Notices, Hardware, ISIS, LANZ, OSPF, PTP, Path-Selection, Profiles | AAA, BFD, BGP, Connectivity, Cvx, Interfaces, Logging, MLAG, SNMP, STUN, Security, Services, Software, System, VLAN | ### Summary Totals Per Category | Test Category | Total Tests | Tests Success | Tests Skipped | Tests Failure | Tests Error | | ------------- | ----------- | ------------- | ------------- | ------------- | ----------- | -| AAA | 2 | 0 | 0 | 2 | 0 | -| BFD | 2 | 0 | 0 | 2 | 0 | -| BGP | 2 | 0 | 0 | 2 | 0 | -| Connectivity | 4 | 0 | 0 | 2 | 2 | -| Interfaces | 2 | 2 | 0 | 0 | 0 | -| MLAG | 2 | 1 | 1 | 0 | 0 | -| Routing | 2 | 1 | 0 | 1 | 0 | -| SNMP | 2 | 0 | 0 | 2 | 0 | -| STP | 2 | 0 | 0 | 2 | 0 | -| Security | 2 | 2 | 0 | 0 | 0 | -| Services | 2 | 0 | 0 | 2 | 0 | -| Software | 2 | 0 | 0 | 2 | 0 | -| System | 2 | 0 | 0 | 2 | 0 | -| VXLAN | 2 | 1 | 1 | 0 | 0 | +| AAA | 1 | 0 | 0 | 1 | 0 | +| AVT | 1 | 0 | 1 | 0 | 0 | +| BFD | 1 | 0 | 0 | 1 | 0 | +| BGP | 1 | 0 | 0 | 0 | 1 | +| Configuration | 1 | 1 | 0 | 0 | 0 | +| Connectivity | 1 | 0 | 0 | 1 | 0 | +| Cvx | 1 | 0 | 0 | 0 | 1 | +| Field Notices | 1 | 0 | 1 | 0 | 0 | +| Hardware | 1 | 0 | 1 | 0 | 0 | +| Interfaces | 1 | 0 | 0 | 1 | 0 | +| ISIS | 1 | 0 | 1 | 0 | 0 | +| LANZ | 1 | 0 | 1 | 0 | 0 | +| Logging | 1 | 0 | 0 | 1 | 0 | +| MLAG | 1 | 0 | 0 | 1 | 0 | +| OSPF | 1 | 0 | 1 | 0 | 0 | +| Path-Selection | 1 | 0 | 1 | 0 | 0 | +| Profiles | 1 | 0 | 1 | 0 | 0 | +| PTP | 1 | 0 | 1 | 0 | 0 | +| Routing | 1 | 1 | 0 | 0 | 0 | +| Security | 2 | 0 | 0 | 2 | 0 | +| Services | 1 | 0 | 0 | 1 | 0 | +| SNMP | 1 | 0 | 0 | 1 | 0 | +| Software | 1 | 0 | 0 | 1 | 0 | +| STP | 1 | 1 | 0 | 0 | 0 | +| STUN | 2 | 0 | 0 | 2 | 0 | +| System | 1 | 0 | 0 | 1 | 0 | +| VLAN | 1 | 0 | 0 | 1 | 0 | +| VXLAN | 1 | 1 | 0 | 0 | 0 | ## Test Results | Device Under Test | Categories | Test | Description | Custom Field | Result | Messages | | ----------------- | ---------- | ---- | ----------- | ------------ | ------ | -------- | -| DC1-LEAF1A | AAA | VerifyTacacsSourceIntf | Verifies TACACS source-interface for a specified VRF. | - | failure | Source-interface Management0 is not configured in VRF default | -| DC1-LEAF1A | BFD | VerifyBFDSpecificPeers | Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF. | - | failure | Following BFD peers are not configured, status is not up or remote disc is zero: {'192.0.255.8': {'default': 'Not Configured'}, '192.0.255.7': {'default': 'Not Configured'}} | -| DC1-LEAF1A | BGP | VerifyBGPPeerCount | Verifies the count of BGP peers. | - | failure | Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Expected: 2, Actual: 1'}}, {'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'DEV': 'Expected: 3, Actual: 0'}}] | -| DC1-LEAF1A | Connectivity | VerifyLLDPNeighbors | Verifies that the provided LLDP neighbors are connected properly. | - | failure | Wrong LLDP neighbor(s) on port(s): Ethernet1 DC1-SPINE1_Ethernet1 Ethernet2 DC1-SPINE2_Ethernet1 Port(s) not configured: Ethernet7 | -| DC1-LEAF1A | Connectivity | VerifyReachability | Test the network reachability to one or many destination IP(s). | - | error | ping vrf MGMT 1.1.1.1 source Management1 repeat 2 has failed: No source interface Management1 | -| DC1-LEAF1A | Interfaces | VerifyInterfaceUtilization | Verifies that the utilization of interfaces is below a certain threshold. | - | success | - | -| DC1-LEAF1A | MLAG | VerifyMlagStatus | Verifies the health status of the MLAG configuration. | - | success | - | -| DC1-LEAF1A | Routing | VerifyRoutingTableEntry | Verifies that the provided routes are present in the routing table of a specified VRF. | - | success | - | -| DC1-LEAF1A | SNMP | VerifySnmpStatus | Verifies if the SNMP agent is enabled. | - | failure | SNMP agent disabled in vrf default | -| DC1-LEAF1A | STP | VerifySTPMode | Verifies the configured STP mode for a provided list of VLAN(s). | - | failure | Wrong STP mode configured for the following VLAN(s): [10, 20] | -| DC1-LEAF1A | Security | VerifyTelnetStatus | Verifies if Telnet is disabled in the default VRF. | - | success | - | -| DC1-LEAF1A | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Expected 's1-spine1' as the hostname, but found 'DC1-LEAF1A' instead. | -| DC1-LEAF1A | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | device is running version "4.31.1F-34554157.4311F (engineering build)" not in expected versions: ['4.25.4M', '4.26.1F'] | -| DC1-LEAF1A | System | VerifyNTP | Verifies if NTP is synchronised. | - | failure | The device is not synchronized with the configured NTP server(s): 'NTP is disabled.' | -| DC1-LEAF1A | VXLAN | VerifyVxlan1Interface | Verifies the Vxlan1 interface status. | - | success | - | -| DC1-SPINE1 | AAA | VerifyTacacsSourceIntf | Verifies TACACS source-interface for a specified VRF. | - | failure | Source-interface Management0 is not configured in VRF default | -| DC1-SPINE1 | BFD | VerifyBFDSpecificPeers | Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF. | - | failure | Following BFD peers are not configured, status is not up or remote disc is zero: {'192.0.255.8': {'default': 'Not Configured'}, '192.0.255.7': {'default': 'Not Configured'}} | -| DC1-SPINE1 | BGP | VerifyBGPPeerCount | Verifies the count of BGP peers. | - | failure | Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Not Configured', 'default': 'Expected: 3, Actual: 4'}}, {'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'DEV': 'Not Configured'}}, {'afi': 'evpn', 'vrfs': {'default': 'Expected: 2, Actual: 4'}}] | -| DC1-SPINE1 | Connectivity | VerifyLLDPNeighbors | Verifies that the provided LLDP neighbors are connected properly. | - | failure | Wrong LLDP neighbor(s) on port(s): Ethernet1 DC1-LEAF1A_Ethernet1 Ethernet2 DC1-LEAF1B_Ethernet1 Port(s) not configured: Ethernet7 | -| DC1-SPINE1 | Connectivity | VerifyReachability | Test the network reachability to one or many destination IP(s). | - | error | ping vrf MGMT 1.1.1.1 source Management1 repeat 2 has failed: No source interface Management1 | -| DC1-SPINE1 | Interfaces | VerifyInterfaceUtilization | Verifies that the utilization of interfaces is below a certain threshold. | - | success | - | -| DC1-SPINE1 | MLAG | VerifyMlagStatus | Verifies the health status of the MLAG configuration. | - | skipped | MLAG is disabled | -| DC1-SPINE1 | Routing | VerifyRoutingTableEntry | Verifies that the provided routes are present in the routing table of a specified VRF. | - | failure | The following route(s) are missing from the routing table of VRF default: ['10.1.0.2'] | -| DC1-SPINE1 | SNMP | VerifySnmpStatus | Verifies if the SNMP agent is enabled. | - | failure | SNMP agent disabled in vrf default | -| DC1-SPINE1 | STP | VerifySTPMode | Verifies the configured STP mode for a provided list of VLAN(s). | - | failure | STP mode 'rapidPvst' not configured for the following VLAN(s): [10, 20] | -| DC1-SPINE1 | Security | VerifyTelnetStatus | Verifies if Telnet is disabled in the default VRF. | - | success | - | -| DC1-SPINE1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Expected 's1-spine1' as the hostname, but found 'DC1-SPINE1' instead. | -| DC1-SPINE1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | device is running version "4.31.1F-34554157.4311F (engineering build)" not in expected versions: ['4.25.4M', '4.26.1F'] | -| DC1-SPINE1 | System | VerifyNTP | Verifies if NTP is synchronised. | - | failure | The device is not synchronized with the configured NTP server(s): 'NTP is disabled.' | -| DC1-SPINE1 | VXLAN | VerifyVxlan1Interface | Verifies the Vxlan1 interface status. | - | skipped | Vxlan1 interface is not configured | +| s1-spine1 | AAA | VerifyAcctConsoleMethods | Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x). | - | failure | AAA console accounting is not configured for commands, exec, system, dot1x | +| s1-spine1 | AVT | VerifyAVTPathHealth | Verifies the status of all AVT paths for all VRFs. | - | skipped | VerifyAVTPathHealth test is not supported on cEOSLab. | +| s1-spine1 | BFD | VerifyBFDPeersHealth | Verifies the health of IPv4 BFD peers across all VRFs. | - | failure | No IPv4 BFD peers are configured for any VRF. | +| s1-spine1 | BGP | VerifyBGPAdvCommunities | Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s). | - | error | show bgp neighbors vrf all has failed: The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model. | +| s1-spine1 | Configuration | VerifyRunningConfigDiffs | Verifies there is no difference between the running-config and the startup-config. | - | success | - | +| s1-spine1 | Connectivity | VerifyLLDPNeighbors | Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. | - | failure | Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3
Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3 | +| s1-spine1 | Cvx | VerifyActiveCVXConnections | Verifies the number of active CVX Connections. | - | error | show cvx connections brief has failed: Unavailable command (controller not ready) (at token 2: 'connections') | +| s1-spine1 | Field Notices | VerifyFieldNotice44Resolution | Verifies that the device is using the correct Aboot version per FN0044. | - | skipped | VerifyFieldNotice44Resolution test is not supported on cEOSLab. | +| s1-spine1 | Hardware | VerifyTemperature | Verifies if the device temperature is within acceptable limits. | - | skipped | VerifyTemperature test is not supported on cEOSLab. | +| s1-spine1 | Interfaces | VerifyIPProxyARP | Verifies if Proxy ARP is enabled. | - | failure | Interface: Ethernet1 - Proxy-ARP disabled
Interface: Ethernet2 - Proxy-ARP disabled | +| s1-spine1 | ISIS | VerifyISISNeighborState | Verifies the health of IS-IS neighbors. | - | skipped | IS-IS not configured | +| s1-spine1 | LANZ | VerifyLANZ | Verifies if LANZ is enabled. | - | skipped | VerifyLANZ test is not supported on cEOSLab. | +| s1-spine1 | Logging | VerifyLoggingHosts | Verifies logging hosts (syslog servers) for a specified VRF. | - | failure | Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default | +| s1-spine1 | MLAG | VerifyMlagDualPrimary | Verifies the MLAG dual-primary detection parameters. | - | failure | Dual-primary detection is disabled | +| s1-spine1 | OSPF | VerifyOSPFMaxLSA | Verifies all OSPF instances did not cross the maximum LSA threshold. | - | skipped | No OSPF instance found. | +| s1-spine1 | Path-Selection | VerifyPathsHealth | Verifies the path and telemetry state of all paths under router path-selection. | - | skipped | VerifyPathsHealth test is not supported on cEOSLab. | +| s1-spine1 | Profiles | VerifyTcamProfile | Verifies the device TCAM profile. | - | skipped | VerifyTcamProfile test is not supported on cEOSLab. | +| s1-spine1 | PTP | VerifyPtpGMStatus | Verifies that the device is locked to a valid PTP Grandmaster. | - | skipped | VerifyPtpGMStatus test is not supported on cEOSLab. | +| s1-spine1 | Routing | VerifyIPv4RouteNextHops | Verifies the next-hops of the IPv4 prefixes. | - | success | - | +| s1-spine1 | Security | VerifyBannerLogin | Verifies the login banner of a device. | - | failure | Expected '# 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.
' as the login banner, but found '' instead. | +| s1-spine1 | Security | VerifyBannerMotd | Verifies the motd banner of a device. | - | failure | Expected '# 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.
' as the motd banner, but found '' instead. | +| s1-spine1 | Services | VerifyHostname | Verifies the hostname of a device. | - | failure | Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1 | +| s1-spine1 | SNMP | VerifySnmpContact | Verifies the SNMP contact of a device. | - | failure | SNMP contact is not configured. | +| s1-spine1 | Software | VerifyEOSVersion | Verifies the EOS version of the device. | - | failure | EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F | +| s1-spine1 | STP | VerifySTPBlockedPorts | Verifies there is no STP blocked ports. | - | success | - | +| s1-spine1 | STUN | VerifyStunClient | (Deprecated) Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found. | +| s1-spine1 | STUN | VerifyStunClientTranslation | Verifies the translation for a source address on a STUN client. | - | failure | Client 172.18.3.2 Port: 4500 - STUN client translation not found.
Client 100.64.3.2 Port: 4500 - STUN client translation not found. | +| s1-spine1 | System | VerifyNTPAssociations | Verifies the Network Time Protocol (NTP) associations. | - | failure | NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured
NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured
NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured | +| s1-spine1 | VLAN | VerifyDynamicVlanSource | Verifies dynamic VLAN allocation for specified VLAN sources. | - | failure | Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync | +| s1-spine1 | VXLAN | VerifyVxlan1ConnSettings | Verifies the interface vxlan1 source interface and UDP port. | - | success | - | diff --git a/tests/units/anta_tests/routing/test_ospf.py b/tests/units/anta_tests/routing/test_ospf.py index 644cd76fa..0c736dcd0 100644 --- a/tests/units/anta_tests/routing/test_ospf.py +++ b/tests/units/anta_tests/routing/test_ospf.py @@ -122,13 +122,13 @@ "expected": { "result": "failure", "messages": [ - "Some neighbors are not correctly configured: [{'vrf': 'default', 'instance': '666', 'neighbor': '7.7.7.7', 'state': '2-way'}," - " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}].", + "Instance: 666 VRF: default Interface: 7.7.7.7 - Incorrect adjacency state - Expected: Full Actual: 2-way", + "Instance: 777 VRF: BLAH Interface: 8.8.8.8 - Incorrect adjacency state - Expected: Full Actual: down", ], }, }, { - "name": "skipped", + "name": "skipped-ospf-not-configured", "test": VerifyOSPFNeighborState, "eos_data": [ { @@ -136,7 +136,33 @@ }, ], "inputs": None, - "expected": {"result": "skipped", "messages": ["no OSPF neighbor found"]}, + "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, + }, + { + "name": "skipped-neighbor-not-found", + "test": VerifyOSPFNeighborState, + "eos_data": [ + { + "vrfs": { + "default": { + "instList": { + "666": { + "ospfNeighborEntries": [], + }, + }, + }, + "BLAH": { + "instList": { + "777": { + "ospfNeighborEntries": [], + }, + }, + }, + }, + }, + ], + "inputs": None, + "expected": {"result": "skipped", "messages": ["No OSPF neighbor detected"]}, }, { "name": "success", @@ -193,35 +219,6 @@ "inputs": {"number": 3}, "expected": {"result": "success"}, }, - { - "name": "failure-wrong-number", - "test": VerifyOSPFNeighborCount, - "eos_data": [ - { - "vrfs": { - "default": { - "instList": { - "666": { - "ospfNeighborEntries": [ - { - "routerId": "7.7.7.7", - "priority": 1, - "drState": "DR", - "interfaceName": "Ethernet1", - "adjacencyState": "full", - "inactivity": 1683298014.844345, - "interfaceAddress": "10.3.0.1", - }, - ], - }, - }, - }, - }, - }, - ], - "inputs": {"number": 3}, - "expected": {"result": "failure", "messages": ["device has 1 neighbors (expected 3)"]}, - }, { "name": "failure-good-number-wrong-state", "test": VerifyOSPFNeighborCount, @@ -277,14 +274,11 @@ "inputs": {"number": 3}, "expected": { "result": "failure", - "messages": [ - "Some neighbors are not correctly configured: [{'vrf': 'default', 'instance': '666', 'neighbor': '7.7.7.7', 'state': '2-way'}," - " {'vrf': 'BLAH', 'instance': '777', 'neighbor': '8.8.8.8', 'state': 'down'}].", - ], + "messages": ["Neighbor count mismatch - Expected: 3 Actual: 1"], }, }, { - "name": "skipped", + "name": "skipped-ospf-not-configured", "test": VerifyOSPFNeighborCount, "eos_data": [ { @@ -292,7 +286,38 @@ }, ], "inputs": {"number": 3}, - "expected": {"result": "skipped", "messages": ["no OSPF neighbor found"]}, + "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, + }, + { + "name": "skipped-no-neighbor-detected", + "test": VerifyOSPFNeighborCount, + "eos_data": [ + { + "vrfs": { + "default": { + "instList": { + "666": { + "ospfNeighborEntries": [], + }, + }, + }, + "BLAH": { + "instList": { + "777": { + "ospfNeighborEntries": [], + }, + }, + }, + }, + }, + ], + "inputs": {"number": 3}, + "expected": { + "result": "skipped", + "messages": [ + "No OSPF neighbor detected", + ], + }, }, { "name": "success", @@ -394,7 +419,10 @@ "inputs": None, "expected": { "result": "failure", - "messages": ["OSPF Instances ['1', '10'] crossed the maximum LSA threshold."], + "messages": [ + "Instance: 1 - Crossed the maximum LSA threshold - Expected: < 9000 Actual: 11500", + "Instance: 10 - Crossed the maximum LSA threshold - Expected: < 750 Actual: 1500", + ], }, }, { @@ -406,6 +434,6 @@ }, ], "inputs": None, - "expected": {"result": "skipped", "messages": ["No OSPF instance found."]}, + "expected": {"result": "skipped", "messages": ["OSPF not configured"]}, }, ] diff --git a/tests/units/anta_tests/test_hardware.py b/tests/units/anta_tests/test_hardware.py index d6993c5f2..0dffe6925 100644 --- a/tests/units/anta_tests/test_hardware.py +++ b/tests/units/anta_tests/test_hardware.py @@ -45,7 +45,13 @@ }, ], "inputs": {"manufacturers": ["Arista"]}, - "expected": {"result": "failure", "messages": ["Some transceivers are from unapproved manufacturers: {'1': 'Arista Networks', '2': 'Arista Networks'}"]}, + "expected": { + "result": "failure", + "messages": [ + "Interface: 1 - Transceiver is from unapproved manufacturers - Expected: Arista Actual: Arista Networks", + "Interface: 2 - Transceiver is from unapproved manufacturers - Expected: Arista Actual: Arista Networks", + ], + }, }, { "name": "success", @@ -72,12 +78,12 @@ "ambientThreshold": 45, "cardSlots": [], "shutdownOnOverheat": "True", - "systemStatus": "temperatureKO", + "systemStatus": "temperatureCritical", "recoveryModeOnOverheat": "recoveryModeNA", }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["Device temperature exceeds acceptable limits. Current system status: 'temperatureKO'"]}, + "expected": {"result": "failure", "messages": ["Device temperature exceeds acceptable limits - Expected: temperatureOk Actual: temperatureCritical"]}, }, { "name": "success", @@ -139,11 +145,7 @@ "inputs": None, "expected": { "result": "failure", - "messages": [ - "The following sensors are operating outside the acceptable temperature range or have raised alerts: " - "{'DomTemperatureSensor54': " - "{'hwStatus': 'ko', 'alertCount': 0}}", - ], + "messages": ["Sensor: DomTemperatureSensor54 - Invalid hardware state - Expected: ok Actual: ko"], }, }, { @@ -176,11 +178,7 @@ "inputs": None, "expected": { "result": "failure", - "messages": [ - "The following sensors are operating outside the acceptable temperature range or have raised alerts: " - "{'DomTemperatureSensor54': " - "{'hwStatus': 'ok', 'alertCount': 1}}", - ], + "messages": ["Sensor: DomTemperatureSensor54 - Non-zero alert counter - Actual: 1"], }, }, { @@ -227,7 +225,7 @@ }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["Device system cooling is not OK: 'coolingKo'"]}, + "expected": {"result": "failure", "messages": ["Device system cooling status invalid - Expected: coolingOk Actual: coolingKo"]}, }, { "name": "success", @@ -626,7 +624,7 @@ }, ], "inputs": {"states": ["ok", "Not Inserted"]}, - "expected": {"result": "failure", "messages": ["Fan 1/1 on Fan Tray 1 is: 'down'"]}, + "expected": {"result": "failure", "messages": ["Fan Tray: 1 Fan: 1/1 - Invalid state - Expected: ok, Not Inserted Actual: down"]}, }, { "name": "failure-power-supply", @@ -759,7 +757,12 @@ }, ], "inputs": {"states": ["ok", "Not Inserted"]}, - "expected": {"result": "failure", "messages": ["Fan PowerSupply1/1 on PowerSupply PowerSupply1 is: 'down'"]}, + "expected": { + "result": "failure", + "messages": [ + "Power Slot: PowerSupply1 Fan: PowerSupply1/1 - Invalid state - Expected: ok, Not Inserted Actual: down", + ], + }, }, { "name": "success", @@ -900,7 +903,7 @@ }, ], "inputs": {"states": ["ok"]}, - "expected": {"result": "failure", "messages": ["The following power supplies status are not in the accepted states list: {'1': {'state': 'powerLoss'}}"]}, + "expected": {"result": "failure", "messages": ["Power Slot: 1 - Invalid power supplies state - Expected: ok Actual: powerLoss"]}, }, { "name": "success", @@ -914,6 +917,6 @@ "test": VerifyAdverseDrops, "eos_data": [{"totalAdverseDrops": 10}], "inputs": None, - "expected": {"result": "failure", "messages": ["Device totalAdverseDrops counter is: '10'"]}, + "expected": {"result": "failure", "messages": ["Non-zero total adverse drops counter - Actual: 10"]}, }, ] diff --git a/tests/units/anta_tests/test_interfaces.py b/tests/units/anta_tests/test_interfaces.py index 4d2bcb4ad..65f60006f 100644 --- a/tests/units/anta_tests/test_interfaces.py +++ b/tests/units/anta_tests/test_interfaces.py @@ -508,7 +508,10 @@ "inputs": {"threshold": 3.0}, "expected": { "result": "failure", - "messages": ["The following interfaces have a usage > 3.0%: {'Ethernet1/1': {'inBpsRate': 10.0}, 'Port-Channel31': {'outBpsRate': 5.0}}"], + "messages": [ + "Interface: Ethernet1/1 BPS Rate: inBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 10.0%", + "Interface: Port-Channel31 BPS Rate: outBpsRate - Usage exceeds the threshold - Expected: < 3.0% Actual: 5.0%", + ], }, }, { @@ -653,7 +656,7 @@ "inputs": {"threshold": 70.0}, "expected": { "result": "failure", - "messages": ["Interface Ethernet1/1 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented."], + "messages": ["Interface Ethernet1/1 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented"], }, }, { @@ -787,7 +790,7 @@ }, "memberInterfaces": { "Ethernet3/1": {"bandwidth": 1000000000, "duplex": "duplexHalf"}, - "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexFull"}, + "Ethernet4/1": {"bandwidth": 1000000000, "duplex": "duplexHalf"}, }, "fallbackEnabled": False, "fallbackEnabledType": "fallbackNone", @@ -798,7 +801,7 @@ "inputs": {"threshold": 70.0}, "expected": { "result": "failure", - "messages": ["Interface Port-Channel31 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented."], + "messages": ["Interface Port-Channel31 or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented"], }, }, { @@ -830,9 +833,8 @@ "expected": { "result": "failure", "messages": [ - "The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts': 0," - " 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}, {'Ethernet6': {'inErrors': 0, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts':" - " 0, 'fcsErrors': 0, 'alignmentErrors': 666, 'symbolErrors': 0}}]", + "Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42", + "Interface: Ethernet6 - Non-zero error counter(s) - alignmentErrors: 666", ], }, }, @@ -851,9 +853,8 @@ "expected": { "result": "failure", "messages": [ - "The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 10, 'frameTooShorts': 0," - " 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}, {'Ethernet6': {'inErrors': 0, 'frameTooLongs': 0, 'outErrors': 0, 'frameTooShorts':" - " 0, 'fcsErrors': 0, 'alignmentErrors': 6, 'symbolErrors': 10}}]", + "Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42, outErrors: 10", + "Interface: Ethernet6 - Non-zero error counter(s) - alignmentErrors: 6, symbolErrors: 10", ], }, }, @@ -870,10 +871,7 @@ "inputs": None, "expected": { "result": "failure", - "messages": [ - "The following interface(s) have non-zero error counters: [{'Ethernet1': {'inErrors': 42, 'frameTooLongs': 0, 'outErrors': 2, 'frameTooShorts': 0," - " 'fcsErrors': 0, 'alignmentErrors': 0, 'symbolErrors': 0}}]", - ], + "messages": ["Interface: Ethernet1 - Non-zero error counter(s) - inErrors: 42, outErrors: 2"], }, }, { @@ -909,8 +907,8 @@ "expected": { "result": "failure", "messages": [ - "The following interfaces have non 0 discard counter(s): [{'Ethernet2': {'outDiscards': 42, 'inDiscards': 0}}," - " {'Ethernet1': {'outDiscards': 0, 'inDiscards': 42}}]", + "Interface: Ethernet2 - Non-zero discard counter(s): outDiscards: 42", + "Interface: Ethernet1 - Non-zero discard counter(s): inDiscards: 42", ], }, }, @@ -948,7 +946,7 @@ }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["The following interfaces are in error disabled state: ['Management1', 'Ethernet8']"]}, + "expected": {"result": "failure", "messages": ["Interface: Management1 - Link status Error disabled", "Interface: Ethernet8 - Link status Error disabled"]}, }, { "name": "success", @@ -1126,7 +1124,7 @@ "inputs": {"interfaces": [{"name": "Ethernet2", "status": "up"}, {"name": "Ethernet8", "status": "up"}, {"name": "Ethernet3", "status": "up"}]}, "expected": { "result": "failure", - "messages": ["Ethernet8 - Expected: up/up, Actual: down/down"], + "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: down/down"], }, }, { @@ -1150,7 +1148,7 @@ }, "expected": { "result": "failure", - "messages": ["Ethernet8 - Expected: up/up, Actual: up/down"], + "messages": ["Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down"], }, }, { @@ -1166,7 +1164,7 @@ "inputs": {"interfaces": [{"name": "PortChannel100", "status": "up"}]}, "expected": { "result": "failure", - "messages": ["Port-Channel100 - Expected: up/up, Actual: down/lowerLayerDown"], + "messages": ["Port-Channel100 - Status mismatch - Expected: up/up, Actual: down/lowerLayerDown"], }, }, { @@ -1191,8 +1189,8 @@ "expected": { "result": "failure", "messages": [ - "Ethernet2 - Expected: up/down, Actual: up/unknown", - "Ethernet8 - Expected: up/up, Actual: up/down", + "Ethernet2 - Status mismatch - Expected: up/down, Actual: up/unknown", + "Ethernet8 - Status mismatch - Expected: up/up, Actual: up/down", ], }, }, @@ -1218,9 +1216,9 @@ "expected": { "result": "failure", "messages": [ - "Ethernet2 - Expected: down, Actual: up", - "Ethernet8 - Expected: down, Actual: up", - "Ethernet3 - Expected: down, Actual: up", + "Ethernet2 - Status mismatch - Expected: down, Actual: up", + "Ethernet8 - Status mismatch - Expected: down, Actual: up", + "Ethernet3 - Status mismatch - Expected: down, Actual: up", ], }, }, @@ -1260,7 +1258,7 @@ }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["The following interfaces have none 0 storm-control drop counters {'Ethernet1': {'broadcast': 666}}"]}, + "expected": {"result": "failure", "messages": ["Interface: Ethernet1 - Non-zero storm-control drop counter(s) - broadcast: 666"]}, }, { "name": "success", @@ -1306,7 +1304,7 @@ }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["The following port-channels have inactive port(s): ['Port-Channel42']"]}, + "expected": {"result": "failure", "messages": ["Port-Channel42 - Inactive port(s) - Ethernet8"]}, }, { "name": "success", @@ -1362,7 +1360,7 @@ "inputs": None, "expected": { "result": "failure", - "messages": ["The following port-channels have received illegal LACP packets on the following ports: [{'Port-Channel42': 'Ethernet8'}]"], + "messages": ["Port-Channel42 Interface: Ethernet8 - Illegal LACP packets found"], }, }, { @@ -1417,7 +1415,7 @@ }, "Loopback666": { "name": "Loopback666", - "interfaceStatus": "connected", + "interfaceStatus": "notconnect", "interfaceAddress": {"ipAddr": {"maskLen": 32, "address": "6.6.6.6"}}, "ipv4Routable240": False, "lineProtocolStatus": "down", @@ -1427,7 +1425,13 @@ }, ], "inputs": {"number": 2}, - "expected": {"result": "failure", "messages": ["The following Loopbacks are not up: ['Loopback666']"]}, + "expected": { + "result": "failure", + "messages": [ + "Interface: Loopback666 - Invalid line protocol status - Expected: up Actual: down", + "Interface: Loopback666 - Invalid interface status - Expected: connected Actual: notconnect", + ], + }, }, { "name": "failure-count-loopback", @@ -1447,7 +1451,7 @@ }, ], "inputs": {"number": 2}, - "expected": {"result": "failure", "messages": ["Found 1 Loopbacks when expecting 2"]}, + "expected": {"result": "failure", "messages": ["Loopback interface(s) count mismatch: Expected 2 Actual: 1"]}, }, { "name": "success", @@ -1487,7 +1491,13 @@ }, ], "inputs": None, - "expected": {"result": "failure", "messages": ["The following SVIs are not up: ['Vlan42']"]}, + "expected": { + "result": "failure", + "messages": [ + "SVI: Vlan42 - Invalid line protocol status - Expected: up Actual: lowerLayerDown", + "SVI: Vlan42 - Invalid interface status - Expected: connected Actual: notconnect", + ], + }, }, { "name": "success", @@ -1703,7 +1713,79 @@ }, ], "inputs": {"mtu": 1500}, - "expected": {"result": "failure", "messages": ["Some interfaces do not have correct MTU configured:\n[{'Ethernet2': 1600}]"]}, + "expected": {"result": "failure", "messages": ["Interface: Ethernet2 - Incorrect MTU - Expected: 1500 Actual: 1600"]}, + }, + { + "name": "failure-specified-interface-mtu", + "test": VerifyL3MTU, + "eos_data": [ + { + "interfaces": { + "Ethernet2": { + "name": "Ethernet2", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1500, + "l3MtuConfigured": True, + "l2Mru": 0, + }, + "Ethernet10": { + "name": "Ethernet10", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1502, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Management0": { + "name": "Management0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "ethernet", + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Port-Channel2": { + "name": "Port-Channel2", + "forwardingModel": "bridged", + "lineProtocolStatus": "lowerLayerDown", + "interfaceStatus": "notconnect", + "hardware": "portChannel", + "mtu": 1500, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Loopback0": { + "name": "Loopback0", + "forwardingModel": "routed", + "lineProtocolStatus": "up", + "interfaceStatus": "connected", + "hardware": "loopback", + "mtu": 65535, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + "Vxlan1": { + "name": "Vxlan1", + "forwardingModel": "bridged", + "lineProtocolStatus": "down", + "interfaceStatus": "notconnect", + "hardware": "vxlan", + "mtu": 0, + "l3MtuConfigured": False, + "l2Mru": 0, + }, + }, + }, + ], + "inputs": {"mtu": 1500, "ignored_interfaces": ["Loopback", "Port-Channel", "Management", "Vxlan"], "specific_mtu": [{"Ethernet10": 1501}]}, + "expected": {"result": "failure", "messages": ["Interface: Ethernet10 - Incorrect MTU - Expected: 1501 Actual: 1502"]}, }, { "name": "success", diff --git a/tests/units/result_manager/conftest.py b/tests/units/result_manager/conftest.py index 0586d63cb..e414c4655 100644 --- a/tests/units/result_manager/conftest.py +++ b/tests/units/result_manager/conftest.py @@ -34,12 +34,12 @@ def _factory(number: int = 0) -> ResultManager: def result_manager() -> ResultManager: """Return a ResultManager with 30 random tests loaded from a JSON file. - Devices: DC1-SPINE1, DC1-LEAF1A + Devices: s1-spine1 - Total tests: 30 - - Success: 7 - - Skipped: 2 - - Failure: 19 + - Success: 4 + - Skipped: 9 + - Failure: 15 - Error: 2 See `tests/units/result_manager/test_md_report_results.json` for details. diff --git a/tests/units/result_manager/test__init__.py b/tests/units/result_manager/test__init__.py index 1ac309fd2..e70dbf9e5 100644 --- a/tests/units/result_manager/test__init__.py +++ b/tests/units/result_manager/test__init__.py @@ -195,12 +195,12 @@ def test_get_results(self, result_manager: ResultManager) -> None: """Test ResultManager.get_results.""" # Check for single status success_results = result_manager.get_results(status={AntaTestStatus.SUCCESS}) - assert len(success_results) == 7 + assert len(success_results) == 4 assert all(r.result == "success" for r in success_results) # Check for multiple statuses failure_results = result_manager.get_results(status={AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) - assert len(failure_results) == 21 + assert len(failure_results) == 17 assert all(r.result in {"failure", "error"} for r in failure_results) # Check all results @@ -212,19 +212,18 @@ def test_get_results_sort_by(self, result_manager: ResultManager) -> None: # Check all results with sort_by result all_results = result_manager.get_results(sort_by=["result"]) assert len(all_results) == 30 - assert [r.result for r in all_results] == ["error"] * 2 + ["failure"] * 19 + ["skipped"] * 2 + ["success"] * 7 + assert [r.result for r in all_results] == ["error"] * 2 + ["failure"] * 15 + ["skipped"] * 9 + ["success"] * 4 # Check all results with sort_by device (name) all_results = result_manager.get_results(sort_by=["name"]) assert len(all_results) == 30 - assert all_results[0].name == "DC1-LEAF1A" - assert all_results[-1].name == "DC1-SPINE1" + assert all_results[0].name == "s1-spine1" # Check multiple statuses with sort_by categories success_skipped_results = result_manager.get_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.SKIPPED}, sort_by=["categories"]) - assert len(success_skipped_results) == 9 - assert success_skipped_results[0].categories == ["Interfaces"] - assert success_skipped_results[-1].categories == ["VXLAN"] + assert len(success_skipped_results) == 13 + assert success_skipped_results[0].categories == ["avt"] + assert success_skipped_results[-1].categories == ["vxlan"] # Check all results with bad sort_by with pytest.raises( @@ -241,14 +240,14 @@ def test_get_total_results(self, result_manager: ResultManager) -> None: assert result_manager.get_total_results() == 30 # Test single status - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS}) == 7 - assert result_manager.get_total_results(status={AntaTestStatus.FAILURE}) == 19 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS}) == 4 + assert result_manager.get_total_results(status={AntaTestStatus.FAILURE}) == 15 assert result_manager.get_total_results(status={AntaTestStatus.ERROR}) == 2 - assert result_manager.get_total_results(status={AntaTestStatus.SKIPPED}) == 2 + assert result_manager.get_total_results(status={AntaTestStatus.SKIPPED}) == 9 # Test multiple statuses - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE}) == 26 - assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) == 28 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE}) == 19 + assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR}) == 21 assert result_manager.get_total_results(status={AntaTestStatus.SUCCESS, AntaTestStatus.FAILURE, AntaTestStatus.ERROR, AntaTestStatus.SKIPPED}) == 30 @pytest.mark.parametrize( diff --git a/tests/units/result_manager/test_files/test_md_report_results.json b/tests/units/result_manager/test_files/test_md_report_results.json index b9ecc0c57..ab932dc5b 100644 --- a/tests/units/result_manager/test_files/test_md_report_results.json +++ b/tests/units/result_manager/test_files/test_md_report_results.json @@ -1,378 +1,389 @@ [ - { - "name": "DC1-SPINE1", - "test": "VerifyTacacsSourceIntf", - "categories": [ - "AAA" - ], - "description": "Verifies TACACS source-interface for a specified VRF.", - "result": "failure", - "messages": [ - "Source-interface Management0 is not configured in VRF default" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyLLDPNeighbors", - "categories": [ - "Connectivity" - ], - "description": "Verifies that the provided LLDP neighbors are connected properly.", - "result": "failure", - "messages": [ - "Wrong LLDP neighbor(s) on port(s):\n Ethernet1\n DC1-LEAF1A_Ethernet1\n Ethernet2\n DC1-LEAF1B_Ethernet1\nPort(s) not configured:\n Ethernet7" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyBGPPeerCount", - "categories": [ - "BGP" - ], - "description": "Verifies the count of BGP peers.", - "result": "failure", - "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Not Configured', 'default': 'Expected: 3, Actual: 4'}}, {'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'DEV': 'Not Configured'}}, {'afi': 'evpn', 'vrfs': {'default': 'Expected: 2, Actual: 4'}}]" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifySTPMode", - "categories": [ - "STP" - ], - "description": "Verifies the configured STP mode for a provided list of VLAN(s).", - "result": "failure", - "messages": [ - "STP mode 'rapidPvst' not configured for the following VLAN(s): [10, 20]" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifySnmpStatus", - "categories": [ - "SNMP" - ], - "description": "Verifies if the SNMP agent is enabled.", - "result": "failure", - "messages": [ - "SNMP agent disabled in vrf default" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyRoutingTableEntry", - "categories": [ - "Routing" - ], - "description": "Verifies that the provided routes are present in the routing table of a specified VRF.", - "result": "failure", - "messages": [ - "The following route(s) are missing from the routing table of VRF default: ['10.1.0.2']" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyInterfaceUtilization", - "categories": [ - "Interfaces" - ], - "description": "Verifies that the utilization of interfaces is below a certain threshold.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyMlagStatus", - "categories": [ - "MLAG" - ], - "description": "Verifies the health status of the MLAG configuration.", - "result": "skipped", - "messages": [ - "MLAG is disabled" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyVxlan1Interface", - "categories": [ - "VXLAN" - ], - "description": "Verifies the Vxlan1 interface status.", - "result": "skipped", - "messages": [ - "Vxlan1 interface is not configured" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyBFDSpecificPeers", - "categories": [ - "BFD" - ], - "description": "Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF.", - "result": "failure", - "messages": [ - "Following BFD peers are not configured, status is not up or remote disc is zero:\n{'192.0.255.8': {'default': 'Not Configured'}, '192.0.255.7': {'default': 'Not Configured'}}" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyNTP", - "categories": [ - "System" - ], - "description": "Verifies if NTP is synchronised.", - "result": "failure", - "messages": [ - "The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyReachability", - "categories": [ - "Connectivity" - ], - "description": "Test the network reachability to one or many destination IP(s).", - "result": "error", - "messages": [ - "ping vrf MGMT 1.1.1.1 source Management1 repeat 2 has failed: No source interface Management1" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyTelnetStatus", - "categories": [ - "Security" - ], - "description": "Verifies if Telnet is disabled in the default VRF.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyEOSVersion", - "categories": [ - "Software" - ], - "description": "Verifies the EOS version of the device.", - "result": "failure", - "messages": [ - "device is running version \"4.31.1F-34554157.4311F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F']" - ], - "custom_field": null - }, - { - "name": "DC1-SPINE1", - "test": "VerifyHostname", - "categories": [ - "Services" - ], - "description": "Verifies the hostname of a device.", - "result": "failure", - "messages": [ - "Expected `s1-spine1` as the hostname, but found `DC1-SPINE1` instead." - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyTacacsSourceIntf", - "categories": [ - "AAA" - ], - "description": "Verifies TACACS source-interface for a specified VRF.", - "result": "failure", - "messages": [ - "Source-interface Management0 is not configured in VRF default" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyLLDPNeighbors", - "categories": [ - "Connectivity" - ], - "description": "Verifies that the provided LLDP neighbors are connected properly.", - "result": "failure", - "messages": [ - "Wrong LLDP neighbor(s) on port(s):\n Ethernet1\n DC1-SPINE1_Ethernet1\n Ethernet2\n DC1-SPINE2_Ethernet1\nPort(s) not configured:\n Ethernet7" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyBGPPeerCount", - "categories": [ - "BGP" - ], - "description": "Verifies the count of BGP peers.", - "result": "failure", - "messages": [ - "Failures: [{'afi': 'ipv4', 'safi': 'unicast', 'vrfs': {'PROD': 'Expected: 2, Actual: 1'}}, {'afi': 'ipv4', 'safi': 'multicast', 'vrfs': {'DEV': 'Expected: 3, Actual: 0'}}]" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifySTPMode", - "categories": [ - "STP" - ], - "description": "Verifies the configured STP mode for a provided list of VLAN(s).", - "result": "failure", - "messages": [ - "Wrong STP mode configured for the following VLAN(s): [10, 20]" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifySnmpStatus", - "categories": [ - "SNMP" - ], - "description": "Verifies if the SNMP agent is enabled.", - "result": "failure", - "messages": [ - "SNMP agent disabled in vrf default" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyRoutingTableEntry", - "categories": [ - "Routing" - ], - "description": "Verifies that the provided routes are present in the routing table of a specified VRF.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyInterfaceUtilization", - "categories": [ - "Interfaces" - ], - "description": "Verifies that the utilization of interfaces is below a certain threshold.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyMlagStatus", - "categories": [ - "MLAG" - ], - "description": "Verifies the health status of the MLAG configuration.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyVxlan1Interface", - "categories": [ - "VXLAN" - ], - "description": "Verifies the Vxlan1 interface status.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyBFDSpecificPeers", - "categories": [ - "BFD" - ], - "description": "Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF.", - "result": "failure", - "messages": [ - "Following BFD peers are not configured, status is not up or remote disc is zero:\n{'192.0.255.8': {'default': 'Not Configured'}, '192.0.255.7': {'default': 'Not Configured'}}" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyNTP", - "categories": [ - "System" - ], - "description": "Verifies if NTP is synchronised.", - "result": "failure", - "messages": [ - "The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyReachability", - "categories": [ - "Connectivity" - ], - "description": "Test the network reachability to one or many destination IP(s).", - "result": "error", - "messages": [ - "ping vrf MGMT 1.1.1.1 source Management1 repeat 2 has failed: No source interface Management1" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyTelnetStatus", - "categories": [ - "Security" - ], - "description": "Verifies if Telnet is disabled in the default VRF.", - "result": "success", - "messages": [], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyEOSVersion", - "categories": [ - "Software" - ], - "description": "Verifies the EOS version of the device.", - "result": "failure", - "messages": [ - "device is running version \"4.31.1F-34554157.4311F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F']" - ], - "custom_field": null - }, - { - "name": "DC1-LEAF1A", - "test": "VerifyHostname", - "categories": [ - "Services" - ], - "description": "Verifies the hostname of a device.", - "result": "failure", - "messages": [ - "Expected `s1-spine1` as the hostname, but found `DC1-LEAF1A` instead." - ], - "custom_field": null - } + { + "name": "s1-spine1", + "test": "VerifyMlagDualPrimary", + "categories": [ + "mlag" + ], + "description": "Verifies the MLAG dual-primary detection parameters.", + "result": "failure", + "messages": [ + "Dual-primary detection is disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyHostname", + "categories": [ + "services" + ], + "description": "Verifies the hostname of a device.", + "result": "failure", + "messages": [ + "Incorrect Hostname - Expected: s1-spine1 Actual: leaf1-dc1" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBGPAdvCommunities", + "categories": [ + "bgp" + ], + "description": "Verifies that advertised communities are standard, extended and large for BGP IPv4 peer(s).", + "result": "error", + "messages": [ + "show bgp neighbors vrf all has failed: The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model., The command is only supported in the multi-agent routing protocol model." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStunClient", + "categories": [ + "stun" + ], + "description": "(Deprecated) Verifies the translation for a source address on a STUN client.", + "result": "failure", + "messages": [ + "Client 172.18.3.2 Port: 4500 - STUN client translation not found." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBannerLogin", + "categories": [ + "security" + ], + "description": "Verifies the login banner of a device.", + "result": "failure", + "messages": [ + "Expected `# Copyright (c) 2023-2024 Arista Networks, Inc.\n# Use of this source code is governed by the Apache License 2.0\n# that can be found in the LICENSE file.\n` as the login banner, but found `` instead." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyISISNeighborState", + "categories": [ + "isis" + ], + "description": "Verifies the health of IS-IS neighbors.", + "result": "skipped", + "messages": [ + "IS-IS not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyEOSVersion", + "categories": [ + "software" + ], + "description": "Verifies the EOS version of the device.", + "result": "failure", + "messages": [ + "EOS version mismatch - Actual: 4.31.0F-33804048.4310F (engineering build) not in Expected: 4.25.4M, 4.26.1F" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTcamProfile", + "categories": [ + "profiles" + ], + "description": "Verifies the device TCAM profile.", + "result": "skipped", + "messages": [ + "VerifyTcamProfile test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPathsHealth", + "categories": [ + "path-selection" + ], + "description": "Verifies the path and telemetry state of all paths under router path-selection.", + "result": "skipped", + "messages": [ + "VerifyPathsHealth test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBannerMotd", + "categories": [ + "security" + ], + "description": "Verifies the motd banner of a device.", + "result": "failure", + "messages": [ + "Expected `# Copyright (c) 2023-2024 Arista Networks, Inc.\n# Use of this source code is governed by the Apache License 2.0\n# that can be found in the LICENSE file.\n` as the motd banner, but found `` instead." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyFieldNotice44Resolution", + "categories": [ + "field notices" + ], + "description": "Verifies that the device is using the correct Aboot version per FN0044.", + "result": "skipped", + "messages": [ + "VerifyFieldNotice44Resolution test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLoggingHosts", + "categories": [ + "logging" + ], + "description": "Verifies logging hosts (syslog servers) for a specified VRF.", + "result": "failure", + "messages": [ + "Syslog servers 1.1.1.1, 2.2.2.2 are not configured in VRF default" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAVTPathHealth", + "categories": [ + "avt" + ], + "description": "Verifies the status of all AVT paths for all VRFs.", + "result": "skipped", + "messages": [ + "VerifyAVTPathHealth test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyTemperature", + "categories": [ + "hardware" + ], + "description": "Verifies if the device temperature is within acceptable limits.", + "result": "skipped", + "messages": [ + "VerifyTemperature test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyNTPAssociations", + "categories": [ + "system" + ], + "description": "Verifies the Network Time Protocol (NTP) associations.", + "result": "failure", + "messages": [ + "NTP Server: 1.1.1.1 Preferred: True Stratum: 1 - Not configured", + "NTP Server: 2.2.2.2 Preferred: False Stratum: 2 - Not configured", + "NTP Server: 3.3.3.3 Preferred: False Stratum: 2 - Not configured" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyDynamicVlanSource", + "categories": [ + "vlan" + ], + "description": "Verifies dynamic VLAN allocation for specified VLAN sources.", + "result": "failure", + "messages": [ + "Dynamic VLAN source(s) exist but have no VLANs allocated: mlagsync" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyActiveCVXConnections", + "categories": [ + "cvx" + ], + "description": "Verifies the number of active CVX Connections.", + "result": "error", + "messages": [ + "show cvx connections brief has failed: Unavailable command (controller not ready) (at token 2: 'connections')" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPv4RouteNextHops", + "categories": [ + "routing" + ], + "description": "Verifies the next-hops of the IPv4 prefixes.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyVxlan1ConnSettings", + "categories": [ + "vxlan" + ], + "description": "Verifies the interface vxlan1 source interface and UDP port.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyStunClientTranslation", + "categories": [ + "stun" + ], + "description": "Verifies the translation for a source address on a STUN client.", + "result": "failure", + "messages": [ + "Client 172.18.3.2 Port: 4500 - STUN client translation not found.", + "Client 100.64.3.2 Port: 4500 - STUN client translation not found." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyPtpGMStatus", + "categories": [ + "ptp" + ], + "description": "Verifies that the device is locked to a valid PTP Grandmaster.", + "result": "skipped", + "messages": [ + "VerifyPtpGMStatus test is not supported on cEOSLab." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyRunningConfigDiffs", + "categories": [ + "configuration" + ], + "description": "Verifies there is no difference between the running-config and the startup-config.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyBFDPeersHealth", + "categories": [ + "bfd" + ], + "description": "Verifies the health of IPv4 BFD peers across all VRFs.", + "result": "failure", + "messages": [ + "No IPv4 BFD peers are configured for any VRF." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyIPProxyARP", + "categories": [ + "interfaces" + ], + "description": "Verifies if Proxy ARP is enabled.", + "result": "failure", + "messages": [ + "Interface: Ethernet1 - Proxy-ARP disabled", + "Interface: Ethernet2 - Proxy-ARP disabled" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySnmpContact", + "categories": [ + "snmp" + ], + "description": "Verifies the SNMP contact of a device.", + "result": "failure", + "messages": [ + "SNMP contact is not configured." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLLDPNeighbors", + "categories": [ + "connectivity" + ], + "description": "Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.", + "result": "failure", + "messages": [ + "Port: Ethernet1 Neighbor: DC1-SPINE1 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine1-dc1.fun.aristanetworks.com/Ethernet3", + "Port: Ethernet2 Neighbor: DC1-SPINE2 Neighbor Port: Ethernet1 - Wrong LLDP neighbors: spine2-dc1.fun.aristanetworks.com/Ethernet3" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyAcctConsoleMethods", + "categories": [ + "aaa" + ], + "description": "Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).", + "result": "failure", + "messages": [ + "AAA console accounting is not configured for commands, exec, system, dot1x" + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyOSPFMaxLSA", + "categories": [ + "ospf" + ], + "description": "Verifies all OSPF instances did not cross the maximum LSA threshold.", + "result": "skipped", + "messages": [ + "No OSPF instance found." + ], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifySTPBlockedPorts", + "categories": [ + "stp" + ], + "description": "Verifies there is no STP blocked ports.", + "result": "success", + "messages": [], + "custom_field": null + }, + { + "name": "s1-spine1", + "test": "VerifyLANZ", + "categories": [ + "lanz" + ], + "description": "Verifies if LANZ is enabled.", + "result": "skipped", + "messages": [ + "VerifyLANZ test is not supported on cEOSLab." + ], + "custom_field": null + } ]