Skip to content

Commit

Permalink
feat(anta.tests): MLAG tests update (#246)
Browse files Browse the repository at this point in the history
* Update MLAG tests
  • Loading branch information
carl-baillargeon authored Jul 6, 2023
1 parent e4d0326 commit 40865d2
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 50 deletions.
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,4 @@ repos:
- types-PyYAML
- types-paramiko
- types-requests
# Do not run mypy on tests but only on code and scripts
files: ^(anta|scripts|tests)/
203 changes: 172 additions & 31 deletions anta/tests/mlag.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,234 @@
"""
Test functions related to Multi-Chassis LAG
Test functions related to Multi-chassis Link Aggregation (MLAG)
"""
import logging
from typing import Any, Dict, cast
from typing import Any, Dict, Optional, cast

from anta.models import AntaTest, AntaTestCommand
from anta.tools.get_value import get_value

logger = logging.getLogger(__name__)


class VerifyMlagStatus(AntaTest):
"""
Verifies if MLAG us running, and if the status is good
state is active, negotiation status is connected, local int is up, peer link is up.
This test verifies the health status of the MLAG configuration.
Expected Results:
* success: The test will pass if the MLAG state is 'active', negotiation status is 'connected',
peer-link status and local interface status are 'up'.
* failure: The test will fail if the MLAG state is not 'active', negotiation status is not 'connected',
peer-link status or local interface status are not 'up'.
* skipped: The test will be skipped if MLAG is 'disabled'.
"""

name = "VerifyMlagStatus"
description = "Verifies MLAG status"
description = "This test verifies the health status of the MLAG configuration."
categories = ["mlag"]
commands = [AntaTestCommand(command="show mlag", ofmt="json")]

@AntaTest.anta_test
def test(self) -> None:
"""Run VerifyMlagStatus validation"""
"""
Run VerifyMlagStatus validation
"""

command_output = cast(Dict[str, Dict[str, Any]], self.instance_commands[0].output)

if command_output["state"] == "disabled":
self.result.is_skipped("MLAG is disabled")
elif (
command_output["state"] != "active"
or command_output["negStatus"] != "connected"
or command_output["localIntfStatus"] != "up"
or command_output["peerLinkStatus"] != "up"
return

keys_to_verify = ["state", "negStatus", "localIntfStatus", "peerLinkStatus"]
verified_output = {key: get_value(command_output, key) for key in keys_to_verify}

if (
verified_output["state"] == "active"
and verified_output["negStatus"] == "connected"
and verified_output["localIntfStatus"] == "up"
and verified_output["peerLinkStatus"] == "up"
):
self.result.is_failure(f"MLAG status is not OK: {command_output}")
else:
self.result.is_success()
else:
self.result.is_failure(f"MLAG status is not OK: {verified_output}")


class VerifyMlagInterfaces(AntaTest):
"""
Verifies there are no inactive or active-partial MLAG interfaces.
This test verifies there are no inactive or active-partial MLAG ports.
Expected Results:
* success: The test will pass if there are NO inactive or active-partial MLAG ports.
* failure: The test will fail if there are inactive or active-partial MLAG ports.
* skipped: The test will be skipped if MLAG is 'disabled'.
"""

name = "VerifyMlagInterfaces"
description = "Verifies there are no inactive or active-partial MLAG interfaces."
description = "This test verifies there are no inactive or active-partial MLAG ports."
categories = ["mlag"]
commands = [AntaTestCommand(command="show mlag", ofmt="json")]

@AntaTest.anta_test
def test(self) -> None:
"""Run VerifyMlagInterfaces validation"""
"""
Run VerifyMlagInterfaces validation
"""

command_output = cast(Dict[str, Dict[str, Any]], self.instance_commands[0].output)

if command_output["state"] == "disabled":
self.result.is_skipped("MLAG is disabled")
elif command_output["mlagPorts"]["Inactive"] != 0 or command_output["mlagPorts"]["Active-partial"] != 0:
self.result.is_failure(f"MLAG status is not OK: {command_output['mlagPorts']}")
else:
return

if command_output["mlagPorts"]["Inactive"] == 0 and command_output["mlagPorts"]["Active-partial"] == 0:
self.result.is_success()
else:
self.result.is_failure(f"MLAG status is not OK: {command_output['mlagPorts']}")


class VerifyMlagConfigSanity(AntaTest):
"""
Verifies there are no MLAG config-sanity inconsistencies.
This test verifies there are no MLAG config-sanity inconsistencies.
Expected Results:
* success: The test will pass if there are NO MLAG config-sanity inconsistencies.
* failure: The test will fail if there are MLAG config-sanity inconsistencies.
* skipped: The test will be skipped if MLAG is 'disabled'.
* error: The test will give an error if 'mlagActive' is not found in the JSON response.
"""

name = "VerifyMlagConfigSanity"
description = "Verifies there are no MLAG config-sanity inconsistencies."
description = "This test verifies there are no MLAG config-sanity inconsistencies."
categories = ["mlag"]
commands = [AntaTestCommand(command="show mlag config-sanity", ofmt="json")]

@AntaTest.anta_test
def test(self) -> None:
"""Run VerifyMlagConfigSanity validation"""
"""
Run VerifyMlagConfigSanity validation
"""

command_output = cast(Dict[str, Dict[str, Any]], self.instance_commands[0].output)

if "mlagActive" not in command_output.keys():
self.result.is_error("Incorrect JSON response - mlagActive state not found")
elif command_output["mlagActive"] is False:
if (mlag_status := get_value(command_output, "mlagActive")) is None:
self.result.is_error("Incorrect JSON response - 'mlagActive' state was not found")
return

if mlag_status is False:
self.result.is_skipped("MLAG is disabled")
elif len(command_output["globalConfiguration"]) > 0 or len(command_output["interfaceConfiguration"]) > 0:
self.result.is_failure()
if len(command_output["globalConfiguration"]) > 0:
self.result.is_failure("MLAG config-sanity returned Global inconsistancies: " f"{command_output['globalConfiguration']}")
if len(command_output["interfaceConfiguration"]) > 0:
self.result.is_failure("MLAG config-sanity returned Interface inconsistancies: " f"{command_output['interfaceConfiguration']}")
return

keys_to_verify = ["globalConfiguration", "interfaceConfiguration"]
verified_output = {key: get_value(command_output, key) for key in keys_to_verify}

if not any(verified_output.values()):
self.result.is_success()
else:
self.result.is_failure(f"MLAG config-sanity returned inconsistencies: {verified_output}")


class VerifyMlagReloadDelay(AntaTest):
"""
This test verifies the reload-delay parameters of the MLAG configuration.
Expected Results:
* success: The test will pass if the reload-delay parameters are configured properly.
* failure: The test will fail if the reload-delay parameters are NOT configured properly.
* skipped: The test will be skipped if the reload-delay parameters are NOT provided or if MLAG is 'disabled'.
"""

name = "VerifyMlagReloadDelay"
description = "This test verifies the reload-delay parameters of the MLAG configuration."
categories = ["mlag"]
commands = [AntaTestCommand(command="show mlag", ofmt="json")]

@AntaTest.anta_test
def test(self, reload_delay: Optional[int] = None, reload_delay_non_mlag: Optional[int] = None) -> None:
"""
Run VerifyMlagReloadDelay validation
Args:
reload_delay: Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled.
reload_delay_non_mlag: Delay (seconds) after reboot until ports that are not part of an MLAG are enabled.
"""

if not reload_delay or not reload_delay_non_mlag:
self.result.is_skipped(f"{self.__class__.name} did not run because reload_delay or reload_delay_non_mlag were not supplied")
return

command_output = cast(Dict[str, Dict[str, Any]], self.instance_commands[0].output)

if command_output["state"] == "disabled":
self.result.is_skipped("MLAG is disabled")
return

keys_to_verify = ["reloadDelay", "reloadDelayNonMlag"]
verified_output = {key: get_value(command_output, key) for key in keys_to_verify}

if verified_output["reloadDelay"] == reload_delay and verified_output["reloadDelayNonMlag"] == reload_delay_non_mlag:
self.result.is_success()

else:
self.result.is_failure(f"The reload-delay parameters are not configured properly: {verified_output}")


class VerifyMlagDualPrimary(AntaTest):
"""
This test verifies the dual-primary detection and its parameters of the MLAG configuration.
Expected Results:
* success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly.
* failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly.
* skipped: The test will be skipped if the dual-primary parameters are NOT provided or if MLAG is 'disabled'.
"""

name = "VerifyMlagDualPrimary"
description = "This test verifies the dual-primary detection and its parameters of the MLAG configuration."
categories = ["mlag"]
commands = [AntaTestCommand(command="show mlag detail", ofmt="json")]

@AntaTest.anta_test
def test(
self, detection_delay: Optional[int] = None, errdisabled: bool = False, recovery_delay: Optional[int] = None, recovery_delay_non_mlag: Optional[int] = None
) -> None:
"""
Run VerifyMlagDualPrimary validation
Args:
detection_delay: Delay detection for <N> seconds.
errdisabled: Errdisabled all interfaces when dual-primary is detected. Defaults to False.
recovery_delay: Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled.
recovery_delay_non_mlag: Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled.
"""

if detection_delay is None or errdisabled is None or recovery_delay is None or recovery_delay_non_mlag is None:
self.result.is_skipped(
f"{self.__class__.name} did not run because detection_delay, errdisabled, recovery_delay or recovery_delay_non_mlag were not supplied"
)
return

errdisabled_action = "errdisableAllInterfaces" if errdisabled else "none"

command_output = cast(Dict[str, Dict[str, Any]], self.instance_commands[0].output)

if command_output["state"] == "disabled":
self.result.is_skipped("MLAG is disabled")
return

if command_output["dualPrimaryDetectionState"] == "disabled":
self.result.is_failure("Dual-primary detection is disabled")
return

keys_to_verify = ["detail.dualPrimaryDetectionDelay", "detail.dualPrimaryAction", "dualPrimaryMlagRecoveryDelay", "dualPrimaryNonMlagRecoveryDelay"]
verified_output = {key: get_value(command_output, key) for key in keys_to_verify}

if (
verified_output["detail.dualPrimaryDetectionDelay"] == detection_delay
and verified_output["detail.dualPrimaryAction"] == errdisabled_action
and verified_output["dualPrimaryMlagRecoveryDelay"] == recovery_delay
and verified_output["dualPrimaryNonMlagRecoveryDelay"] == recovery_delay_non_mlag
):
self.result.is_success()

else:
self.result.is_failure(f"The dual-primary parameters are not configured properly: {verified_output}")
8 changes: 8 additions & 0 deletions examples/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ anta.tests.mlag:
- VerifyMlagStatus:
- VerifyMlagInterfaces:
- VerifyMlagConfigSanity:
- VerifyMlagReloadDelay:
reload_delay: 300
reload_delay_non_mlag: 330
- VerifyMlagDualPrimary:
detection_delay: 200
errdisabled: True
recovery_delay: 60
recovery_delay_non_mlag: 0

anta.tests.multicast:
- VerifyIGMPSnoopingVlans:
Expand Down
Loading

0 comments on commit 40865d2

Please sign in to comment.