diff --git a/anta/tests/cvx.py b/anta/tests/cvx.py index fc369a893..61600829c 100644 --- a/anta/tests/cvx.py +++ b/anta/tests/cvx.py @@ -7,7 +7,7 @@ # mypy: disable-error-code=attr-defined from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Literal +from typing import TYPE_CHECKING, Any, ClassVar, Literal from anta.custom_types import PositiveInteger from anta.models import AntaCommand, AntaTest @@ -91,6 +91,81 @@ def test(self) -> None: self.result.is_failure(f"Management CVX status is not valid: {cluster_state}") +class VerifyMcsServerMounts(AntaTest): + """Verify if all MCS server mounts are in a MountComplete state. + + Expected Results + ---------------- + * Success: The test will pass if all the MCS mount status on MCS server are mountStateMountComplete. + * Failure: The test will fail even if any MCS server mount status is not mountStateMountComplete. + + Examples + -------- + ```yaml + anta.tests.cvx: + + - VerifyMcsServerMounts: + connections_count: 100 + ``` + """ + + categories: ClassVar[list[str]] = ["cvx"] + commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show cvx mounts", revision=1)] + + mcs_path_types: ClassVar[list[str]] = ["Mcs::ApiConfigRedundancyStatus", "Mcs::ActiveFlows", "Mcs::Client::Status"] + """The list of expected MCS path types to verify.""" + + class Input(AntaTest.Input): + """Input model for the VerifyMcsServerMounts test.""" + + connections_count: int + """The expected number of active CVX Connections with mountStateMountComplete""" + + def validate_mount_states(self, mount: dict[str, Any], hostname: str) -> None: + """Validate the mount states of a given mount.""" + mount_states = mount["mountStates"][0] + + if (num_path_states := len(mount_states["pathStates"])) != (expected_num := len(self.mcs_path_types)): + self.result.is_failure(f"Incorrect number of mount path states for {hostname} - Expected: {expected_num}, Actual: {num_path_states}") + + for path in mount_states["pathStates"]: + if (path_type := path.get("type")) not in self.mcs_path_types: + self.result.is_failure(f"Unexpected MCS path type for {hostname}: '{path_type}'.") + if (path_state := path.get("state")) != "mountStateMountComplete": + self.result.is_failure(f"MCS server mount state for path '{path_type}' is not valid is for {hostname}: '{path_state}'.") + + @AntaTest.anta_test + def test(self) -> None: + """Main test function for VerifyMcsServerMounts.""" + command_output = self.instance_commands[0].json_output + self.result.is_success() + active_count = 0 + + if not (connections := command_output.get("connections")): + self.result.is_failure("CVX connections are not available.") + return + + for connection in connections: + mounts = connection.get("mounts", []) + hostname = connection["hostname"] + + mcs_mounts = [mount for mount in mounts if mount["service"] == "Mcs"] + + if not mounts: + self.result.is_failure(f"No mount status for {hostname}") + continue + + if not mcs_mounts: + self.result.is_failure(f"MCS mount state not detected for {hostname}") + else: + for mount in mcs_mounts: + self.validate_mount_states(mount, hostname) + active_count += 1 + + if active_count != self.inputs.connections_count: + self.result.is_failure(f"Incorrect CVX successful connections count. Expected: {self.inputs.connections_count}, Actual : {active_count}") + + class VerifyActiveCVXConnections(AntaTest): """Verifies the number of active CVX Connections. @@ -116,7 +191,7 @@ class Input(AntaTest.Input): """Input model for the VerifyActiveCVXConnections test.""" connections_count: PositiveInteger - """The expected number of active CVX Connections""" + """The expected number of active CVX Connections.""" @AntaTest.anta_test def test(self) -> None: @@ -131,7 +206,7 @@ def test(self) -> None: active_count = len([connection for connection in connections if connection.get("oobConnectionActive")]) if self.inputs.connections_count != active_count: - self.result.is_failure(f"CVX active connections count. Expected: {self.inputs.connections_count} , Actual : {active_count}") + self.result.is_failure(f"CVX active connections count. Expected: {self.inputs.connections_count}, Actual : {active_count}") class VerifyCVXClusterStatus(AntaTest): diff --git a/examples/tests.yaml b/examples/tests.yaml index d8b95f30f..d2a029986 100644 --- a/examples/tests.yaml +++ b/examples/tests.yaml @@ -151,6 +151,9 @@ anta.tests.cvx: enabled: true - VerifyMcsClientMounts: # Verify if all MCS client mounts are in mountStateMountComplete. + - VerifyMcsServerMounts: + # Verify if all MCS server mounts are in a MountComplete state. + connections_count: 100 anta.tests.field_notices: - VerifyFieldNotice44Resolution: # Verifies that the device is using the correct Aboot version per FN0044. diff --git a/tests/units/anta_tests/test_cvx.py b/tests/units/anta_tests/test_cvx.py index 0b1105cb0..46d83b02a 100644 --- a/tests/units/anta_tests/test_cvx.py +++ b/tests/units/anta_tests/test_cvx.py @@ -7,7 +7,7 @@ from typing import Any -from anta.tests.cvx import VerifyActiveCVXConnections, VerifyCVXClusterStatus, VerifyManagementCVX, VerifyMcsClientMounts +from anta.tests.cvx import VerifyActiveCVXConnections, VerifyCVXClusterStatus, VerifyManagementCVX, VerifyMcsClientMounts, VerifyMcsServerMounts from tests.units.anta_tests import test DATA: list[dict[str, Any]] = [ @@ -146,6 +146,175 @@ "inputs": {"enabled": False}, "expected": {"result": "failure", "messages": ["Management CVX status is not valid: None"]}, }, + { + "name": "failure - no clusterStatus", + "test": VerifyManagementCVX, + "eos_data": [{}], + "inputs": {"enabled": False}, + "expected": {"result": "failure", "messages": ["Management CVX status is not valid: None"]}, + }, + { + "name": "success", + "test": VerifyMcsServerMounts, + "eos_data": [ + { + "connections": [ + { + "hostname": "media-leaf-1", + "mounts": [ + { + "service": "Mcs", + "mountStates": [ + { + "pathStates": [ + {"path": "mcs/v1/apiCfgRedStatus", "type": "Mcs::ApiConfigRedundancyStatus", "state": "mountStateMountComplete"}, + {"path": "mcs/v1/activeflows", "type": "Mcs::ActiveFlows", "state": "mountStateMountComplete"}, + {"path": "mcs/switch/status", "type": "Mcs::Client::Status", "state": "mountStateMountComplete"}, + ] + } + ], + } + ], + } + ] + } + ], + "inputs": {"connections_count": 1}, + "expected": {"result": "success"}, + }, + { + "name": "failure-no-mounts", + "test": VerifyMcsServerMounts, + "eos_data": [{"connections": [{"hostname": "media-leaf-1", "mounts": []}]}], + "inputs": {"connections_count": 1}, + "expected": { + "result": "failure", + "messages": ["No mount status for media-leaf-1", "Incorrect CVX successful connections count. Expected: 1, Actual : 0"], + }, + }, + { + "name": "failure-unexpected-number-paths", + "test": VerifyMcsServerMounts, + "eos_data": [ + { + "connections": [ + { + "hostname": "media-leaf-1", + "mounts": [ + { + "service": "Mcs", + "mountStates": [ + { + "pathStates": [ + {"path": "mcs/v1/apiCfgRedStatus", "type": "Mcs::ApiStatus", "state": "mountStateMountComplete"}, + {"path": "mcs/v1/activeflows", "type": "Mcs::ActiveFlows", "state": "mountStateMountComplete"}, + ] + } + ], + } + ], + } + ] + } + ], + "inputs": {"connections_count": 1}, + "expected": { + "result": "failure", + "messages": [ + "Incorrect number of mount path states for media-leaf-1 - Expected: 3, Actual: 2", + "Unexpected MCS path type for media-leaf-1: 'Mcs::ApiStatus'.", + ], + }, + }, + { + "name": "failure-unexpected-path-type", + "test": VerifyMcsServerMounts, + "eos_data": [ + { + "connections": [ + { + "hostname": "media-leaf-1", + "mounts": [ + { + "service": "Mcs", + "mountStates": [ + { + "pathStates": [ + {"path": "mcs/v1/apiCfgRedStatus", "type": "Mcs::ApiStatus", "state": "mountStateMountComplete"}, + {"path": "mcs/v1/activeflows", "type": "Mcs::ActiveFlows", "state": "mountStateMountComplete"}, + {"path": "mcs/switch/status", "type": "Mcs::Client::Status", "state": "mountStateMountComplete"}, + ] + } + ], + } + ], + } + ] + } + ], + "inputs": {"connections_count": 1}, + "expected": {"result": "failure", "messages": ["Unexpected MCS path type for media-leaf-1: 'Mcs::ApiStatus'"]}, + }, + { + "name": "failure-invalid-mount-state", + "test": VerifyMcsServerMounts, + "eos_data": [ + { + "connections": [ + { + "hostname": "media-leaf-1", + "mounts": [ + { + "service": "Mcs", + "mountStates": [ + { + "pathStates": [ + {"path": "mcs/v1/apiCfgRedStatus", "type": "Mcs::ApiConfigRedundancyStatus", "state": "mountStateMountFailed"}, + {"path": "mcs/v1/activeflows", "type": "Mcs::ActiveFlows", "state": "mountStateMountComplete"}, + {"path": "mcs/switch/status", "type": "Mcs::Client::Status", "state": "mountStateMountComplete"}, + ] + } + ], + } + ], + } + ] + } + ], + "inputs": {"connections_count": 1}, + "expected": { + "result": "failure", + "messages": ["MCS server mount state for path 'Mcs::ApiConfigRedundancyStatus' is not valid is for media-leaf-1: 'mountStateMountFailed'"], + }, + }, + { + "name": "failure-no-mcs-mount", + "test": VerifyMcsServerMounts, + "eos_data": [ + { + "connections": [ + { + "hostname": "media-leaf-1", + "mounts": [ + { + "service": "blah-blah", + "mountStates": [{"pathStates": [{"path": "blah-blah-path", "type": "blah-blah-type", "state": "blah-blah-state"}]}], + } + ], + } + ] + } + ], + "inputs": {"connections_count": 1}, + "expected": {"result": "failure", "messages": ["MCS mount state not detected", "Incorrect CVX successful connections count. Expected: 1, Actual : 0"]}, + }, + { + "name": "failure-connections", + "test": VerifyMcsServerMounts, + "eos_data": [{}], + "inputs": {"connections_count": 1}, + "expected": {"result": "failure", "messages": ["CVX connections are not available."]}, + }, { "name": "success", "test": VerifyActiveCVXConnections, @@ -188,7 +357,7 @@ } ], "inputs": {"connections_count": 2}, - "expected": {"result": "failure", "messages": ["CVX active connections count. Expected: 2 , Actual : 1"]}, + "expected": {"result": "failure", "messages": ["CVX active connections count. Expected: 2, Actual : 1"]}, }, { "name": "failure-no-connections", @@ -197,13 +366,6 @@ "inputs": {"connections_count": 2}, "expected": {"result": "failure", "messages": ["CVX connections are not available"]}, }, - { - "name": "failure - no clusterStatus", - "test": VerifyManagementCVX, - "eos_data": [{}], - "inputs": {"enabled": False}, - "expected": {"result": "failure", "messages": ["Management CVX status is not valid: None"]}, - }, { "name": "success-all", "test": VerifyCVXClusterStatus,